diff --git a/CHANGELOG.md b/CHANGELOG.md index 769935f..85c792c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `container` now allows users to configure keyboard keys that move focus to the next or the previous container. - the `button` widget allows users to specify multiple trigger keys. -- the `button` widget can now be drawn without the shadow and the pressing the +- the `button` widget can now be drawn without the shadow and the press animation. - the `button` widget can now be drawn without horizontal padding around its text. - the `button`widget now allows specifying cell options for each cell of the - displayed text. + displayed text. Separate cell options can be specified for each of button's + main states (up, focused and up, down). +- the `button` widget allows specifying separate fill color values for each of + its main states (up, focused and up, down). +- added a new demo called `formdemo` under the `button` widget that + demonstrates the ability of `termdash` to display forms with editable content + and keyboard navigation. ## [0.13.0] - 17-Nov-2020 diff --git a/widgets/button/button.go b/widgets/button/button.go index 8cab2a5..17110df 100644 --- a/widgets/button/button.go +++ b/widgets/button/button.go @@ -196,7 +196,17 @@ func (b *Button) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error { buttonAr = shadowAr } - if err := cvs.SetAreaCells(buttonAr, buttonRune, cell.BgColor(b.opts.fillColor)); err != nil { + var fillColor cell.Color + switch { + case b.state == button.Down && b.opts.pressedFillColor != nil: + fillColor = *b.opts.pressedFillColor + case meta.Focused && b.opts.focusedFillColor != nil: + fillColor = *b.opts.focusedFillColor + default: + fillColor = b.opts.fillColor + } + + if err := cvs.SetAreaCells(buttonAr, buttonRune, cell.BgColor(fillColor)); err != nil { return err } @@ -229,7 +239,16 @@ func (b *Button) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error { } tOpts := b.givenTOpts[optRange.AttrIdx] - cells, err := cvs.SetCell(cur, r, tOpts.cellOpts...) + var cellOpts []cell.Option + switch { + case b.state == button.Down && len(tOpts.pressedCellOpts) > 0: + cellOpts = tOpts.pressedCellOpts + case meta.Focused && len(tOpts.focusedCellOpts) > 0: + cellOpts = tOpts.focusedCellOpts + default: + cellOpts = tOpts.cellOpts + } + cells, err := cvs.SetCell(cur, r, cellOpts...) if err != nil { return err } diff --git a/widgets/button/button_test.go b/widgets/button/button_test.go index 733d4df..bc7206b 100644 --- a/widgets/button/button_test.go +++ b/widgets/button/button_test.go @@ -65,8 +65,12 @@ func (ct *callbackTracker) callback() error { func TestButton(t *testing.T) { tests := []struct { - desc string - text string + desc string + + // Only one of these must be specified. + text string // Calls New() as the constructor. + textChunks []*TextChunk // Calls NewFromChunks() as the constructor. + callback *callbackTracker opts []Option events []terminalapi.Event @@ -86,6 +90,7 @@ func TestButton(t *testing.T) { { desc: "New fails with nil callback", canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, wantNewErr: true, }, { @@ -95,6 +100,7 @@ func TestButton(t *testing.T) { KeyUpDelay(-1 * time.Second), }, canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, wantNewErr: true, }, { @@ -104,6 +110,7 @@ func TestButton(t *testing.T) { Height(0), }, canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, wantNewErr: true, }, { @@ -113,6 +120,7 @@ func TestButton(t *testing.T) { Width(0), }, canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, wantNewErr: true, }, { @@ -122,6 +130,79 @@ func TestButton(t *testing.T) { TextHorizontalPadding(-1), }, canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, + wantNewErr: true, + }, + { + desc: "NewFromChunks fails with nil callback", + textChunks: []*TextChunk{ + NewChunk("text"), + }, + canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, + wantNewErr: true, + }, + { + desc: "NewFromChunks fails with negative keyUpDelay", + textChunks: []*TextChunk{ + NewChunk("text"), + }, + callback: &callbackTracker{}, + opts: []Option{ + KeyUpDelay(-1 * time.Second), + }, + canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, + wantNewErr: true, + }, + { + desc: "NewFromChunks fails with zero Height", + textChunks: []*TextChunk{ + NewChunk("text"), + }, + callback: &callbackTracker{}, + opts: []Option{ + Height(0), + }, + canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, + wantNewErr: true, + }, + { + desc: "NewFromChunks fails with zero Width", + textChunks: []*TextChunk{ + NewChunk("text"), + }, + callback: &callbackTracker{}, + opts: []Option{ + Width(0), + }, + canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, + wantNewErr: true, + }, + { + desc: "NewFromChunks fails with negative textHorizontalPadding", + textChunks: []*TextChunk{ + NewChunk("text"), + }, + callback: &callbackTracker{}, + opts: []Option{ + TextHorizontalPadding(-1), + }, + canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, + wantNewErr: true, + }, + { + desc: "NewFromChunks fails with zero chunks", + textChunks: []*TextChunk{}, + callback: &callbackTracker{}, + opts: []Option{ + TextHorizontalPadding(-1), + }, + canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, wantNewErr: true, }, { @@ -129,6 +210,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "hello", canvas: image.Rect(0, 0, 1, 1), + meta: &widgetapi.Meta{Focused: false}, wantDrawErr: true, }, { @@ -136,6 +218,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) cvs := testcanvas.MustNew(ft.Area()) @@ -166,6 +249,7 @@ func TestButton(t *testing.T) { }, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) cvs := testcanvas.MustNew(ft.Area()) @@ -190,6 +274,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, }, @@ -220,6 +305,7 @@ func TestButton(t *testing.T) { }, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, }, @@ -247,6 +333,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, @@ -284,6 +371,7 @@ func TestButton(t *testing.T) { Key(keyboard.KeyEnter), }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyEnter}, }, @@ -317,6 +405,7 @@ func TestButton(t *testing.T) { Keys(keyboard.KeyEnter, keyboard.KeyTab), }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyTab}, }, @@ -350,6 +439,7 @@ func TestButton(t *testing.T) { GlobalKey(keyboard.KeyTab), }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyTab}, }, @@ -383,6 +473,7 @@ func TestButton(t *testing.T) { GlobalKeys(keyboard.KeyEnter, keyboard.KeyTab), }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyTab}, }, @@ -413,6 +504,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyEnter}, }, @@ -449,6 +541,7 @@ func TestButton(t *testing.T) { return 200 * time.Millisecond }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyEnter}, }, @@ -486,6 +579,7 @@ func TestButton(t *testing.T) { return 200 * time.Millisecond }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyEnter}, }, @@ -526,6 +620,7 @@ func TestButton(t *testing.T) { return 200 * time.Millisecond }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyEnter}, &terminalapi.Keyboard{Key: keyboard.KeyEnter}, @@ -561,6 +656,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, @@ -599,6 +695,7 @@ func TestButton(t *testing.T) { }, text: "hello", canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease}, @@ -619,6 +716,7 @@ func TestButton(t *testing.T) { return 200 * time.Millisecond }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, events: []terminalapi.Event{ &terminalapi.Keyboard{Key: keyboard.KeyEnter}, }, @@ -629,6 +727,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "hello", canvas: image.Rect(0, 0, 8, 2), + meta: &widgetapi.Meta{Focused: false}, want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) cvs := testcanvas.MustNew(ft.Area()) @@ -656,6 +755,7 @@ func TestButton(t *testing.T) { callback: &callbackTracker{}, text: "h", canvas: image.Rect(0, 0, 4, 2), + meta: &widgetapi.Meta{Focused: false}, want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) cvs := testcanvas.MustNew(ft.Area()) @@ -686,6 +786,7 @@ func TestButton(t *testing.T) { TextColor(cell.ColorRed), }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) cvs := testcanvas.MustNew(ft.Area()) @@ -716,6 +817,7 @@ func TestButton(t *testing.T) { FillColor(cell.ColorRed), }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) cvs := testcanvas.MustNew(ft.Area()) @@ -746,6 +848,7 @@ func TestButton(t *testing.T) { ShadowColor(cell.ColorRed), }, canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, want: func(size image.Point) *faketerm.Terminal { ft := faketerm.MustNew(size) cvs := testcanvas.MustNew(ft.Area()) @@ -768,6 +871,303 @@ func TestButton(t *testing.T) { }, wantCallback: &callbackTracker{}, }, + { + desc: "draws button with text chunks and custom fill color in up state", + callback: &callbackTracker{}, + opts: []Option{ + FillColor(cell.ColorBlue), + FocusedFillColor(cell.ColorYellow), + PressedFillColor(cell.ColorRed), + DisableShadow(), + }, + textChunks: []*TextChunk{ + NewChunk( + "h", + TextCellOpts(cell.FgColor(cell.ColorBlack)), + FocusedTextCellOpts(cell.FgColor(cell.ColorWhite)), + PressedTextCellOpts(cell.FgColor(cell.ColorGreen)), + ), + NewChunk( + "ello", + TextCellOpts(cell.FgColor(cell.ColorMagenta)), + FocusedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + PressedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + ), + }, + canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + + // Button. + testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 8, 4), 'x', cell.BgColor(cell.ColorBlue)) + + // Text. + testdraw.MustText(cvs, "h", image.Point{1, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorBlack), + cell.BgColor(cell.ColorBlue)), + ) + testdraw.MustText(cvs, "ello", image.Point{2, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorMagenta), + cell.BgColor(cell.ColorBlue)), + ) + + testcanvas.MustApply(cvs, ft) + return ft + }, + wantCallback: &callbackTracker{}, + }, + { + desc: "draws button with text chunks and custom fill color in focused up state", + callback: &callbackTracker{}, + opts: []Option{ + FillColor(cell.ColorBlue), + FocusedFillColor(cell.ColorYellow), + PressedFillColor(cell.ColorRed), + DisableShadow(), + }, + textChunks: []*TextChunk{ + NewChunk( + "h", + TextCellOpts(cell.FgColor(cell.ColorBlack)), + FocusedTextCellOpts(cell.FgColor(cell.ColorWhite)), + PressedTextCellOpts(cell.FgColor(cell.ColorGreen)), + ), + NewChunk( + "ello", + TextCellOpts(cell.FgColor(cell.ColorMagenta)), + FocusedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + PressedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + ), + }, + canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: true}, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + + // Button. + testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 8, 4), 'x', cell.BgColor(cell.ColorYellow)) + + // Text. + testdraw.MustText(cvs, "h", image.Point{1, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorWhite), + cell.BgColor(cell.ColorYellow)), + ) + testdraw.MustText(cvs, "ello", image.Point{2, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorMagenta), + cell.BgColor(cell.ColorYellow)), + ) + + testcanvas.MustApply(cvs, ft) + return ft + }, + wantCallback: &callbackTracker{}, + }, + { + desc: "draws button with text chunks in up state, focused colors default to regular colors", + callback: &callbackTracker{}, + opts: []Option{ + FillColor(cell.ColorBlue), + PressedFillColor(cell.ColorRed), + DisableShadow(), + }, + textChunks: []*TextChunk{ + NewChunk( + "h", + TextCellOpts(cell.FgColor(cell.ColorBlack)), + PressedTextCellOpts(cell.FgColor(cell.ColorGreen)), + ), + NewChunk( + "ello", + TextCellOpts(cell.FgColor(cell.ColorMagenta)), + PressedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + ), + }, + canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: true}, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + + // Button. + testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 8, 4), 'x', cell.BgColor(cell.ColorBlue)) + + // Text. + testdraw.MustText(cvs, "h", image.Point{1, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorBlack), + cell.BgColor(cell.ColorBlue)), + ) + testdraw.MustText(cvs, "ello", image.Point{2, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorMagenta), + cell.BgColor(cell.ColorBlue)), + ) + + testcanvas.MustApply(cvs, ft) + return ft + }, + wantCallback: &callbackTracker{}, + }, + { + desc: "draws button with text chunks and custom fill color in down state", + events: []terminalapi.Event{ + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, + }, + callback: &callbackTracker{}, + opts: []Option{ + FillColor(cell.ColorBlue), + FocusedFillColor(cell.ColorYellow), + PressedFillColor(cell.ColorRed), + DisableShadow(), + }, + textChunks: []*TextChunk{ + NewChunk( + "h", + TextCellOpts(cell.FgColor(cell.ColorBlack)), + FocusedTextCellOpts(cell.FgColor(cell.ColorWhite)), + PressedTextCellOpts(cell.FgColor(cell.ColorGreen)), + ), + NewChunk( + "ello", + TextCellOpts(cell.FgColor(cell.ColorMagenta)), + FocusedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + PressedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + ), + }, + canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + + // Button. + testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 8, 4), 'x', cell.BgColor(cell.ColorRed)) + + // Text. + testdraw.MustText(cvs, "h", image.Point{1, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorGreen), + cell.BgColor(cell.ColorRed)), + ) + testdraw.MustText(cvs, "ello", image.Point{2, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorMagenta), + cell.BgColor(cell.ColorRed)), + ) + + testcanvas.MustApply(cvs, ft) + return ft + }, + wantCallback: &callbackTracker{}, + }, + { + desc: "draws button with text chunks and custom fill color in down focused state (focus has no impact)", + events: []terminalapi.Event{ + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, + }, + callback: &callbackTracker{}, + opts: []Option{ + FillColor(cell.ColorBlue), + FocusedFillColor(cell.ColorYellow), + PressedFillColor(cell.ColorRed), + DisableShadow(), + }, + textChunks: []*TextChunk{ + NewChunk( + "h", + TextCellOpts(cell.FgColor(cell.ColorBlack)), + FocusedTextCellOpts(cell.FgColor(cell.ColorWhite)), + PressedTextCellOpts(cell.FgColor(cell.ColorGreen)), + ), + NewChunk( + "ello", + TextCellOpts(cell.FgColor(cell.ColorMagenta)), + FocusedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + PressedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + ), + }, + canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: true}, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + + // Button. + testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 8, 4), 'x', cell.BgColor(cell.ColorRed)) + + // Text. + testdraw.MustText(cvs, "h", image.Point{1, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorGreen), + cell.BgColor(cell.ColorRed)), + ) + testdraw.MustText(cvs, "ello", image.Point{2, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorMagenta), + cell.BgColor(cell.ColorRed)), + ) + + testcanvas.MustApply(cvs, ft) + return ft + }, + wantCallback: &callbackTracker{}, + }, + { + desc: "draws button with text chunks in down satte, pressed colors default to regular colors", + events: []terminalapi.Event{ + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft}, + }, + callback: &callbackTracker{}, + opts: []Option{ + FillColor(cell.ColorBlue), + FocusedFillColor(cell.ColorYellow), + DisableShadow(), + }, + textChunks: []*TextChunk{ + NewChunk( + "h", + TextCellOpts(cell.FgColor(cell.ColorBlack)), + FocusedTextCellOpts(cell.FgColor(cell.ColorWhite)), + ), + NewChunk( + "ello", + TextCellOpts(cell.FgColor(cell.ColorMagenta)), + FocusedTextCellOpts(cell.FgColor(cell.ColorMagenta)), + ), + }, + canvas: image.Rect(0, 0, 8, 4), + meta: &widgetapi.Meta{Focused: false}, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + + // Button. + testcanvas.MustSetAreaCells(cvs, image.Rect(0, 0, 8, 4), 'x', cell.BgColor(cell.ColorBlue)) + + // Text. + testdraw.MustText(cvs, "h", image.Point{1, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorBlack), + cell.BgColor(cell.ColorBlue)), + ) + testdraw.MustText(cvs, "ello", image.Point{2, 1}, + draw.TextCellOpts( + cell.FgColor(cell.ColorMagenta), + cell.BgColor(cell.ColorBlue)), + ) + + testcanvas.MustApply(cvs, ft) + return ft + }, + wantCallback: &callbackTracker{}, + }, } buttonRune = 'x' @@ -787,12 +1187,30 @@ func TestButton(t *testing.T) { } 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 tc.text != "" && tc.textChunks != nil { + t.Fatalf("cannot specify both text and textChunks in the testdata") } - if err != nil { - return + + var btn *Button + if tc.textChunks != nil { + b, err := NewFromChunks(tc.textChunks, cFn, tc.opts...) + if (err != nil) != tc.wantNewErr { + t.Errorf("NewFromChunks => unexpected error: %v, wantNewErr: %v", err, tc.wantNewErr) + } + if err != nil { + return + } + btn = b + } else { + 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 + } + btn = b } { @@ -801,7 +1219,7 @@ func TestButton(t *testing.T) { if err != nil { t.Fatalf("canvas.New => unexpected error: %v", err) } - err = b.Draw(c, tc.meta) + err = btn.Draw(c, tc.meta) if (err != nil) != tc.wantDrawErr { t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr) } @@ -813,7 +1231,7 @@ func TestButton(t *testing.T) { for i, ev := range tc.events { switch e := ev.(type) { case *terminalapi.Mouse: - err := b.Mouse(e) + err := btn.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 { @@ -829,7 +1247,7 @@ func TestButton(t *testing.T) { } case *terminalapi.Keyboard: - err := b.Keyboard(e) + err := btn.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 { @@ -854,7 +1272,7 @@ func TestButton(t *testing.T) { t.Fatalf("canvas.New => unexpected error: %v", err) } - err = b.Draw(c, tc.meta) + err = btn.Draw(c, tc.meta) if (err != nil) != tc.wantDrawErr { t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr) } diff --git a/widgets/button/formdemo/formdemo.go b/widgets/button/formdemo/formdemo.go index 37e1716..716f096 100644 --- a/widgets/button/formdemo/formdemo.go +++ b/widgets/button/formdemo/formdemo.go @@ -31,6 +31,40 @@ import ( "github.com/mum4k/termdash/widgets/segmentdisplay" ) +// buttonChunks creates the text chunks for a button from the provided text. +func buttonChunks(text string) []*button.TextChunk { + if len(text) == 0 { + return nil + } + first := string(text[0]) + rest := string(text[1:]) + + return []*button.TextChunk{ + button.NewChunk( + "<", + button.TextCellOpts(cell.FgColor(cell.ColorWhite)), + button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)), + button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)), + ), + button.NewChunk( + first, + button.TextCellOpts(cell.FgColor(cell.ColorRed)), + ), + button.NewChunk( + rest, + button.TextCellOpts(cell.FgColor(cell.ColorWhite)), + button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)), + button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)), + ), + button.NewChunk( + ">", + button.TextCellOpts(cell.FgColor(cell.ColorWhite)), + button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)), + button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)), + ), + } +} + func main() { t, err := tcell.New() if err != nil { @@ -51,7 +85,7 @@ func main() { panic(err) } - addB, err := button.New("", func() error { + addB, err := button.NewFromChunks(buttonChunks("Submit"), func() error { val++ return display.Write([]*segmentdisplay.TextChunk{ segmentdisplay.NewChunk(fmt.Sprintf("%d", val)), @@ -61,12 +95,15 @@ func main() { button.DisableShadow(), button.Height(1), button.TextHorizontalPadding(0), + button.FillColor(cell.ColorBlack), + button.FocusedFillColor(cell.ColorNumber(117)), + button.PressedFillColor(cell.ColorNumber(220)), ) if err != nil { panic(err) } - subB, err := button.New("", func() error { + subB, err := button.NewFromChunks(buttonChunks("Cancel"), func() error { val-- return display.Write([]*segmentdisplay.TextChunk{ segmentdisplay.NewChunk(fmt.Sprintf("%d", val)), @@ -77,6 +114,9 @@ func main() { button.DisableShadow(), button.Height(1), button.TextHorizontalPadding(0), + button.FillColor(cell.ColorBlack), + button.FocusedFillColor(cell.ColorNumber(117)), + button.PressedFillColor(cell.ColorNumber(220)), ) if err != nil { panic(err) diff --git a/widgets/button/options.go b/widgets/button/options.go index a5c139d..144db76 100644 --- a/widgets/button/options.go +++ b/widgets/button/options.go @@ -43,6 +43,8 @@ func (o option) set(opts *options) { // options holds the provided options. type options struct { fillColor cell.Color + focusedFillColor *cell.Color + pressedFillColor *cell.Color textColor cell.Color textHorizontalPadding int shadowColor cell.Color @@ -98,6 +100,23 @@ func FillColor(c cell.Color) Option { }) } +// FocusedFillColor sets the fill color of the button when the widget's +// container is focused. +// Defaults to FillColor. +func FocusedFillColor(c cell.Color) Option { + return option(func(opts *options) { + opts.focusedFillColor = &c + }) +} + +// PressedFillColor sets the fill color of the button when it is pressed. +// Defaults to FillColor. +func PressedFillColor(c cell.Color) Option { + return option(func(opts *options) { + opts.pressedFillColor = &c + }) +} + // TextColor sets the color of the text label in the button. func TextColor(c cell.Color) Option { return option(func(opts *options) { diff --git a/widgets/button/text_options.go b/widgets/button/text_options.go index 0d4cd25..3311229 100644 --- a/widgets/button/text_options.go +++ b/widgets/button/text_options.go @@ -26,7 +26,9 @@ type TextOption interface { // textOptions stores the provided options. type textOptions struct { - cellOpts []cell.Option + cellOpts []cell.Option + focusedCellOpts []cell.Option + pressedCellOpts []cell.Option } // setDefaultFgColor configures a default color for text if one isn't specified @@ -69,6 +71,15 @@ func TextCellOpts(opts ...cell.Option) TextOption { // If not specified, TextCellOpts will be used instead. func FocusedTextCellOpts(opts ...cell.Option) TextOption { return textOption(func(tOpts *textOptions) { - tOpts.cellOpts = opts + tOpts.focusedCellOpts = opts + }) +} + +// PressedTextCellOpts sets options on the cells that contain the button text +// when it is pressed. +// If not specified, TextCellOpts will be used instead. +func PressedTextCellOpts(opts ...cell.Option) TextOption { + return textOption(func(tOpts *textOptions) { + tOpts.pressedCellOpts = opts }) }