mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-27 13:48:49 +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
|
||||
|
||||
- The Button widget.
|
||||
- A function that draws text vertically.
|
||||
- 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
|
||||
@ -34,10 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
subscriber.
|
||||
- The infrastructure now throttles event driven screen redraw rather than
|
||||
redrawing for each input event.
|
||||
- Widgets can now specify the scope at which they want to receive keyboard
|
||||
events, i.e. KeyScopeNone for no events, KeyScopeFocused to receive events
|
||||
only if the parent container is focused and KeyScopeGlobal to receive all
|
||||
keyboard events.
|
||||
- Widgets can now specify the scope at which they want to receive keyboard and
|
||||
mouse events.
|
||||
|
||||
#### 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
|
||||
aren't needed by container users.
|
||||
- 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
|
||||
widgets.
|
||||
widget specifies if it wants keyboard or mouse events. This only impacts
|
||||
development of new widgets.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
14
README.md
14
README.md
@ -8,7 +8,7 @@
|
||||
|
||||
# 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.
|
||||
The feature set is inspired by the
|
||||
@ -62,6 +62,18 @@ Project documentation is available in the [doc](doc/) directory.
|
||||
|
||||
## 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
|
||||
|
||||
Displays the progress of an operation. Run the
|
||||
|
@ -295,7 +295,7 @@ func TestBraille(t *testing.T) {
|
||||
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),
|
||||
pixelOps: func(c *Canvas) error {
|
||||
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),
|
||||
pixelOps: func(c *Canvas) error {
|
||||
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/cell"
|
||||
"github.com/mum4k/termdash/cell/runewidth"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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.
|
||||
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) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
|
@ -51,6 +51,13 @@ func MustSetCell(c *canvas.Canvas, p image.Point, r rune, opts ...cell.Option) i
|
||||
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.
|
||||
func MustCell(c *canvas.Canvas, p image.Point) *cell.Cell {
|
||||
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.
|
||||
func (c *Container) keyboardToWidget(k *terminalapi.Keyboard) 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 {
|
||||
func (c *Container) keyboardToWidget(k *terminalapi.Keyboard, scope widgetapi.KeyScope) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !c.focusTracker.isActive(c) {
|
||||
if scope == widgetapi.KeyScopeFocused && !c.focusTracker.isActive(c) {
|
||||
return nil
|
||||
}
|
||||
return c.opts.widget.Keyboard(k)
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -229,7 +221,7 @@ func (c *Container) mouseToWidget(m *terminalapi.Mouse) error {
|
||||
}
|
||||
|
||||
// Ignore clicks falling outside of the container.
|
||||
if !m.Position.In(target.usable()) {
|
||||
if scope != widgetapi.MouseScopeGlobal && !m.Position.In(c.area) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -238,7 +230,7 @@ func (c *Container) mouseToWidget(m *terminalapi.Mouse) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !m.Position.In(wa) {
|
||||
if scope == widgetapi.MouseScopeWidget && !m.Position.In(wa) {
|
||||
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
|
||||
// terminal.
|
||||
offset := wa.Min
|
||||
wm := &terminalapi.Mouse{
|
||||
Position: m.Position.Sub(offset),
|
||||
Button: m.Button,
|
||||
var wm *terminalapi.Mouse
|
||||
if m.Position.In(wa) {
|
||||
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)
|
||||
}
|
||||
@ -274,28 +274,25 @@ func (c *Container) Subscribe(eds *event.DistributionSystem) {
|
||||
preOrder(root, &errStr, visitFunc(func(c *Container) error {
|
||||
if c.hasWidget() {
|
||||
wOpt := c.opts.widget.Options()
|
||||
switch wOpt.WantKeyboard {
|
||||
switch scope := wOpt.WantKeyboard; scope {
|
||||
case widgetapi.KeyScopeNone:
|
||||
// Widget doesn't want any keyboard events.
|
||||
|
||||
case widgetapi.KeyScopeFocused:
|
||||
default:
|
||||
eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
|
||||
if err := c.keyboardToFocusedWidget(ev.(*terminalapi.Keyboard)); 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 {
|
||||
if err := c.keyboardToWidget(ev.(*terminalapi.Keyboard), scope); err != nil {
|
||||
eds.Event(terminalapi.NewErrorf("failed to send global keyboard event %v to widget %T: %v", ev, c.opts.widget, err))
|
||||
}
|
||||
}, 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) {
|
||||
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))
|
||||
}
|
||||
}, event.MaxRepetitive(maxReps))
|
||||
|
@ -841,7 +841,7 @@ func TestMouse(t *testing.T) {
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||
)
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
@ -883,15 +883,15 @@ func TestMouse(t *testing.T) {
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||
),
|
||||
Right(
|
||||
SplitHorizontal(
|
||||
Top(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||
),
|
||||
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(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(25, 10, 50, 20)),
|
||||
widgetapi.Options{WantMouse: true},
|
||||
widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
|
||||
&terminalapi.Keyboard{},
|
||||
)
|
||||
|
||||
@ -921,7 +921,7 @@ func TestMouse(t *testing.T) {
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
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.ButtonRelease},
|
||||
)
|
||||
@ -935,7 +935,7 @@ func TestMouse(t *testing.T) {
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: false})),
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeNone})),
|
||||
)
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
@ -954,14 +954,14 @@ func TestMouse(t *testing.T) {
|
||||
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},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
Border(draw.LineStyleLight),
|
||||
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,
|
||||
},
|
||||
{
|
||||
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},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
PlaceWidget(
|
||||
fakewidget.New(widgetapi.Options{
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
Ratio: image.Point{2, 1},
|
||||
}),
|
||||
),
|
||||
@ -1020,14 +1092,187 @@ func TestMouse(t *testing.T) {
|
||||
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},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
PlaceWidget(
|
||||
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},
|
||||
}),
|
||||
),
|
||||
@ -1044,7 +1289,7 @@ func TestMouse(t *testing.T) {
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
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},
|
||||
)
|
||||
return ft
|
||||
@ -1059,7 +1304,7 @@ func TestMouse(t *testing.T) {
|
||||
ft,
|
||||
PlaceWidget(
|
||||
fakewidget.New(widgetapi.Options{
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
Ratio: image.Point{9, 10},
|
||||
}),
|
||||
),
|
||||
@ -1076,7 +1321,7 @@ func TestMouse(t *testing.T) {
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
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},
|
||||
)
|
||||
return ft
|
||||
@ -1089,7 +1334,7 @@ func TestMouse(t *testing.T) {
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: true})),
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
|
||||
)
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Returns bool indicating if a click happened, the state of the button and the
|
||||
// next state of the FSM.
|
||||
|
@ -25,6 +25,9 @@ import (
|
||||
|
||||
// eventTestCase is one mouse event and the output expectation.
|
||||
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 *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",
|
||||
button: mouse.ButtonRight,
|
||||
@ -234,6 +300,10 @@ func TestFSM(t *testing.T) {
|
||||
t.Run(fmt.Sprintf(tc.desc), func(t *testing.T) {
|
||||
fsm := NewFSM(tc.button, tc.area)
|
||||
for _, etc := range tc.eventCases {
|
||||
if etc.area != nil {
|
||||
fsm.UpdateArea(*etc.area)
|
||||
}
|
||||
|
||||
gotClick, gotState := fsm.Event(etc.event)
|
||||
t.Logf("Called fsm.Event(%v) => %v, %v", etc.event, gotClick, gotState)
|
||||
if gotClick != etc.wantClick || gotState != etc.wantState {
|
||||
|
@ -49,7 +49,7 @@ func Example() {
|
||||
wOpts := widgetapi.Options{
|
||||
MinimumSize: fakewidget.MinimumSize,
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
}
|
||||
|
||||
// Create the container with two fake widgets.
|
||||
@ -89,7 +89,7 @@ func Example_triggered() {
|
||||
wOpts := widgetapi.Options{
|
||||
MinimumSize: fakewidget.MinimumSize,
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
}
|
||||
|
||||
// Create the container with a widget.
|
||||
@ -239,7 +239,7 @@ func TestRun(t *testing.T) {
|
||||
ft,
|
||||
testcanvas.MustNew(ft.Area()),
|
||||
widgetapi.Options{
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
|
||||
)
|
||||
@ -265,7 +265,7 @@ func TestRun(t *testing.T) {
|
||||
testcanvas.MustNew(ft.Area()),
|
||||
widgetapi.Options{
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||
)
|
||||
@ -360,7 +360,7 @@ func TestRun(t *testing.T) {
|
||||
ft,
|
||||
testcanvas.MustNew(ft.Area()),
|
||||
widgetapi.Options{
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
&terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp},
|
||||
)
|
||||
@ -394,7 +394,7 @@ func TestRun(t *testing.T) {
|
||||
got,
|
||||
container.PlaceWidget(fakewidget.New(widgetapi.Options{
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
})),
|
||||
)
|
||||
if err != nil {
|
||||
@ -460,7 +460,7 @@ func TestController(t *testing.T) {
|
||||
testcanvas.MustNew(ft.Area()),
|
||||
widgetapi.Options{
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||
)
|
||||
@ -580,7 +580,7 @@ func TestController(t *testing.T) {
|
||||
|
||||
mi := fakewidget.New(widgetapi.Options{
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
})
|
||||
cont, err := container.New(
|
||||
got,
|
||||
|
@ -21,15 +21,18 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"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/barchart"
|
||||
"github.com/mum4k/termdash/widgets/button"
|
||||
"github.com/mum4k/termdash/widgets/donut"
|
||||
"github.com/mum4k/termdash/widgets/gauge"
|
||||
"github.com/mum4k/termdash/widgets/linechart"
|
||||
@ -128,10 +131,34 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sineLC, err := newSines(ctx)
|
||||
leftB, rightB, sineLC, err := newSines(ctx)
|
||||
if err != nil {
|
||||
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{
|
||||
container.SplitHorizontal(
|
||||
container.Top(
|
||||
@ -148,12 +175,7 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
||||
container.BorderTitleAlignRight(),
|
||||
container.PlaceWidget(don),
|
||||
),
|
||||
container.Bottom(
|
||||
container.Border(draw.LineStyleLight),
|
||||
container.BorderTitle("Multiple series"),
|
||||
container.BorderTitleAlignRight(),
|
||||
container.PlaceWidget(sineLC),
|
||||
),
|
||||
container.Bottom(lcAndButtons...),
|
||||
container.SplitPercent(30),
|
||||
),
|
||||
),
|
||||
@ -422,23 +444,47 @@ func newBarChart(ctx context.Context) (*barchart.BarChart, error) {
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
// newSines returns a line chart that displays multiple sine series.
|
||||
func newSines(ctx context.Context) (*linechart.LineChart, error) {
|
||||
// distance is a thread-safe int value used by the newSince method.
|
||||
// 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
|
||||
for i := 0; i < 200; i++ {
|
||||
v := math.Sin(float64(i) / 100 * math.Pi)
|
||||
inputs = append(inputs, v)
|
||||
}
|
||||
|
||||
lc, err := linechart.New(
|
||||
sineLc, err := linechart.New(
|
||||
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
|
||||
linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
linechart.XLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
step1 := 0
|
||||
secondDist := &distance{v: 100}
|
||||
go periodic(ctx, redrawInterval/3, func() error {
|
||||
step1 = (step1 + 1) % len(inputs)
|
||||
if err := lc.Series("first", rotateFloats(inputs, step1),
|
||||
@ -447,10 +493,30 @@ func newSines(ctx context.Context) (*linechart.LineChart, error) {
|
||||
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, 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.
|
||||
|
@ -55,6 +55,54 @@ const (
|
||||
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.
|
||||
// This is how the widget indicates its needs to the infrastructure.
|
||||
type Options struct {
|
||||
@ -82,11 +130,13 @@ type Options struct {
|
||||
// forwarded to the widget.
|
||||
WantKeyboard KeyScope
|
||||
|
||||
// WantMouse allows a widget to request mouse events.
|
||||
// If false, mouse events won't be forwarded to the widget.
|
||||
// If true, the widget receives all mouse events whose coordinates fall
|
||||
// within its canvas.
|
||||
WantMouse bool
|
||||
// WantMouse allows a widget to request mouse events and specify their
|
||||
// desired scope. If set to MouseScopeNone, no mouse events are forwarded
|
||||
// to the widget.
|
||||
// Note that the widget is only able to see the position of the mouse event
|
||||
// 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.
|
||||
|
@ -270,7 +270,7 @@ func (bc *BarChart) Options() widgetapi.Options {
|
||||
return widgetapi.Options{
|
||||
MinimumSize: bc.minSize(),
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -629,7 +629,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -642,7 +642,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -660,7 +660,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -678,7 +678,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{3, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -699,7 +699,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{8, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -720,7 +720,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{8, 2},
|
||||
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.
|
||||
MinimumSize: image.Point{3, 3},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
}
|
||||
|
@ -666,7 +666,7 @@ func TestOptions(t *testing.T) {
|
||||
Ratio: image.Point{4, 2},
|
||||
MinimumSize: image.Point{3, 3},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
if diff := pretty.Compare(want, got); 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 {
|
||||
switch e := ev.(type) {
|
||||
case *terminalapi.Mouse:
|
||||
if !mirror.opts.WantMouse {
|
||||
if mirror.opts.WantMouse == widgetapi.MouseScopeNone {
|
||||
continue
|
||||
}
|
||||
if err := mirror.Mouse(e); err != nil {
|
||||
|
@ -324,7 +324,7 @@ func TestDraw(t *testing.T) {
|
||||
desc: "draws both keyboard and mouse events",
|
||||
opts: widgetapi.Options{
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
cvs: testcanvas.MustNew(image.Rect(0, 0, 17, 5)),
|
||||
events: []terminalapi.Event{
|
||||
|
@ -330,6 +330,6 @@ func (g *Gauge) Options() widgetapi.Options {
|
||||
MaximumSize: g.maxSize(),
|
||||
MinimumSize: g.minSize(),
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
}
|
||||
|
@ -814,7 +814,7 @@ func TestOptions(t *testing.T) {
|
||||
MaximumSize: image.Point{0, 0}, // Unlimited.
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -826,7 +826,7 @@ func TestOptions(t *testing.T) {
|
||||
MaximumSize: image.Point{0, 2},
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -839,7 +839,7 @@ func TestOptions(t *testing.T) {
|
||||
MaximumSize: image.Point{0, 4},
|
||||
MinimumSize: image.Point{3, 3},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -492,7 +492,7 @@ func (lc *LineChart) Options() widgetapi.Options {
|
||||
|
||||
return widgetapi.Options{
|
||||
MinimumSize: lc.minSize(),
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1514,7 +1514,7 @@ func TestOptions(t *testing.T) {
|
||||
desc: "reserves space for axis without series",
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{3, 4},
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1524,7 +1524,7 @@ func TestOptions(t *testing.T) {
|
||||
},
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{5, 4},
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1534,7 +1534,7 @@ func TestOptions(t *testing.T) {
|
||||
},
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{6, 4},
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1547,7 +1547,7 @@ func TestOptions(t *testing.T) {
|
||||
},
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{4, 5},
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1560,7 +1560,7 @@ func TestOptions(t *testing.T) {
|
||||
},
|
||||
want: widgetapi.Options{
|
||||
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.
|
||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
}
|
||||
|
@ -822,7 +822,7 @@ func TestOptions(t *testing.T) {
|
||||
want := widgetapi.Options{
|
||||
MinimumSize: image.Point{sixteen.MinCols, sixteen.MinRows},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
if diff := pretty.Compare(want, got); diff != "" {
|
||||
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
||||
|
@ -231,6 +231,6 @@ func (sl *SparkLine) Options() widgetapi.Options {
|
||||
MinimumSize: min,
|
||||
MaximumSize: max,
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
}
|
||||
}
|
||||
|
@ -470,7 +470,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -481,7 +481,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 2},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -493,7 +493,7 @@ func TestOptions(t *testing.T) {
|
||||
MinimumSize: image.Point{1, 3},
|
||||
MaximumSize: image.Point{1, 3},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -506,7 +506,7 @@ func TestOptions(t *testing.T) {
|
||||
MinimumSize: image.Point{1, 4},
|
||||
MaximumSize: image.Point{1, 4},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -303,16 +303,19 @@ func (t *Text) Mouse(m *terminalapi.Mouse) error {
|
||||
// Options of the widget
|
||||
func (t *Text) Options() widgetapi.Options {
|
||||
var ks widgetapi.KeyScope
|
||||
var ms widgetapi.MouseScope
|
||||
if t.opts.disableScrolling {
|
||||
ks = widgetapi.KeyScopeNone
|
||||
ms = widgetapi.MouseScopeNone
|
||||
} else {
|
||||
ks = widgetapi.KeyScopeFocused
|
||||
ms = widgetapi.MouseScopeWidget
|
||||
}
|
||||
|
||||
return widgetapi.Options{
|
||||
// At least one line with at least one full-width rune.
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantMouse: !t.opts.disableScrolling,
|
||||
WantMouse: ms,
|
||||
WantKeyboard: ks,
|
||||
}
|
||||
}
|
||||
|
@ -785,7 +785,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeFocused,
|
||||
WantMouse: true,
|
||||
WantMouse: widgetapi.MouseScopeWidget,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -796,7 +796,7 @@ func TestOptions(t *testing.T) {
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantKeyboard: widgetapi.KeyScopeNone,
|
||||
WantMouse: false,
|
||||
WantMouse: widgetapi.MouseScopeNone,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user