mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-28 13:48:51 +08:00
Merge pull request #149 from mum4k/button
Implementing the Button widget.
This commit is contained in:
commit
1d3071e969
11
CHANGELOG.md
11
CHANGELOG.md
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- The Button widget.
|
||||||
- A function that draws text vertically.
|
- A function that draws text vertically.
|
||||||
- The LineChart widget can display X axis labels in vertical orientation.
|
- The LineChart widget can display X axis labels in vertical orientation.
|
||||||
- The LineChart widget allows the user to specify a custom scale for the Y
|
- The LineChart widget allows the user to specify a custom scale for the Y
|
||||||
@ -34,10 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
subscriber.
|
subscriber.
|
||||||
- The infrastructure now throttles event driven screen redraw rather than
|
- The infrastructure now throttles event driven screen redraw rather than
|
||||||
redrawing for each input event.
|
redrawing for each input event.
|
||||||
- Widgets can now specify the scope at which they want to receive keyboard
|
- Widgets can now specify the scope at which they want to receive keyboard and
|
||||||
events, i.e. KeyScopeNone for no events, KeyScopeFocused to receive events
|
mouse events.
|
||||||
only if the parent container is focused and KeyScopeGlobal to receive all
|
|
||||||
keyboard events.
|
|
||||||
|
|
||||||
#### Breaking API changes
|
#### Breaking API changes
|
||||||
|
|
||||||
@ -55,8 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
distribution system. This shouldn't affect users as the removed methods
|
distribution system. This shouldn't affect users as the removed methods
|
||||||
aren't needed by container users.
|
aren't needed by container users.
|
||||||
- The widgetapi.Options struct now uses an enum instead of a boolean when
|
- The widgetapi.Options struct now uses an enum instead of a boolean when
|
||||||
widget specifies if it wants keyboard events. This affects development of new
|
widget specifies if it wants keyboard or mouse events. This only impacts
|
||||||
widgets.
|
development of new widgets.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
14
README.md
14
README.md
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
# termdash
|
# termdash
|
||||||
|
|
||||||
[<img src="./images/termdashdemo_0_6_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
|
[<img src="./images/termdashdemo_0_7_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
|
||||||
|
|
||||||
This project implements a cross-platform customizable terminal based dashboard.
|
This project implements a cross-platform customizable terminal based dashboard.
|
||||||
The feature set is inspired by the
|
The feature set is inspired by the
|
||||||
@ -62,6 +62,18 @@ Project documentation is available in the [doc](doc/) directory.
|
|||||||
|
|
||||||
## Implemented Widgets
|
## Implemented Widgets
|
||||||
|
|
||||||
|
### The Button
|
||||||
|
|
||||||
|
Allows users to interact with the application, each button press runs a callback function.
|
||||||
|
Run the
|
||||||
|
[buttondemo](widgets/button/buttondemo/buttondemo.go).
|
||||||
|
|
||||||
|
```go
|
||||||
|
go run github.com/mum4k/termdash/widgets/button/buttondemo/buttondemo.go
|
||||||
|
```
|
||||||
|
|
||||||
|
[<img src="./images/buttondemo.gif" alt="buttondemo" type="image/gif">](widgets/button/buttondemo/buttondemo.go)
|
||||||
|
|
||||||
### The Gauge
|
### The Gauge
|
||||||
|
|
||||||
Displays the progress of an operation. Run the
|
Displays the progress of an operation. Run the
|
||||||
|
@ -295,7 +295,7 @@ func TestBraille(t *testing.T) {
|
|||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "SetCellOptions sets the cell options in full area",
|
desc: "SetAreaCellOptions sets the cell options in full area",
|
||||||
ar: image.Rect(0, 0, 1, 1),
|
ar: image.Rect(0, 0, 1, 1),
|
||||||
pixelOps: func(c *Canvas) error {
|
pixelOps: func(c *Canvas) error {
|
||||||
return c.SetAreaCellOpts(image.Rect(0, 0, 1, 1), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
return c.SetAreaCellOpts(image.Rect(0, 0, 1, 1), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
@ -315,7 +315,7 @@ func TestBraille(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "SetCellOptions sets the cell options in a sub-area",
|
desc: "SetAreaCellOptions sets the cell options in a sub-area",
|
||||||
ar: image.Rect(0, 0, 3, 3),
|
ar: image.Rect(0, 0, 3, 3),
|
||||||
pixelOps: func(c *Canvas) error {
|
pixelOps: func(c *Canvas) error {
|
||||||
return c.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
return c.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mum4k/termdash/area"
|
"github.com/mum4k/termdash/area"
|
||||||
"github.com/mum4k/termdash/cell"
|
"github.com/mum4k/termdash/cell"
|
||||||
|
"github.com/mum4k/termdash/cell/runewidth"
|
||||||
"github.com/mum4k/termdash/terminalapi"
|
"github.com/mum4k/termdash/terminalapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -94,6 +95,72 @@ func (c *Canvas) Cell(p image.Point) (*cell.Cell, error) {
|
|||||||
return c.buffer[p.X][p.Y].Copy(), nil
|
return c.buffer[p.X][p.Y].Copy(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCellOpts sets options on the specified cell of the canvas without
|
||||||
|
// modifying the content of the cell.
|
||||||
|
// Sets the default cell options if no options are provided.
|
||||||
|
// This method is idempotent.
|
||||||
|
func (c *Canvas) SetCellOpts(p image.Point, opts ...cell.Option) error {
|
||||||
|
curCell, err := c.Cell(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts) == 0 {
|
||||||
|
// Set the default options.
|
||||||
|
opts = []cell.Option{
|
||||||
|
cell.FgColor(cell.ColorDefault),
|
||||||
|
cell.BgColor(cell.ColorDefault),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := c.SetCell(p, curCell.Rune, opts...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAreaCells is like SetCell, but sets the specified rune and options on all
|
||||||
|
// the cells within the provided area.
|
||||||
|
// This method is idempotent.
|
||||||
|
func (c *Canvas) SetAreaCells(cellArea image.Rectangle, r rune, opts ...cell.Option) error {
|
||||||
|
haveArea := c.Area()
|
||||||
|
if !cellArea.In(haveArea) {
|
||||||
|
return fmt.Errorf("unable to set cell runes in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := runewidth.RuneWidth(r)
|
||||||
|
for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
|
||||||
|
for col := cellArea.Min.X; col < cellArea.Max.X; {
|
||||||
|
p := image.Point{col, row}
|
||||||
|
if col+rw > cellArea.Max.X {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cells, err := c.SetCell(p, r, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
col += cells
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAreaCellOpts is like SetCellOpts, but sets the specified options on all
|
||||||
|
// the cells within the provided area.
|
||||||
|
func (c *Canvas) SetAreaCellOpts(cellArea image.Rectangle, opts ...cell.Option) error {
|
||||||
|
haveArea := c.Area()
|
||||||
|
if !cellArea.In(haveArea) {
|
||||||
|
return fmt.Errorf("unable to set cell options in area %v, it must fit inside the available cell area is %v", cellArea, haveArea)
|
||||||
|
}
|
||||||
|
for col := cellArea.Min.X; col < cellArea.Max.X; col++ {
|
||||||
|
for row := cellArea.Min.Y; row < cellArea.Max.Y; row++ {
|
||||||
|
if err := c.SetCellOpts(image.Point{col, row}, opts...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// setCellFunc is a function that sets cell content on a terminal or a canvas.
|
// setCellFunc is a function that sets cell content on a terminal or a canvas.
|
||||||
type setCellFunc func(image.Point, rune, ...cell.Option) error
|
type setCellFunc func(image.Point, rune, ...cell.Option) error
|
||||||
|
|
||||||
|
@ -100,6 +100,445 @@ func TestNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanvas(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
canvas image.Rectangle
|
||||||
|
ops func(*Canvas) error
|
||||||
|
want func(size image.Point) (*faketerm.Terminal, error)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "SetCellOpts fails on a point outside of the canvas",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetCellOpts(image.Point{1, 1})
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetCellOpts sets options on a cell with no options",
|
||||||
|
canvas: image.Rect(0, 0, 2, 2),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := cvs.Cell(image.Point{0, 1})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetCellOpts preserves cell rune",
|
||||||
|
canvas: image.Rect(0, 0, 2, 2),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X'); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetCellOpts overwrites options set previously",
|
||||||
|
canvas: image.Rect(0, 0, 2, 2),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorGreen), cell.BgColor(cell.ColorYellow))
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorGreen), cell.BgColor(cell.ColorYellow)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetCellOpts sets default options when no options provided",
|
||||||
|
canvas: image.Rect(0, 0, 2, 2),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cvs.SetCellOpts(image.Point{0, 1})
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X'); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetCellOpts is idempotent",
|
||||||
|
canvas: image.Rect(0, 0, 2, 2),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X'); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cvs.SetCellOpts(image.Point{0, 1}, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 1}, 'X', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCellOpts fails on area too large",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCellOpts sets options in the full canvas",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCellOpts(image.Rect(0, 0, 1, 1), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range []image.Point{
|
||||||
|
{0, 0},
|
||||||
|
} {
|
||||||
|
c, err := cvs.Cell(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := cvs.SetCell(p, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCellOpts sets options in a sub-area",
|
||||||
|
canvas: image.Rect(0, 0, 3, 3),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCellOpts(image.Rect(0, 0, 2, 2), cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range []image.Point{
|
||||||
|
{0, 0},
|
||||||
|
{0, 1},
|
||||||
|
{1, 0},
|
||||||
|
{1, 1},
|
||||||
|
} {
|
||||||
|
c, err := cvs.Cell(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := cvs.SetCell(p, c.Rune, cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCells sets cells in the full canvas",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r')
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 0}, 'r'); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCells is idempotent",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
if err := cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r'); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r')
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 0}, 'r'); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCells fails on area too large",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCells sets cell options",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCells(image.Rect(0, 0, 1, 1), 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cvs.SetCell(image.Point{0, 0}, 'r', cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCells sets cell in a sub-area",
|
||||||
|
canvas: image.Rect(0, 0, 3, 3),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), 'p')
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range []image.Point{
|
||||||
|
{0, 0},
|
||||||
|
{0, 1},
|
||||||
|
{1, 0},
|
||||||
|
{1, 1},
|
||||||
|
} {
|
||||||
|
if _, err := cvs.SetCell(p, 'p'); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCells sets full-width runes that fit",
|
||||||
|
canvas: image.Rect(0, 0, 3, 3),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCells(image.Rect(0, 0, 2, 2), '世')
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range []image.Point{
|
||||||
|
{0, 0},
|
||||||
|
{0, 1},
|
||||||
|
} {
|
||||||
|
if _, err := cvs.SetCell(p, '世'); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetAreaCells sets full-width runes that will leave a gap at the end of each row",
|
||||||
|
canvas: image.Rect(0, 0, 3, 3),
|
||||||
|
ops: func(cvs *Canvas) error {
|
||||||
|
return cvs.SetAreaCells(image.Rect(0, 0, 3, 3), '世')
|
||||||
|
},
|
||||||
|
want: func(size image.Point) (*faketerm.Terminal, error) {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs, err := New(ft.Area())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range []image.Point{
|
||||||
|
{0, 0},
|
||||||
|
{0, 1},
|
||||||
|
{0, 2},
|
||||||
|
} {
|
||||||
|
if _, err := cvs.SetCell(p, '世'); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.Apply(ft); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ft, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
cvs, err := New(tc.canvas)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.ops != nil {
|
||||||
|
err := tc.ops(cvs)
|
||||||
|
if (err != nil) != tc.wantErr {
|
||||||
|
t.Errorf("tc.ops => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size := cvs.Size()
|
||||||
|
got, err := faketerm.New(size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if err := cvs.Apply(got); err != nil {
|
||||||
|
t.Fatalf("cvs.Apply => %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var want *faketerm.Terminal
|
||||||
|
if tc.want != nil {
|
||||||
|
want, err = tc.want(size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tc.want => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w, err := faketerm.New(size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
want = w
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := faketerm.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("cvs.SetCellOpts => %v", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetCellAndApply(t *testing.T) {
|
func TestSetCellAndApply(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
desc string
|
desc string
|
||||||
|
@ -51,6 +51,13 @@ func MustSetCell(c *canvas.Canvas, p image.Point, r rune, opts ...cell.Option) i
|
|||||||
return cells
|
return cells
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustSetAreaCells sets the cells in the area or panics.
|
||||||
|
func MustSetAreaCells(c *canvas.Canvas, cellArea image.Rectangle, r rune, opts ...cell.Option) {
|
||||||
|
if err := c.SetAreaCells(cellArea, r, opts...); err != nil {
|
||||||
|
panic(fmt.Sprintf("canvas.SetAreaCells => unexpected error: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MustCell returns the cell or panics.
|
// MustCell returns the cell or panics.
|
||||||
func MustCell(c *canvas.Canvas, p image.Point) *cell.Cell {
|
func MustCell(c *canvas.Canvas, p image.Point) *cell.Cell {
|
||||||
cell, err := c.Cell(p)
|
cell, err := c.Cell(p)
|
||||||
|
@ -200,26 +200,18 @@ func (c *Container) updateFocus(m *terminalapi.Mouse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// keyboardToWidget forwards the keyboard event to the widget unconditionally.
|
// keyboardToWidget forwards the keyboard event to the widget unconditionally.
|
||||||
func (c *Container) keyboardToWidget(k *terminalapi.Keyboard) error {
|
func (c *Container) keyboardToWidget(k *terminalapi.Keyboard, scope widgetapi.KeyScope) error {
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.opts.widget.Keyboard(k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// keyboardToFocusedWidget forwards the keyboard event to the widget if its
|
|
||||||
// container is focused.
|
|
||||||
func (c *Container) keyboardToFocusedWidget(k *terminalapi.Keyboard) error {
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if !c.focusTracker.isActive(c) {
|
if scope == widgetapi.KeyScopeFocused && !c.focusTracker.isActive(c) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.opts.widget.Keyboard(k)
|
return c.opts.widget.Keyboard(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mouseToWidget forwards the mouse event to the widget.
|
// mouseToWidget forwards the mouse event to the widget.
|
||||||
func (c *Container) mouseToWidget(m *terminalapi.Mouse) error {
|
func (c *Container) mouseToWidget(m *terminalapi.Mouse, scope widgetapi.MouseScope) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -229,7 +221,7 @@ func (c *Container) mouseToWidget(m *terminalapi.Mouse) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ignore clicks falling outside of the container.
|
// Ignore clicks falling outside of the container.
|
||||||
if !m.Position.In(target.usable()) {
|
if scope != widgetapi.MouseScopeGlobal && !m.Position.In(c.area) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +230,7 @@ func (c *Container) mouseToWidget(m *terminalapi.Mouse) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !m.Position.In(wa) {
|
if scope == widgetapi.MouseScopeWidget && !m.Position.In(wa) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,9 +238,17 @@ func (c *Container) mouseToWidget(m *terminalapi.Mouse) error {
|
|||||||
// based, even though the widget might not be in the top left corner on the
|
// based, even though the widget might not be in the top left corner on the
|
||||||
// terminal.
|
// terminal.
|
||||||
offset := wa.Min
|
offset := wa.Min
|
||||||
wm := &terminalapi.Mouse{
|
var wm *terminalapi.Mouse
|
||||||
Position: m.Position.Sub(offset),
|
if m.Position.In(wa) {
|
||||||
Button: m.Button,
|
wm = &terminalapi.Mouse{
|
||||||
|
Position: m.Position.Sub(offset),
|
||||||
|
Button: m.Button,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wm = &terminalapi.Mouse{
|
||||||
|
Position: image.Point{-1, -1},
|
||||||
|
Button: m.Button,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return c.opts.widget.Mouse(wm)
|
return c.opts.widget.Mouse(wm)
|
||||||
}
|
}
|
||||||
@ -274,28 +274,25 @@ func (c *Container) Subscribe(eds *event.DistributionSystem) {
|
|||||||
preOrder(root, &errStr, visitFunc(func(c *Container) error {
|
preOrder(root, &errStr, visitFunc(func(c *Container) error {
|
||||||
if c.hasWidget() {
|
if c.hasWidget() {
|
||||||
wOpt := c.opts.widget.Options()
|
wOpt := c.opts.widget.Options()
|
||||||
switch wOpt.WantKeyboard {
|
switch scope := wOpt.WantKeyboard; scope {
|
||||||
case widgetapi.KeyScopeNone:
|
case widgetapi.KeyScopeNone:
|
||||||
// Widget doesn't want any keyboard events.
|
// Widget doesn't want any keyboard events.
|
||||||
|
|
||||||
case widgetapi.KeyScopeFocused:
|
default:
|
||||||
eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
|
eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
|
||||||
if err := c.keyboardToFocusedWidget(ev.(*terminalapi.Keyboard)); err != nil {
|
if err := c.keyboardToWidget(ev.(*terminalapi.Keyboard), scope); err != nil {
|
||||||
eds.Event(terminalapi.NewErrorf("failed to send keyboard event %v to widget %T: %v", ev, c.opts.widget, err))
|
|
||||||
}
|
|
||||||
}, event.MaxRepetitive(maxReps))
|
|
||||||
|
|
||||||
case widgetapi.KeyScopeGlobal:
|
|
||||||
eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
|
|
||||||
if err := c.keyboardToWidget(ev.(*terminalapi.Keyboard)); err != nil {
|
|
||||||
eds.Event(terminalapi.NewErrorf("failed to send global keyboard event %v to widget %T: %v", ev, c.opts.widget, err))
|
eds.Event(terminalapi.NewErrorf("failed to send global keyboard event %v to widget %T: %v", ev, c.opts.widget, err))
|
||||||
}
|
}
|
||||||
}, event.MaxRepetitive(maxReps))
|
}, event.MaxRepetitive(maxReps))
|
||||||
}
|
}
|
||||||
|
|
||||||
if wOpt.WantMouse {
|
switch scope := wOpt.WantMouse; scope {
|
||||||
|
case widgetapi.MouseScopeNone:
|
||||||
|
// Widget doesn't want any mouse events.
|
||||||
|
|
||||||
|
default:
|
||||||
eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
|
eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
|
||||||
if err := c.mouseToWidget(ev.(*terminalapi.Mouse)); err != nil {
|
if err := c.mouseToWidget(ev.(*terminalapi.Mouse), scope); err != nil {
|
||||||
eds.Event(terminalapi.NewErrorf("failed to send mouse event %v to widget %T: %v", ev, c.opts.widget, err))
|
eds.Event(terminalapi.NewErrorf("failed to send mouse event %v to widget %T: %v", ev, c.opts.widget, err))
|
||||||
}
|
}
|
||||||
}, event.MaxRepetitive(maxReps))
|
}, event.MaxRepetitive(maxReps))
|
||||||
|
@ -841,7 +841,7 @@ func TestMouse(t *testing.T) {
|
|||||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
return New(
|
return New(
|
||||||
ft,
|
ft,
|
||||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
events: []terminalapi.Event{
|
events: []terminalapi.Event{
|
||||||
@ -883,15 +883,15 @@ func TestMouse(t *testing.T) {
|
|||||||
ft,
|
ft,
|
||||||
SplitVertical(
|
SplitVertical(
|
||||||
Left(
|
Left(
|
||||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||||
),
|
),
|
||||||
Right(
|
Right(
|
||||||
SplitHorizontal(
|
SplitHorizontal(
|
||||||
Top(
|
Top(
|
||||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||||
),
|
),
|
||||||
Bottom(
|
Bottom(
|
||||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -913,7 +913,7 @@ func TestMouse(t *testing.T) {
|
|||||||
fakewidget.MustDraw(
|
fakewidget.MustDraw(
|
||||||
ft,
|
ft,
|
||||||
testcanvas.MustNew(image.Rect(25, 10, 50, 20)),
|
testcanvas.MustNew(image.Rect(25, 10, 50, 20)),
|
||||||
widgetapi.Options{WantMouse: true},
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
&terminalapi.Keyboard{},
|
&terminalapi.Keyboard{},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -921,7 +921,7 @@ func TestMouse(t *testing.T) {
|
|||||||
fakewidget.MustDraw(
|
fakewidget.MustDraw(
|
||||||
ft,
|
ft,
|
||||||
testcanvas.MustNew(image.Rect(25, 0, 50, 10)),
|
testcanvas.MustNew(image.Rect(25, 0, 50, 10)),
|
||||||
widgetapi.Options{WantMouse: true},
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
&terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonLeft},
|
&terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonLeft},
|
||||||
&terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonRelease},
|
&terminalapi.Mouse{Position: image.Point{24, 9}, Button: mouse.ButtonRelease},
|
||||||
)
|
)
|
||||||
@ -935,7 +935,7 @@ func TestMouse(t *testing.T) {
|
|||||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
return New(
|
return New(
|
||||||
ft,
|
ft,
|
||||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: false})),
|
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeNone})),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
events: []terminalapi.Event{
|
events: []terminalapi.Event{
|
||||||
@ -954,14 +954,14 @@ func TestMouse(t *testing.T) {
|
|||||||
wantProcessed: 1,
|
wantProcessed: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "event not forwarded if it falls on the container's border",
|
desc: "MouseScopeWidget, event not forwarded if it falls on the container's border",
|
||||||
termSize: image.Point{20, 20},
|
termSize: image.Point{20, 20},
|
||||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
return New(
|
return New(
|
||||||
ft,
|
ft,
|
||||||
Border(draw.LineStyleLight),
|
Border(draw.LineStyleLight),
|
||||||
PlaceWidget(
|
PlaceWidget(
|
||||||
fakewidget.New(widgetapi.Options{WantMouse: true}),
|
fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -989,14 +989,86 @@ func TestMouse(t *testing.T) {
|
|||||||
wantProcessed: 2,
|
wantProcessed: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "event not forwarded if it falls outside of widget's canvas",
|
desc: "MouseScopeContainer, event forwarded if it falls on the container's border",
|
||||||
|
termSize: image.Point{21, 20},
|
||||||
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
|
return New(
|
||||||
|
ft,
|
||||||
|
Border(draw.LineStyleLight),
|
||||||
|
PlaceWidget(
|
||||||
|
fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeContainer}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
testdraw.MustBorder(
|
||||||
|
cvs,
|
||||||
|
ft.Area(),
|
||||||
|
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
|
||||||
|
)
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(image.Rect(1, 1, 20, 19)),
|
||||||
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantProcessed: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "MouseScopeGlobal, event forwarded if it falls on the container's border",
|
||||||
|
termSize: image.Point{21, 20},
|
||||||
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
|
return New(
|
||||||
|
ft,
|
||||||
|
Border(draw.LineStyleLight),
|
||||||
|
PlaceWidget(
|
||||||
|
fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeGlobal}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
testdraw.MustBorder(
|
||||||
|
cvs,
|
||||||
|
ft.Area(),
|
||||||
|
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
|
||||||
|
)
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(image.Rect(1, 1, 20, 19)),
|
||||||
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantProcessed: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "MouseScopeWidget event not forwarded if it falls outside of widget's canvas",
|
||||||
termSize: image.Point{20, 20},
|
termSize: image.Point{20, 20},
|
||||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
return New(
|
return New(
|
||||||
ft,
|
ft,
|
||||||
PlaceWidget(
|
PlaceWidget(
|
||||||
fakewidget.New(widgetapi.Options{
|
fakewidget.New(widgetapi.Options{
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
Ratio: image.Point{2, 1},
|
Ratio: image.Point{2, 1},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -1020,14 +1092,187 @@ func TestMouse(t *testing.T) {
|
|||||||
wantProcessed: 2,
|
wantProcessed: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "mouse poisition adjusted relative to widget's canvas, vertical offset",
|
desc: "MouseScopeContainer event forwarded if it falls outside of widget's canvas",
|
||||||
termSize: image.Point{20, 20},
|
termSize: image.Point{20, 20},
|
||||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
return New(
|
return New(
|
||||||
ft,
|
ft,
|
||||||
PlaceWidget(
|
PlaceWidget(
|
||||||
fakewidget.New(widgetapi.Options{
|
fakewidget.New(widgetapi.Options{
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeContainer,
|
||||||
|
Ratio: image.Point{2, 1},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
AlignVertical(align.VerticalMiddle),
|
||||||
|
AlignHorizontal(align.HorizontalCenter),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
|
||||||
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantProcessed: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "MouseScopeGlobal event forwarded if it falls outside of widget's canvas",
|
||||||
|
termSize: image.Point{20, 20},
|
||||||
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
|
return New(
|
||||||
|
ft,
|
||||||
|
PlaceWidget(
|
||||||
|
fakewidget.New(widgetapi.Options{
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
Ratio: image.Point{2, 1},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
AlignVertical(align.VerticalMiddle),
|
||||||
|
AlignHorizontal(align.HorizontalCenter),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
|
||||||
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantProcessed: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "MouseScopeWidget event not forwarded if it falls to another container",
|
||||||
|
termSize: image.Point{20, 20},
|
||||||
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
|
return New(
|
||||||
|
ft,
|
||||||
|
SplitHorizontal(
|
||||||
|
Top(),
|
||||||
|
Bottom(
|
||||||
|
PlaceWidget(
|
||||||
|
fakewidget.New(widgetapi.Options{
|
||||||
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
|
Ratio: image.Point{2, 1},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
AlignVertical(align.VerticalMiddle),
|
||||||
|
AlignHorizontal(align.HorizontalCenter),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
|
||||||
|
widgetapi.Options{},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantProcessed: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "MouseScopeContainer event not forwarded if it falls to another container",
|
||||||
|
termSize: image.Point{20, 20},
|
||||||
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
|
return New(
|
||||||
|
ft,
|
||||||
|
SplitHorizontal(
|
||||||
|
Top(),
|
||||||
|
Bottom(
|
||||||
|
PlaceWidget(
|
||||||
|
fakewidget.New(widgetapi.Options{
|
||||||
|
WantMouse: widgetapi.MouseScopeContainer,
|
||||||
|
Ratio: image.Point{2, 1},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
AlignVertical(align.VerticalMiddle),
|
||||||
|
AlignHorizontal(align.HorizontalCenter),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
|
||||||
|
widgetapi.Options{},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantProcessed: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "MouseScopeGlobal event forwarded if it falls to another container",
|
||||||
|
termSize: image.Point{20, 20},
|
||||||
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
|
return New(
|
||||||
|
ft,
|
||||||
|
SplitHorizontal(
|
||||||
|
Top(),
|
||||||
|
Bottom(
|
||||||
|
PlaceWidget(
|
||||||
|
fakewidget.New(widgetapi.Options{
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
Ratio: image.Point{2, 1},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
AlignVertical(align.VerticalMiddle),
|
||||||
|
AlignHorizontal(align.HorizontalCenter),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
|
||||||
|
fakewidget.MustDraw(
|
||||||
|
ft,
|
||||||
|
testcanvas.MustNew(image.Rect(0, 10, 20, 20)),
|
||||||
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{-1, -1}, Button: mouse.ButtonLeft},
|
||||||
|
)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantProcessed: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "mouse position adjusted relative to widget's canvas, vertical offset",
|
||||||
|
termSize: image.Point{20, 20},
|
||||||
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
|
return New(
|
||||||
|
ft,
|
||||||
|
PlaceWidget(
|
||||||
|
fakewidget.New(widgetapi.Options{
|
||||||
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
Ratio: image.Point{2, 1},
|
Ratio: image.Point{2, 1},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -1044,7 +1289,7 @@ func TestMouse(t *testing.T) {
|
|||||||
fakewidget.MustDraw(
|
fakewidget.MustDraw(
|
||||||
ft,
|
ft,
|
||||||
testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
|
testcanvas.MustNew(image.Rect(0, 5, 20, 15)),
|
||||||
widgetapi.Options{WantMouse: true},
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
)
|
)
|
||||||
return ft
|
return ft
|
||||||
@ -1059,7 +1304,7 @@ func TestMouse(t *testing.T) {
|
|||||||
ft,
|
ft,
|
||||||
PlaceWidget(
|
PlaceWidget(
|
||||||
fakewidget.New(widgetapi.Options{
|
fakewidget.New(widgetapi.Options{
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
Ratio: image.Point{9, 10},
|
Ratio: image.Point{9, 10},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -1076,7 +1321,7 @@ func TestMouse(t *testing.T) {
|
|||||||
fakewidget.MustDraw(
|
fakewidget.MustDraw(
|
||||||
ft,
|
ft,
|
||||||
testcanvas.MustNew(image.Rect(6, 0, 24, 20)),
|
testcanvas.MustNew(image.Rect(6, 0, 24, 20)),
|
||||||
widgetapi.Options{WantMouse: true},
|
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||||
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
)
|
)
|
||||||
return ft
|
return ft
|
||||||
@ -1089,7 +1334,7 @@ func TestMouse(t *testing.T) {
|
|||||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||||
return New(
|
return New(
|
||||||
ft,
|
ft,
|
||||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
events: []terminalapi.Event{
|
events: []terminalapi.Event{
|
||||||
|
BIN
images/buttondemo.gif
Normal file
BIN
images/buttondemo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 767 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.0 MiB |
BIN
images/termdashdemo_0_7_0.gif
Normal file
BIN
images/termdashdemo_0_7_0.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 MiB |
@ -90,6 +90,12 @@ func (fsm *FSM) Event(m *terminalapi.Mouse) (bool, State) {
|
|||||||
return clicked, bs
|
return clicked, bs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateArea informs FSM of an area change.
|
||||||
|
// This method is idempotent.
|
||||||
|
func (fsm *FSM) UpdateArea(area image.Rectangle) {
|
||||||
|
fsm.area = area
|
||||||
|
}
|
||||||
|
|
||||||
// stateFn is a single state in the state machine.
|
// stateFn is a single state in the state machine.
|
||||||
// Returns bool indicating if a click happened, the state of the button and the
|
// Returns bool indicating if a click happened, the state of the button and the
|
||||||
// next state of the FSM.
|
// next state of the FSM.
|
||||||
|
@ -25,6 +25,9 @@ import (
|
|||||||
|
|
||||||
// eventTestCase is one mouse event and the output expectation.
|
// eventTestCase is one mouse event and the output expectation.
|
||||||
type eventTestCase struct {
|
type eventTestCase struct {
|
||||||
|
// area if specified, will be provided to UpdateArea *before* processing the event.
|
||||||
|
area *image.Rectangle
|
||||||
|
|
||||||
// event is the mouse event to send.
|
// event is the mouse event to send.
|
||||||
event *terminalapi.Mouse
|
event *terminalapi.Mouse
|
||||||
|
|
||||||
@ -59,6 +62,69 @@ func TestFSM(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "updates area so the clicks falls outside",
|
||||||
|
button: mouse.ButtonLeft,
|
||||||
|
area: image.Rect(0, 0, 1, 1),
|
||||||
|
eventCases: []*eventTestCase{
|
||||||
|
{
|
||||||
|
area: func() *image.Rectangle {
|
||||||
|
ar := image.Rect(1, 1, 2, 2)
|
||||||
|
return &ar
|
||||||
|
}(),
|
||||||
|
event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
wantClick: false,
|
||||||
|
wantState: Up,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
|
||||||
|
wantClick: false,
|
||||||
|
wantState: Up,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "updates area before release, so the release falls outside",
|
||||||
|
button: mouse.ButtonLeft,
|
||||||
|
area: image.Rect(0, 0, 1, 1),
|
||||||
|
eventCases: []*eventTestCase{
|
||||||
|
{
|
||||||
|
event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
wantClick: false,
|
||||||
|
wantState: Down,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
area: func() *image.Rectangle {
|
||||||
|
ar := image.Rect(1, 1, 2, 2)
|
||||||
|
return &ar
|
||||||
|
}(),
|
||||||
|
event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
|
||||||
|
wantClick: false,
|
||||||
|
wantState: Up,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "increased area makes the release count",
|
||||||
|
button: mouse.ButtonLeft,
|
||||||
|
area: image.Rect(0, 0, 1, 1),
|
||||||
|
eventCases: []*eventTestCase{
|
||||||
|
{
|
||||||
|
event: &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
wantClick: false,
|
||||||
|
wantState: Down,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
area: func() *image.Rectangle {
|
||||||
|
ar := image.Rect(0, 0, 2, 2)
|
||||||
|
return &ar
|
||||||
|
}(),
|
||||||
|
event: &terminalapi.Mouse{Position: image.Point{1, 1}, Button: mouse.ButtonRelease},
|
||||||
|
wantClick: true,
|
||||||
|
wantState: Up,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "tracks single right button click",
|
desc: "tracks single right button click",
|
||||||
button: mouse.ButtonRight,
|
button: mouse.ButtonRight,
|
||||||
@ -234,6 +300,10 @@ func TestFSM(t *testing.T) {
|
|||||||
t.Run(fmt.Sprintf(tc.desc), func(t *testing.T) {
|
t.Run(fmt.Sprintf(tc.desc), func(t *testing.T) {
|
||||||
fsm := NewFSM(tc.button, tc.area)
|
fsm := NewFSM(tc.button, tc.area)
|
||||||
for _, etc := range tc.eventCases {
|
for _, etc := range tc.eventCases {
|
||||||
|
if etc.area != nil {
|
||||||
|
fsm.UpdateArea(*etc.area)
|
||||||
|
}
|
||||||
|
|
||||||
gotClick, gotState := fsm.Event(etc.event)
|
gotClick, gotState := fsm.Event(etc.event)
|
||||||
t.Logf("Called fsm.Event(%v) => %v, %v", etc.event, gotClick, gotState)
|
t.Logf("Called fsm.Event(%v) => %v, %v", etc.event, gotClick, gotState)
|
||||||
if gotClick != etc.wantClick || gotState != etc.wantState {
|
if gotClick != etc.wantClick || gotState != etc.wantState {
|
||||||
|
@ -49,7 +49,7 @@ func Example() {
|
|||||||
wOpts := widgetapi.Options{
|
wOpts := widgetapi.Options{
|
||||||
MinimumSize: fakewidget.MinimumSize,
|
MinimumSize: fakewidget.MinimumSize,
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the container with two fake widgets.
|
// Create the container with two fake widgets.
|
||||||
@ -89,7 +89,7 @@ func Example_triggered() {
|
|||||||
wOpts := widgetapi.Options{
|
wOpts := widgetapi.Options{
|
||||||
MinimumSize: fakewidget.MinimumSize,
|
MinimumSize: fakewidget.MinimumSize,
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the container with a widget.
|
// Create the container with a widget.
|
||||||
@ -239,7 +239,7 @@ func TestRun(t *testing.T) {
|
|||||||
ft,
|
ft,
|
||||||
testcanvas.MustNew(ft.Area()),
|
testcanvas.MustNew(ft.Area()),
|
||||||
widgetapi.Options{
|
widgetapi.Options{
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
)
|
)
|
||||||
@ -265,7 +265,7 @@ func TestRun(t *testing.T) {
|
|||||||
testcanvas.MustNew(ft.Area()),
|
testcanvas.MustNew(ft.Area()),
|
||||||
widgetapi.Options{
|
widgetapi.Options{
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
)
|
)
|
||||||
@ -360,7 +360,7 @@ func TestRun(t *testing.T) {
|
|||||||
ft,
|
ft,
|
||||||
testcanvas.MustNew(ft.Area()),
|
testcanvas.MustNew(ft.Area()),
|
||||||
widgetapi.Options{
|
widgetapi.Options{
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp},
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp},
|
||||||
)
|
)
|
||||||
@ -394,7 +394,7 @@ func TestRun(t *testing.T) {
|
|||||||
got,
|
got,
|
||||||
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -460,7 +460,7 @@ func TestController(t *testing.T) {
|
|||||||
testcanvas.MustNew(ft.Area()),
|
testcanvas.MustNew(ft.Area()),
|
||||||
widgetapi.Options{
|
widgetapi.Options{
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
)
|
)
|
||||||
@ -580,7 +580,7 @@ func TestController(t *testing.T) {
|
|||||||
|
|
||||||
mi := fakewidget.New(widgetapi.Options{
|
mi := fakewidget.New(widgetapi.Options{
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
})
|
})
|
||||||
cont, err := container.New(
|
cont, err := container.New(
|
||||||
got,
|
got,
|
||||||
|
@ -21,15 +21,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mum4k/termdash"
|
"github.com/mum4k/termdash"
|
||||||
|
"github.com/mum4k/termdash/align"
|
||||||
"github.com/mum4k/termdash/cell"
|
"github.com/mum4k/termdash/cell"
|
||||||
"github.com/mum4k/termdash/container"
|
"github.com/mum4k/termdash/container"
|
||||||
"github.com/mum4k/termdash/draw"
|
"github.com/mum4k/termdash/draw"
|
||||||
"github.com/mum4k/termdash/terminal/termbox"
|
"github.com/mum4k/termdash/terminal/termbox"
|
||||||
"github.com/mum4k/termdash/terminalapi"
|
"github.com/mum4k/termdash/terminalapi"
|
||||||
"github.com/mum4k/termdash/widgets/barchart"
|
"github.com/mum4k/termdash/widgets/barchart"
|
||||||
|
"github.com/mum4k/termdash/widgets/button"
|
||||||
"github.com/mum4k/termdash/widgets/donut"
|
"github.com/mum4k/termdash/widgets/donut"
|
||||||
"github.com/mum4k/termdash/widgets/gauge"
|
"github.com/mum4k/termdash/widgets/gauge"
|
||||||
"github.com/mum4k/termdash/widgets/linechart"
|
"github.com/mum4k/termdash/widgets/linechart"
|
||||||
@ -128,10 +131,34 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sineLC, err := newSines(ctx)
|
leftB, rightB, sineLC, err := newSines(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
lcAndButtons := []container.Option{
|
||||||
|
container.SplitHorizontal(
|
||||||
|
container.Top(
|
||||||
|
container.Border(draw.LineStyleLight),
|
||||||
|
container.BorderTitle("Multiple series"),
|
||||||
|
container.BorderTitleAlignRight(),
|
||||||
|
container.PlaceWidget(sineLC),
|
||||||
|
),
|
||||||
|
container.Bottom(
|
||||||
|
container.SplitVertical(
|
||||||
|
container.Left(
|
||||||
|
container.PlaceWidget(leftB),
|
||||||
|
container.AlignHorizontal(align.HorizontalRight),
|
||||||
|
),
|
||||||
|
container.Right(
|
||||||
|
container.PlaceWidget(rightB),
|
||||||
|
container.AlignHorizontal(align.HorizontalLeft),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
container.SplitPercent(80),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
rightSide := []container.Option{
|
rightSide := []container.Option{
|
||||||
container.SplitHorizontal(
|
container.SplitHorizontal(
|
||||||
container.Top(
|
container.Top(
|
||||||
@ -148,12 +175,7 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
|||||||
container.BorderTitleAlignRight(),
|
container.BorderTitleAlignRight(),
|
||||||
container.PlaceWidget(don),
|
container.PlaceWidget(don),
|
||||||
),
|
),
|
||||||
container.Bottom(
|
container.Bottom(lcAndButtons...),
|
||||||
container.Border(draw.LineStyleLight),
|
|
||||||
container.BorderTitle("Multiple series"),
|
|
||||||
container.BorderTitleAlignRight(),
|
|
||||||
container.PlaceWidget(sineLC),
|
|
||||||
),
|
|
||||||
container.SplitPercent(30),
|
container.SplitPercent(30),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -422,23 +444,47 @@ func newBarChart(ctx context.Context) (*barchart.BarChart, error) {
|
|||||||
return bc, nil
|
return bc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSines returns a line chart that displays multiple sine series.
|
// distance is a thread-safe int value used by the newSince method.
|
||||||
func newSines(ctx context.Context) (*linechart.LineChart, error) {
|
// Buttons write it and the line chart reads it.
|
||||||
|
type distance struct {
|
||||||
|
v int
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// add adds the provided value to the one stored.
|
||||||
|
func (d *distance) add(v int) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
d.v += v
|
||||||
|
}
|
||||||
|
|
||||||
|
// get returns the current value.
|
||||||
|
func (d *distance) get() int {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
return d.v
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSines returns a line chart that displays multiple sine series and two buttons.
|
||||||
|
// The left button shifts the second series relative to the first series to
|
||||||
|
// the left and the right button shifts it to the right.
|
||||||
|
func newSines(ctx context.Context) (left, right *button.Button, lc *linechart.LineChart, err error) {
|
||||||
var inputs []float64
|
var inputs []float64
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
v := math.Sin(float64(i) / 100 * math.Pi)
|
v := math.Sin(float64(i) / 100 * math.Pi)
|
||||||
inputs = append(inputs, v)
|
inputs = append(inputs, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
lc, err := linechart.New(
|
sineLc, err := linechart.New(
|
||||||
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
|
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
|
||||||
linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||||
linechart.XLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
linechart.XLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
step1 := 0
|
step1 := 0
|
||||||
|
secondDist := &distance{v: 100}
|
||||||
go periodic(ctx, redrawInterval/3, func() error {
|
go periodic(ctx, redrawInterval/3, func() error {
|
||||||
step1 = (step1 + 1) % len(inputs)
|
step1 = (step1 + 1) % len(inputs)
|
||||||
if err := lc.Series("first", rotateFloats(inputs, step1),
|
if err := lc.Series("first", rotateFloats(inputs, step1),
|
||||||
@ -447,10 +493,30 @@ func newSines(ctx context.Context) (*linechart.LineChart, error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
step2 := (step1 + 100) % len(inputs)
|
step2 := (step1 + secondDist.get()) % len(inputs)
|
||||||
return lc.Series("second", rotateFloats(inputs, step2), linechart.SeriesCellOpts(cell.FgColor(cell.ColorWhite)))
|
return lc.Series("second", rotateFloats(inputs, step2), linechart.SeriesCellOpts(cell.FgColor(cell.ColorWhite)))
|
||||||
})
|
})
|
||||||
return lc, nil
|
|
||||||
|
// diff is the difference a single button press adds or removes to the
|
||||||
|
// second series.
|
||||||
|
const diff = 20
|
||||||
|
leftB, err := button.New("(l)eft", func() error {
|
||||||
|
secondDist.add(diff)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
button.GlobalKey('l'),
|
||||||
|
button.WidthFor("(r)ight"),
|
||||||
|
button.FillColor(cell.ColorNumber(220)),
|
||||||
|
)
|
||||||
|
|
||||||
|
rightB, err := button.New("(r)ight", func() error {
|
||||||
|
secondDist.add(-diff)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
button.GlobalKey('r'),
|
||||||
|
button.FillColor(cell.ColorNumber(196)),
|
||||||
|
)
|
||||||
|
return leftB, rightB, sineLc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// rotateFloats returns a new slice with inputs rotated by step.
|
// rotateFloats returns a new slice with inputs rotated by step.
|
||||||
|
@ -55,6 +55,54 @@ const (
|
|||||||
KeyScopeGlobal
|
KeyScopeGlobal
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MouseScope indicates the scope at which the widget wants to receive mouse
|
||||||
|
// events.
|
||||||
|
type MouseScope int
|
||||||
|
|
||||||
|
// String implements fmt.Stringer()
|
||||||
|
func (ms MouseScope) String() string {
|
||||||
|
if n, ok := mouseScopeNames[ms]; ok {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return "MouseScopeUnknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// mouseScopeNames maps MouseScope values to human readable names.
|
||||||
|
var mouseScopeNames = map[MouseScope]string{
|
||||||
|
MouseScopeNone: "MouseScopeNone",
|
||||||
|
MouseScopeWidget: "MouseScopeWidget",
|
||||||
|
MouseScopeContainer: "MouseScopeContainer",
|
||||||
|
MouseScopeGlobal: "MouseScopeGlobal",
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MouseScopeNone is used when the widget doesn't want to receive any mouse
|
||||||
|
// events.
|
||||||
|
MouseScopeNone MouseScope = iota
|
||||||
|
|
||||||
|
// MouseScopeWidget is used when the widget only wants mouse events that
|
||||||
|
// fall onto its canvas.
|
||||||
|
// The position of these widgets is always relative to widget's canvas.
|
||||||
|
MouseScopeWidget
|
||||||
|
|
||||||
|
// MouseScopeContainer is used when the widget only wants mouse events that
|
||||||
|
// fall onto its container. The area size of a container is always larger
|
||||||
|
// or equal to the one of the widget's canvas. So a widget selecting
|
||||||
|
// MouseScopeContainer will either receive the same or larger amount of
|
||||||
|
// events as compared to MouseScopeWidget.
|
||||||
|
// The position of mouse events that fall outside of widget's canvas is
|
||||||
|
// reset to image.Point{-1, -1}.
|
||||||
|
// The widgets are allowed to process the button event.
|
||||||
|
MouseScopeContainer
|
||||||
|
|
||||||
|
// MouseScopeGlobal is used when the widget wants to receive all mouse
|
||||||
|
// events regardless on where on the terminal they land.
|
||||||
|
// The position of mouse events that fall outside of widget's canvas is
|
||||||
|
// reset to image.Point{-1, -1} and must not be used by the widgets.
|
||||||
|
// The widgets are allowed to process the button event.
|
||||||
|
MouseScopeGlobal
|
||||||
|
)
|
||||||
|
|
||||||
// Options contains registration options for a widget.
|
// Options contains registration options for a widget.
|
||||||
// This is how the widget indicates its needs to the infrastructure.
|
// This is how the widget indicates its needs to the infrastructure.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@ -82,11 +130,13 @@ type Options struct {
|
|||||||
// forwarded to the widget.
|
// forwarded to the widget.
|
||||||
WantKeyboard KeyScope
|
WantKeyboard KeyScope
|
||||||
|
|
||||||
// WantMouse allows a widget to request mouse events.
|
// WantMouse allows a widget to request mouse events and specify their
|
||||||
// If false, mouse events won't be forwarded to the widget.
|
// desired scope. If set to MouseScopeNone, no mouse events are forwarded
|
||||||
// If true, the widget receives all mouse events whose coordinates fall
|
// to the widget.
|
||||||
// within its canvas.
|
// Note that the widget is only able to see the position of the mouse event
|
||||||
WantMouse bool
|
// if it falls onto its canvas. See the documentation next to individual
|
||||||
|
// MouseScope values for details.
|
||||||
|
WantMouse MouseScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget is a single widget on the dashboard.
|
// Widget is a single widget on the dashboard.
|
||||||
|
@ -270,7 +270,7 @@ func (bc *BarChart) Options() widgetapi.Options {
|
|||||||
return widgetapi.Options{
|
return widgetapi.Options{
|
||||||
MinimumSize: bc.minSize(),
|
MinimumSize: bc.minSize(),
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -629,7 +629,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -642,7 +642,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -660,7 +660,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -678,7 +678,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{3, 1},
|
MinimumSize: image.Point{3, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -699,7 +699,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{8, 1},
|
MinimumSize: image.Point{8, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -720,7 +720,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{8, 2},
|
MinimumSize: image.Point{8, 2},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
209
widgets/button/button.go
Normal file
209
widgets/button/button.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
// Copyright 2019 Google Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package button implements an interactive widget that can be pressed to
|
||||||
|
// activate.
|
||||||
|
package button
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mum4k/termdash/align"
|
||||||
|
"github.com/mum4k/termdash/canvas"
|
||||||
|
"github.com/mum4k/termdash/cell"
|
||||||
|
"github.com/mum4k/termdash/draw"
|
||||||
|
"github.com/mum4k/termdash/mouse"
|
||||||
|
"github.com/mum4k/termdash/mouse/button"
|
||||||
|
"github.com/mum4k/termdash/terminalapi"
|
||||||
|
"github.com/mum4k/termdash/widgetapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CallbackFn is the function called when the button is pressed.
|
||||||
|
// The callback function must be light-weight, ideally just storing a value and
|
||||||
|
// returning, since more button presses might occur.
|
||||||
|
//
|
||||||
|
// The callback function must be thread-safe as the mouse or keyboard events
|
||||||
|
// that press the button are processed in a separate goroutine.
|
||||||
|
//
|
||||||
|
// If the function returns an error, the widget will forward it back to the
|
||||||
|
// termdash infrastructure which causes a panic, unless the user provided a
|
||||||
|
// termdash.ErrorHandler.
|
||||||
|
type CallbackFn func() error
|
||||||
|
|
||||||
|
// Button can be pressed using a mouse click or a configured keyboard key.
|
||||||
|
//
|
||||||
|
// Upon each press, the button invokes a callback provided by the user.
|
||||||
|
//
|
||||||
|
// Implements widgetapi.Widget. This object is thread-safe.
|
||||||
|
type Button struct {
|
||||||
|
// text in the text label displayed in the button.
|
||||||
|
text string
|
||||||
|
|
||||||
|
// mouseFSM tracks left mouse clicks.
|
||||||
|
mouseFSM *button.FSM
|
||||||
|
// state is the current state of the button.
|
||||||
|
state button.State
|
||||||
|
|
||||||
|
// keyTriggerTime is the last time the button was pressed using a keyboard
|
||||||
|
// key. It is nil if the button was triggered by a mouse event.
|
||||||
|
// Used to draw button presses on keyboard events, since termbox doesn't
|
||||||
|
// provide us with release events for keys.
|
||||||
|
keyTriggerTime *time.Time
|
||||||
|
|
||||||
|
// callback gets called on each button press.
|
||||||
|
callback CallbackFn
|
||||||
|
|
||||||
|
// mu protects the widget.
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
// opts are the provided options.
|
||||||
|
opts *options
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Button that will display the provided text.
|
||||||
|
// Each press of the button will invoke the callback function.
|
||||||
|
func New(text string, cFn CallbackFn, opts ...Option) (*Button, error) {
|
||||||
|
if cFn == nil {
|
||||||
|
return nil, errors.New("the CallbackFn argument cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := newOptions(text)
|
||||||
|
for _, o := range opts {
|
||||||
|
o.set(opt)
|
||||||
|
}
|
||||||
|
if err := opt.validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Button{
|
||||||
|
text: text,
|
||||||
|
mouseFSM: button.NewFSM(mouse.ButtonLeft, image.ZR),
|
||||||
|
callback: cFn,
|
||||||
|
opts: opt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vars to be replaced from tests.
|
||||||
|
var (
|
||||||
|
// Runes to use in cells that contain the button.
|
||||||
|
// Changed from tests to provide readable test failures.
|
||||||
|
buttonRune = ' '
|
||||||
|
// Runes to use in cells that contain the shadow.
|
||||||
|
// Changed from tests to provide readable test failures.
|
||||||
|
shadowRune = ' '
|
||||||
|
|
||||||
|
// timeSince is a function that calculates duration since some time.
|
||||||
|
timeSince = time.Since
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw draws the Button widget onto the canvas.
|
||||||
|
// Implements widgetapi.Widget.Draw.
|
||||||
|
func (b *Button) Draw(cvs *canvas.Canvas) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.keyTriggerTime != nil {
|
||||||
|
since := timeSince(*b.keyTriggerTime)
|
||||||
|
if since > b.opts.keyUpDelay {
|
||||||
|
b.state = button.Up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cvsAr := cvs.Area()
|
||||||
|
b.mouseFSM.UpdateArea(cvsAr)
|
||||||
|
|
||||||
|
shadowAr := image.Rect(shadowWidth, shadowWidth, cvsAr.Dx(), cvsAr.Dy())
|
||||||
|
if err := cvs.SetAreaCells(shadowAr, shadowRune, cell.BgColor(b.opts.shadowColor)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttonAr image.Rectangle
|
||||||
|
if b.state == button.Up {
|
||||||
|
buttonAr = image.Rect(0, 0, cvsAr.Dx()-shadowWidth, cvsAr.Dy()-shadowWidth)
|
||||||
|
} else {
|
||||||
|
buttonAr = shadowAr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cvs.SetAreaCells(buttonAr, buttonRune, cell.BgColor(b.opts.fillColor)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
textAr := image.Rect(buttonAr.Min.X+1, buttonAr.Min.Y, buttonAr.Dx()-1, buttonAr.Max.Y)
|
||||||
|
start, err := align.Text(textAr, b.text, align.HorizontalCenter, align.VerticalMiddle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := draw.Text(cvs, b.text, start,
|
||||||
|
draw.TextOverrunMode(draw.OverrunModeThreeDot),
|
||||||
|
draw.TextMaxX(buttonAr.Max.X),
|
||||||
|
draw.TextCellOpts(cell.FgColor(b.opts.textColor)),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard processes keyboard events, acts as a button press on the configured
|
||||||
|
// Key.
|
||||||
|
//
|
||||||
|
// Implements widgetapi.Widget.Keyboard.
|
||||||
|
func (b *Button) Keyboard(k *terminalapi.Keyboard) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if k.Key == b.opts.key {
|
||||||
|
b.state = button.Down
|
||||||
|
now := time.Now().UTC()
|
||||||
|
b.keyTriggerTime = &now
|
||||||
|
return b.callback()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse processes mouse events, acts as a button press if both the press and
|
||||||
|
// the release happen inside the button.
|
||||||
|
//
|
||||||
|
// Implements widgetapi.Widget.Mouse.
|
||||||
|
func (b *Button) Mouse(m *terminalapi.Mouse) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
clicked, state := b.mouseFSM.Event(m)
|
||||||
|
b.state = state
|
||||||
|
b.keyTriggerTime = nil
|
||||||
|
|
||||||
|
if clicked {
|
||||||
|
return b.callback()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shadowWidth is the width of the shadow under the button in cell.
|
||||||
|
const shadowWidth = 1
|
||||||
|
|
||||||
|
// Options implements widgetapi.Widget.Options.
|
||||||
|
func (b *Button) Options() widgetapi.Options {
|
||||||
|
// No need to lock, as the height and width get fixed when New is called.
|
||||||
|
|
||||||
|
width := b.opts.width + shadowWidth
|
||||||
|
height := b.opts.height + shadowWidth
|
||||||
|
return widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{width, height},
|
||||||
|
MaximumSize: image.Point{width, height},
|
||||||
|
WantKeyboard: b.opts.keyScope,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
}
|
||||||
|
}
|
846
widgets/button/button_test.go
Normal file
846
widgets/button/button_test.go
Normal file
@ -0,0 +1,846 @@
|
|||||||
|
// Copyright 2019 Google Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package button
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kylelemons/godebug/pretty"
|
||||||
|
"github.com/mum4k/termdash/canvas"
|
||||||
|
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||||
|
"github.com/mum4k/termdash/cell"
|
||||||
|
"github.com/mum4k/termdash/draw"
|
||||||
|
"github.com/mum4k/termdash/draw/testdraw"
|
||||||
|
"github.com/mum4k/termdash/keyboard"
|
||||||
|
"github.com/mum4k/termdash/mouse"
|
||||||
|
"github.com/mum4k/termdash/terminal/faketerm"
|
||||||
|
"github.com/mum4k/termdash/terminalapi"
|
||||||
|
"github.com/mum4k/termdash/widgetapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// callbackTracker tracks whether callback was called.
|
||||||
|
type callbackTracker struct {
|
||||||
|
// wantErr when set to true, makes callback return an error.
|
||||||
|
wantErr bool
|
||||||
|
|
||||||
|
// called asserts whether the callback was called.
|
||||||
|
called bool
|
||||||
|
|
||||||
|
// count is the number of times the callback was called.
|
||||||
|
count int
|
||||||
|
|
||||||
|
// mu protects the tracker.
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback is the callback function.
|
||||||
|
func (ct *callbackTracker) callback() error {
|
||||||
|
ct.mu.Lock()
|
||||||
|
defer ct.mu.Unlock()
|
||||||
|
|
||||||
|
if ct.wantErr {
|
||||||
|
return errors.New("ct.wantErr set to true")
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.count++
|
||||||
|
ct.called = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestButton(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
text string
|
||||||
|
callback *callbackTracker
|
||||||
|
opts []Option
|
||||||
|
events []terminalapi.Event
|
||||||
|
canvas image.Rectangle
|
||||||
|
|
||||||
|
// timeSince is used to replace time.Since for tests, leave nil to use
|
||||||
|
// the original.
|
||||||
|
timeSince func(time.Time) time.Duration
|
||||||
|
|
||||||
|
want func(size image.Point) *faketerm.Terminal
|
||||||
|
wantCallback *callbackTracker
|
||||||
|
wantNewErr bool
|
||||||
|
wantDrawErr bool
|
||||||
|
wantCallbackErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "New fails with nil callback",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
wantNewErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "New fails with negative keyUpDelay",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
opts: []Option{
|
||||||
|
KeyUpDelay(-1 * time.Second),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
wantNewErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "New fails with zero Height",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
opts: []Option{
|
||||||
|
Height(0),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
wantNewErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "New fails with zero Width",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
opts: []Option{
|
||||||
|
Width(0),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
wantNewErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "draw fails on canvas too small",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 1, 1),
|
||||||
|
wantDrawErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "draws button in up state",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "draws button in down state due to a mouse event",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{2, 2},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "mouse triggered the callback",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{
|
||||||
|
called: true,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "draws button in down state due to a keyboard event, callback triggered",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Key(keyboard.KeyEnter),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{2, 2},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{
|
||||||
|
called: true,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "keyboard event ignored when no key specified",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "keyboard event triggers the button, trigger time didn't expire so button is down",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Key(keyboard.KeyEnter),
|
||||||
|
},
|
||||||
|
timeSince: func(time.Time) time.Duration {
|
||||||
|
return 200 * time.Millisecond
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{2, 2},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{
|
||||||
|
called: true,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "keyboard event triggers the button, custom trigger time expired so button is up",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Key(keyboard.KeyEnter),
|
||||||
|
KeyUpDelay(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
timeSince: func(time.Time) time.Duration {
|
||||||
|
return 200 * time.Millisecond
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{
|
||||||
|
called: true,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "keyboard event triggers the button multiple times",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Key(keyboard.KeyEnter),
|
||||||
|
KeyUpDelay(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
timeSince: func(time.Time) time.Duration {
|
||||||
|
return 200 * time.Millisecond
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{
|
||||||
|
called: true,
|
||||||
|
count: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "mouse event triggers the button multiple times",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{
|
||||||
|
called: true,
|
||||||
|
count: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "the callback returns an error after a mouse event",
|
||||||
|
callback: &callbackTracker{
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||||
|
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
|
||||||
|
},
|
||||||
|
wantCallbackErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "the callback returns an error after a keyboard event",
|
||||||
|
callback: &callbackTracker{
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Key(keyboard.KeyEnter),
|
||||||
|
KeyUpDelay(100 * time.Millisecond),
|
||||||
|
},
|
||||||
|
timeSince: func(time.Time) time.Duration {
|
||||||
|
return 200 * time.Millisecond
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
events: []terminalapi.Event{
|
||||||
|
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||||
|
},
|
||||||
|
wantCallbackErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "draws button with custom height (infra gives smaller canvas)",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
canvas: image.Rect(0, 0, 8, 2),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 2), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 1), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 0},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "button width adjusts to width (infra gives smaller canvas)",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "h",
|
||||||
|
canvas: image.Rect(0, 0, 4, 2),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 4, 2), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 3, 1), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "h", image.Point{1, 0},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "sets custom text color",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
TextColor(cell.ColorRed),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorRed),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "sets custom fill color",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
FillColor(cell.ColorRed),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorNumber(240)))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorRed))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorRed)),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "sets custom shadow color",
|
||||||
|
callback: &callbackTracker{},
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
ShadowColor(cell.ColorRed),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 8, 4),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
// Shadow.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(1, 1, 8, 4), 's', cell.BgColor(cell.ColorRed))
|
||||||
|
|
||||||
|
// Button.
|
||||||
|
testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 7, 3), 'x', cell.BgColor(cell.ColorNumber(117)))
|
||||||
|
|
||||||
|
// Text.
|
||||||
|
testdraw.MustText(cvs, "hello", image.Point{1, 1},
|
||||||
|
draw.TextCellOpts(
|
||||||
|
cell.FgColor(cell.ColorBlack),
|
||||||
|
cell.BgColor(cell.ColorNumber(117))),
|
||||||
|
)
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCallback: &callbackTracker{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonRune = 'x'
|
||||||
|
shadowRune = 's'
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
if tc.timeSince != nil {
|
||||||
|
timeSince = tc.timeSince
|
||||||
|
} else {
|
||||||
|
timeSince = time.Since
|
||||||
|
}
|
||||||
|
|
||||||
|
gotCallback := tc.callback
|
||||||
|
var cFn CallbackFn
|
||||||
|
if gotCallback == nil {
|
||||||
|
cFn = nil
|
||||||
|
} else {
|
||||||
|
cFn = gotCallback.callback
|
||||||
|
}
|
||||||
|
b, err := New(tc.text, cFn, tc.opts...)
|
||||||
|
if (err != nil) != tc.wantNewErr {
|
||||||
|
t.Errorf("New => unexpected error: %v, wantNewErr: %v", err, tc.wantNewErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Draw once which initializes the mouse state machine with the current canvas area.
|
||||||
|
c, err := canvas.New(tc.canvas)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
err = b.Draw(c)
|
||||||
|
if (err != nil) != tc.wantDrawErr {
|
||||||
|
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, ev := range tc.events {
|
||||||
|
switch e := ev.(type) {
|
||||||
|
case *terminalapi.Mouse:
|
||||||
|
err := b.Mouse(e)
|
||||||
|
// Only the last event in test cases is the one that triggers the callback.
|
||||||
|
if i == len(tc.events)-1 {
|
||||||
|
if (err != nil) != tc.wantCallbackErr {
|
||||||
|
t.Errorf("Mouse => unexpected error: %v, wantCallbackErr: %v", err, tc.wantCallbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Mouse => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *terminalapi.Keyboard:
|
||||||
|
err := b.Keyboard(e)
|
||||||
|
// Only the last event in test cases is the one that triggers the callback.
|
||||||
|
if i == len(tc.events)-1 {
|
||||||
|
if (err != nil) != tc.wantCallbackErr {
|
||||||
|
t.Errorf("Keyboard => unexpected error: %v, wantCallbackErr: %v", err, tc.wantCallbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Keyboard => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Fatalf("unsupported event type: %T", ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := canvas.New(tc.canvas)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.Draw(c)
|
||||||
|
if (err != nil) != tc.wantDrawErr {
|
||||||
|
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := faketerm.New(c.Size())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Apply(got); err != nil {
|
||||||
|
t.Fatalf("Apply => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var want *faketerm.Terminal
|
||||||
|
if tc.want != nil {
|
||||||
|
want = tc.want(c.Size())
|
||||||
|
} else {
|
||||||
|
want = faketerm.MustNew(c.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := faketerm.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("Draw => %v", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := pretty.Compare(tc.wantCallback, gotCallback); diff != "" {
|
||||||
|
t.Errorf("CallbackFn => unexpected diff (-want, +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
text string
|
||||||
|
opts []Option
|
||||||
|
want widgetapi.Options
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "width is based on the text width by default",
|
||||||
|
text: "hello world",
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{14, 4},
|
||||||
|
MaximumSize: image.Point{14, 4},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "width supports full-width unicode characters",
|
||||||
|
text: "■㈱の世界①",
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{13, 4},
|
||||||
|
MaximumSize: image.Point{13, 4},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "width specified via WidthFor",
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
WidthFor("■㈱の世界①"),
|
||||||
|
},
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{13, 4},
|
||||||
|
MaximumSize: image.Point{13, 4},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "custom height specified",
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Height(10),
|
||||||
|
},
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{8, 11},
|
||||||
|
MaximumSize: image.Point{8, 11},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "custom width specified",
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Width(10),
|
||||||
|
},
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{11, 4},
|
||||||
|
MaximumSize: image.Point{11, 4},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
desc: "doesn't want keyboard by default",
|
||||||
|
text: "hello",
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{8, 4},
|
||||||
|
MaximumSize: image.Point{8, 4},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "registers for focused keyboard events",
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
Key(keyboard.KeyEnter),
|
||||||
|
},
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{8, 4},
|
||||||
|
MaximumSize: image.Point{8, 4},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "registers for global keyboard events",
|
||||||
|
text: "hello",
|
||||||
|
opts: []Option{
|
||||||
|
GlobalKey(keyboard.KeyEnter),
|
||||||
|
},
|
||||||
|
want: widgetapi.Options{
|
||||||
|
MinimumSize: image.Point{8, 4},
|
||||||
|
MaximumSize: image.Point{8, 4},
|
||||||
|
WantKeyboard: widgetapi.KeyScopeGlobal,
|
||||||
|
WantMouse: widgetapi.MouseScopeGlobal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
ct := &callbackTracker{}
|
||||||
|
b, err := New(tc.text, ct.callback, tc.opts...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New => unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := b.Options()
|
||||||
|
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||||
|
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
116
widgets/button/buttondemo/buttondemo.go
Normal file
116
widgets/button/buttondemo/buttondemo.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2019 Google Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Binary buttondemo shows the functionality of a button widget.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mum4k/termdash"
|
||||||
|
"github.com/mum4k/termdash/align"
|
||||||
|
"github.com/mum4k/termdash/cell"
|
||||||
|
"github.com/mum4k/termdash/container"
|
||||||
|
"github.com/mum4k/termdash/draw"
|
||||||
|
"github.com/mum4k/termdash/terminal/termbox"
|
||||||
|
"github.com/mum4k/termdash/terminalapi"
|
||||||
|
"github.com/mum4k/termdash/widgets/button"
|
||||||
|
"github.com/mum4k/termdash/widgets/segmentdisplay"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
t, err := termbox.New()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer t.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
val := 0
|
||||||
|
display, err := segmentdisplay.New()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := display.Write([]*segmentdisplay.TextChunk{
|
||||||
|
segmentdisplay.NewChunk(fmt.Sprintf("%d", val)),
|
||||||
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addB, err := button.New("(a)dd", func() error {
|
||||||
|
val++
|
||||||
|
return display.Write([]*segmentdisplay.TextChunk{
|
||||||
|
segmentdisplay.NewChunk(fmt.Sprintf("%d", val)),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
button.GlobalKey('a'),
|
||||||
|
button.WidthFor("(s)ubtract"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subB, err := button.New("(s)ubtract", func() error {
|
||||||
|
val--
|
||||||
|
return display.Write([]*segmentdisplay.TextChunk{
|
||||||
|
segmentdisplay.NewChunk(fmt.Sprintf("%d", val)),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
button.FillColor(cell.ColorNumber(220)),
|
||||||
|
button.GlobalKey('s'),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := container.New(
|
||||||
|
t,
|
||||||
|
container.Border(draw.LineStyleLight),
|
||||||
|
container.BorderTitle("PRESS Q TO QUIT"),
|
||||||
|
container.SplitHorizontal(
|
||||||
|
container.Top(
|
||||||
|
container.PlaceWidget(display),
|
||||||
|
),
|
||||||
|
container.Bottom(
|
||||||
|
container.SplitVertical(
|
||||||
|
container.Left(
|
||||||
|
container.PlaceWidget(addB),
|
||||||
|
container.AlignHorizontal(align.HorizontalRight),
|
||||||
|
),
|
||||||
|
container.Right(
|
||||||
|
container.PlaceWidget(subB),
|
||||||
|
container.AlignHorizontal(align.HorizontalLeft),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
container.SplitPercent(60),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
quitter := func(k *terminalapi.Keyboard) {
|
||||||
|
if k.Key == 'q' || k.Key == 'Q' {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(100*time.Millisecond)); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
171
widgets/button/options.go
Normal file
171
widgets/button/options.go
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// Copyright 2019 Google Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package button
|
||||||
|
|
||||||
|
// options.go contains configurable options for Button.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mum4k/termdash/cell"
|
||||||
|
"github.com/mum4k/termdash/cell/runewidth"
|
||||||
|
"github.com/mum4k/termdash/keyboard"
|
||||||
|
"github.com/mum4k/termdash/widgetapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option is used to provide options.
|
||||||
|
type Option interface {
|
||||||
|
// set sets the provided option.
|
||||||
|
set(*options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// option implements Option.
|
||||||
|
type option func(*options)
|
||||||
|
|
||||||
|
// set implements Option.set.
|
||||||
|
func (o option) set(opts *options) {
|
||||||
|
o(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// options holds the provided options.
|
||||||
|
type options struct {
|
||||||
|
fillColor cell.Color
|
||||||
|
textColor cell.Color
|
||||||
|
shadowColor cell.Color
|
||||||
|
height int
|
||||||
|
width int
|
||||||
|
key keyboard.Key
|
||||||
|
keyScope widgetapi.KeyScope
|
||||||
|
keyUpDelay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate validates the provided options.
|
||||||
|
func (o *options) validate() error {
|
||||||
|
if min := 1; o.height < min {
|
||||||
|
return fmt.Errorf("invalid height %d, must be %d <= height", o.height, min)
|
||||||
|
}
|
||||||
|
if min := 1; o.width < min {
|
||||||
|
return fmt.Errorf("invalid width %d, must be %d <= width", o.width, min)
|
||||||
|
}
|
||||||
|
if min := time.Duration(0); o.keyUpDelay < min {
|
||||||
|
return fmt.Errorf("invalid keyUpDelay %v, must be %v <= keyUpDelay", o.keyUpDelay, min)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions returns options with the default values set.
|
||||||
|
func newOptions(text string) *options {
|
||||||
|
return &options{
|
||||||
|
fillColor: cell.ColorNumber(117),
|
||||||
|
textColor: cell.ColorBlack,
|
||||||
|
shadowColor: cell.ColorNumber(240),
|
||||||
|
height: DefaultHeight,
|
||||||
|
width: widthFor(text),
|
||||||
|
keyUpDelay: DefaultKeyUpDelay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillColor sets the fill color of the button.
|
||||||
|
func FillColor(c cell.Color) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.fillColor = c
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextColor sets the color of the text label in the button.
|
||||||
|
func TextColor(c cell.Color) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.textColor = c
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShadowColor sets the color of the shadow under the button.
|
||||||
|
func ShadowColor(c cell.Color) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.shadowColor = c
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultHeight is the default for the Height option.
|
||||||
|
const DefaultHeight = 3
|
||||||
|
|
||||||
|
// Height sets the height of the button in cells.
|
||||||
|
// Must be a positive non-zero integer.
|
||||||
|
// Defaults to DefaultHeight.
|
||||||
|
func Height(cells int) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.height = cells
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width sets the width of the button in cells.
|
||||||
|
// Must be a positive non-zero integer.
|
||||||
|
// Defaults to the auto-width based on the length of the text label.
|
||||||
|
func Width(cells int) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.width = cells
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WidthFor sets the width of the button as if it was displaying the provided text.
|
||||||
|
// Useful when displaying multiple buttons with the intention to set all of
|
||||||
|
// their sizes equal to the one with the longest text.
|
||||||
|
func WidthFor(text string) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.width = widthFor(text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key configures the keyboard key that presses the button.
|
||||||
|
// The widget responds to this key only if its container if focused.
|
||||||
|
// When not provided, the widget ignores all keyboard events.
|
||||||
|
func Key(k keyboard.Key) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.key = k
|
||||||
|
opts.keyScope = widgetapi.KeyScopeFocused
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalKey is like Key, but makes the widget respond to the key even if its
|
||||||
|
// container isn't focused.
|
||||||
|
// When not provided, the widget ignores all keyboard events.
|
||||||
|
func GlobalKey(k keyboard.Key) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.key = k
|
||||||
|
opts.keyScope = widgetapi.KeyScopeGlobal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultKeyUpDelay is the default value for the KeyUpDelay option.
|
||||||
|
const DefaultKeyUpDelay = 250 * time.Millisecond
|
||||||
|
|
||||||
|
// KeyUpDelay is the amount of time the button will remain "pressed down" after
|
||||||
|
// triggered by the configured key. Termbox doesn't emit events for key
|
||||||
|
// releases so the button simulates it by timing it.
|
||||||
|
// This only works if the manual termdash redraw or the periodic redraw
|
||||||
|
// interval are reasonably close to this delay.
|
||||||
|
// The duration cannot be negative.
|
||||||
|
// Defaults to DefaultKeyUpDelay.
|
||||||
|
func KeyUpDelay(d time.Duration) Option {
|
||||||
|
return option(func(opts *options) {
|
||||||
|
opts.keyUpDelay = d
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// widthFor returns the required width for the specified text.
|
||||||
|
func widthFor(text string) int {
|
||||||
|
return runewidth.StringWidth(text) + 2 // One empty cell at each side.
|
||||||
|
}
|
@ -248,6 +248,6 @@ func (d *Donut) Options() widgetapi.Options {
|
|||||||
// The smallest circle that "looks" like a circle on the canvas.
|
// The smallest circle that "looks" like a circle on the canvas.
|
||||||
MinimumSize: image.Point{3, 3},
|
MinimumSize: image.Point{3, 3},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -666,7 +666,7 @@ func TestOptions(t *testing.T) {
|
|||||||
Ratio: image.Point{4, 2},
|
Ratio: image.Point{4, 2},
|
||||||
MinimumSize: image.Point{3, 3},
|
MinimumSize: image.Point{3, 3},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
}
|
}
|
||||||
if diff := pretty.Compare(want, got); diff != "" {
|
if diff := pretty.Compare(want, got); diff != "" {
|
||||||
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
||||||
|
@ -173,7 +173,7 @@ func DrawWithMirror(mirror *Mirror, t terminalapi.Terminal, cvs *canvas.Canvas,
|
|||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
switch e := ev.(type) {
|
switch e := ev.(type) {
|
||||||
case *terminalapi.Mouse:
|
case *terminalapi.Mouse:
|
||||||
if !mirror.opts.WantMouse {
|
if mirror.opts.WantMouse == widgetapi.MouseScopeNone {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := mirror.Mouse(e); err != nil {
|
if err := mirror.Mouse(e); err != nil {
|
||||||
|
@ -324,7 +324,7 @@ func TestDraw(t *testing.T) {
|
|||||||
desc: "draws both keyboard and mouse events",
|
desc: "draws both keyboard and mouse events",
|
||||||
opts: widgetapi.Options{
|
opts: widgetapi.Options{
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
cvs: testcanvas.MustNew(image.Rect(0, 0, 17, 5)),
|
cvs: testcanvas.MustNew(image.Rect(0, 0, 17, 5)),
|
||||||
events: []terminalapi.Event{
|
events: []terminalapi.Event{
|
||||||
|
@ -330,6 +330,6 @@ func (g *Gauge) Options() widgetapi.Options {
|
|||||||
MaximumSize: g.maxSize(),
|
MaximumSize: g.maxSize(),
|
||||||
MinimumSize: g.minSize(),
|
MinimumSize: g.minSize(),
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -814,7 +814,7 @@ func TestOptions(t *testing.T) {
|
|||||||
MaximumSize: image.Point{0, 0}, // Unlimited.
|
MaximumSize: image.Point{0, 0}, // Unlimited.
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -826,7 +826,7 @@ func TestOptions(t *testing.T) {
|
|||||||
MaximumSize: image.Point{0, 2},
|
MaximumSize: image.Point{0, 2},
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -839,7 +839,7 @@ func TestOptions(t *testing.T) {
|
|||||||
MaximumSize: image.Point{0, 4},
|
MaximumSize: image.Point{0, 4},
|
||||||
MinimumSize: image.Point{3, 3},
|
MinimumSize: image.Point{3, 3},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -492,7 +492,7 @@ func (lc *LineChart) Options() widgetapi.Options {
|
|||||||
|
|
||||||
return widgetapi.Options{
|
return widgetapi.Options{
|
||||||
MinimumSize: lc.minSize(),
|
MinimumSize: lc.minSize(),
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1514,7 +1514,7 @@ func TestOptions(t *testing.T) {
|
|||||||
desc: "reserves space for axis without series",
|
desc: "reserves space for axis without series",
|
||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{3, 4},
|
MinimumSize: image.Point{3, 4},
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1524,7 +1524,7 @@ func TestOptions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{5, 4},
|
MinimumSize: image.Point{5, 4},
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1534,7 +1534,7 @@ func TestOptions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{6, 4},
|
MinimumSize: image.Point{6, 4},
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1547,7 +1547,7 @@ func TestOptions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{4, 5},
|
MinimumSize: image.Point{4, 5},
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1560,7 +1560,7 @@ func TestOptions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{5, 7},
|
MinimumSize: image.Point{5, 7},
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -259,6 +259,6 @@ func (sd *SegmentDisplay) Options() widgetapi.Options {
|
|||||||
// The smallest supported size of a display segment.
|
// The smallest supported size of a display segment.
|
||||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -822,7 +822,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want := widgetapi.Options{
|
want := widgetapi.Options{
|
||||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
}
|
}
|
||||||
if diff := pretty.Compare(want, got); diff != "" {
|
if diff := pretty.Compare(want, got); diff != "" {
|
||||||
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
||||||
|
@ -231,6 +231,6 @@ func (sl *SparkLine) Options() widgetapi.Options {
|
|||||||
MinimumSize: min,
|
MinimumSize: min,
|
||||||
MaximumSize: max,
|
MaximumSize: max,
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -470,7 +470,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -481,7 +481,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{1, 2},
|
MinimumSize: image.Point{1, 2},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -493,7 +493,7 @@ func TestOptions(t *testing.T) {
|
|||||||
MinimumSize: image.Point{1, 3},
|
MinimumSize: image.Point{1, 3},
|
||||||
MaximumSize: image.Point{1, 3},
|
MaximumSize: image.Point{1, 3},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -506,7 +506,7 @@ func TestOptions(t *testing.T) {
|
|||||||
MinimumSize: image.Point{1, 4},
|
MinimumSize: image.Point{1, 4},
|
||||||
MaximumSize: image.Point{1, 4},
|
MaximumSize: image.Point{1, 4},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -303,16 +303,19 @@ func (t *Text) Mouse(m *terminalapi.Mouse) error {
|
|||||||
// Options of the widget
|
// Options of the widget
|
||||||
func (t *Text) Options() widgetapi.Options {
|
func (t *Text) Options() widgetapi.Options {
|
||||||
var ks widgetapi.KeyScope
|
var ks widgetapi.KeyScope
|
||||||
|
var ms widgetapi.MouseScope
|
||||||
if t.opts.disableScrolling {
|
if t.opts.disableScrolling {
|
||||||
ks = widgetapi.KeyScopeNone
|
ks = widgetapi.KeyScopeNone
|
||||||
|
ms = widgetapi.MouseScopeNone
|
||||||
} else {
|
} else {
|
||||||
ks = widgetapi.KeyScopeFocused
|
ks = widgetapi.KeyScopeFocused
|
||||||
|
ms = widgetapi.MouseScopeWidget
|
||||||
}
|
}
|
||||||
|
|
||||||
return widgetapi.Options{
|
return widgetapi.Options{
|
||||||
// At least one line with at least one full-width rune.
|
// At least one line with at least one full-width rune.
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantMouse: !t.opts.disableScrolling,
|
WantMouse: ms,
|
||||||
WantKeyboard: ks,
|
WantKeyboard: ks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -785,7 +785,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||||
WantMouse: true,
|
WantMouse: widgetapi.MouseScopeWidget,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -796,7 +796,7 @@ func TestOptions(t *testing.T) {
|
|||||||
want: widgetapi.Options{
|
want: widgetapi.Options{
|
||||||
MinimumSize: image.Point{1, 1},
|
MinimumSize: image.Point{1, 1},
|
||||||
WantKeyboard: widgetapi.KeyScopeNone,
|
WantKeyboard: widgetapi.KeyScopeNone,
|
||||||
WantMouse: false,
|
WantMouse: widgetapi.MouseScopeNone,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user