mirror of
https://github.com/mum4k/termdash.git
synced 2025-05-01 22:17:51 +08:00
Merge pull request #270 from mum4k/243-key-based-focus
Ability to move focus using keyboard.
This commit is contained in:
commit
42af42e991
@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ability to configure keyboard keys that move focus to the next or the
|
||||
previous container.
|
||||
|
||||
## [0.13.0] - 17-Nov-2020
|
||||
|
||||
### Added
|
||||
|
@ -122,6 +122,12 @@ func (c *Container) hasWidget() bool {
|
||||
return c.opts.widget != nil
|
||||
}
|
||||
|
||||
// isLeaf determines if this container is a leaf container in the binary tree of containers.
|
||||
// Only leaf containers are guaranteed to be "visible" on the screen.
|
||||
func (c *Container) isLeaf() bool {
|
||||
return c.first == nil && c.second == nil
|
||||
}
|
||||
|
||||
// usable returns the usable area in this container.
|
||||
// This depends on whether the container has a border, etc.
|
||||
func (c *Container) usable() image.Rectangle {
|
||||
@ -257,10 +263,10 @@ func (c *Container) Update(id string, opts ...Option) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateFocus processes the mouse event and determines if it changes the
|
||||
// focused container.
|
||||
// updateFocusFromMouse processes the mouse event and determines if it changes
|
||||
// the focused container.
|
||||
// Caller must hold c.mu.
|
||||
func (c *Container) updateFocus(m *terminalapi.Mouse) {
|
||||
func (c *Container) updateFocusFromMouse(m *terminalapi.Mouse) {
|
||||
target := pointCont(c, m.Position)
|
||||
if target == nil { // Ignore mouse clicks where no containers are.
|
||||
return
|
||||
@ -268,6 +274,18 @@ func (c *Container) updateFocus(m *terminalapi.Mouse) {
|
||||
c.focusTracker.mouse(target, m)
|
||||
}
|
||||
|
||||
// updateFocusFromKeyboard processes the keyboard event and determines if it
|
||||
// changes the focused container.
|
||||
// Caller must hold c.mu.
|
||||
func (c *Container) updateFocusFromKeyboard(k *terminalapi.Keyboard) {
|
||||
switch {
|
||||
case c.opts.global.keyFocusNext != nil && *c.opts.global.keyFocusNext == k.Key:
|
||||
c.focusTracker.next()
|
||||
case c.opts.global.keyFocusPrevious != nil && *c.opts.global.keyFocusPrevious == k.Key:
|
||||
c.focusTracker.previous()
|
||||
}
|
||||
}
|
||||
|
||||
// processEvent processes events delivered to the container.
|
||||
func (c *Container) processEvent(ev terminalapi.Event) error {
|
||||
// This is done in two stages.
|
||||
@ -293,7 +311,7 @@ func (c *Container) processEvent(ev terminalapi.Event) error {
|
||||
func (c *Container) prepareEvTargets(ev terminalapi.Event) (func() error, error) {
|
||||
switch e := ev.(type) {
|
||||
case *terminalapi.Mouse:
|
||||
c.updateFocus(ev.(*terminalapi.Mouse))
|
||||
c.updateFocusFromMouse(ev.(*terminalapi.Mouse))
|
||||
|
||||
targets, err := c.mouseEvTargets(e)
|
||||
if err != nil {
|
||||
@ -310,6 +328,12 @@ func (c *Container) prepareEvTargets(ev terminalapi.Event) (func() error, error)
|
||||
|
||||
case *terminalapi.Keyboard:
|
||||
targets := c.keyEvTargets()
|
||||
|
||||
// Update the focused container based on the pressed key.
|
||||
// Done after collecting "targets" above. If the key changes which
|
||||
// widget is focused, they key press itself should go to the widget
|
||||
// that was focused when the key was pressed.
|
||||
c.updateFocusFromKeyboard(ev.(*terminalapi.Keyboard))
|
||||
return func() error {
|
||||
for _, w := range targets {
|
||||
if err := w.Keyboard(e); err != nil {
|
||||
|
@ -1510,6 +1510,182 @@ func TestMouse(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "key event focuses the next container, widget with KeyScopeFocused does not get the key as it was not focused yet",
|
||||
termSize: image.Point{50, 20},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
c, err := New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
),
|
||||
Right(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
),
|
||||
),
|
||||
KeyFocusNext(keyboard.KeyTab),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(0, 0, 25, 20)),
|
||||
&widgetapi.Meta{Focused: true},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
)
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(25, 0, 50, 20)),
|
||||
&widgetapi.Meta{},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
&terminalapi.Keyboard{},
|
||||
)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "key event focuses the previous container, option set on both parent and child, the last option is used since focus keys are global options",
|
||||
termSize: image.Point{50, 20},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
c, err := New(
|
||||
ft,
|
||||
KeyFocusPrevious(keyboard.KeyEnter),
|
||||
SplitVertical(
|
||||
Left(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
),
|
||||
Right(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
KeyFocusPrevious(keyboard.KeyTab),
|
||||
),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(0, 0, 25, 20)),
|
||||
&widgetapi.Meta{},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
)
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(25, 0, 50, 20)),
|
||||
&widgetapi.Meta{Focused: true},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
&terminalapi.Keyboard{},
|
||||
)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "key event focuses the next container, widget with KeyScopeGlobal also gets the key",
|
||||
termSize: image.Point{50, 20},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
c, err := New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
),
|
||||
Right(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeGlobal})),
|
||||
),
|
||||
),
|
||||
KeyFocusNext(keyboard.KeyTab),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(0, 0, 25, 20)),
|
||||
&widgetapi.Meta{Focused: true},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
)
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(25, 0, 50, 20)),
|
||||
&widgetapi.Meta{},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeGlobal},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "key event moves focus from a widget with KeyScopeFocused, the originally focused widget gets the key",
|
||||
termSize: image.Point{50, 20},
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
c, err := New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
),
|
||||
Right(
|
||||
PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
|
||||
),
|
||||
),
|
||||
KeyFocusNext(keyboard.KeyTab),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
// Focus the left container.
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
// Move focus from left to right.
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(0, 0, 25, 20)),
|
||||
&widgetapi.Meta{},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyTab},
|
||||
)
|
||||
fakewidget.MustDraw(
|
||||
ft,
|
||||
testcanvas.MustNew(image.Rect(25, 0, 50, 20)),
|
||||
&widgetapi.Meta{Focused: true},
|
||||
widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
|
||||
)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "event not forwarded if the widget didn't request it",
|
||||
termSize: image.Point{20, 20},
|
||||
|
@ -78,6 +78,80 @@ func (ft *focusTracker) setActive(c *Container) {
|
||||
ft.container = c
|
||||
}
|
||||
|
||||
// next moves focus to the next container.
|
||||
func (ft *focusTracker) next() {
|
||||
var (
|
||||
errStr string
|
||||
firstCont *Container
|
||||
nextCont *Container
|
||||
focusNext bool
|
||||
)
|
||||
preOrder(rootCont(ft.container), &errStr, visitFunc(func(c *Container) error {
|
||||
if nextCont != nil {
|
||||
// Already found the next container, nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.isLeaf() && firstCont == nil {
|
||||
// Remember the first eligible container in case we "wrap" over,
|
||||
// i.e. finish the iteration before finding the next container.
|
||||
firstCont = c
|
||||
}
|
||||
|
||||
if ft.container == c {
|
||||
// Visiting the currently focused container, going to focus the
|
||||
// next one.
|
||||
focusNext = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.isLeaf() && focusNext {
|
||||
nextCont = c
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
if nextCont == nil && firstCont != nil {
|
||||
// If the traversal finishes without finding the next container, move
|
||||
// focus back to the first container.
|
||||
nextCont = firstCont
|
||||
}
|
||||
if nextCont != nil {
|
||||
ft.setActive(nextCont)
|
||||
}
|
||||
}
|
||||
|
||||
// previous moves focus to the previous container.
|
||||
func (ft *focusTracker) previous() {
|
||||
var (
|
||||
errStr string
|
||||
prevCont *Container
|
||||
lastCont *Container
|
||||
visitedCurr bool
|
||||
)
|
||||
preOrder(rootCont(ft.container), &errStr, visitFunc(func(c *Container) error {
|
||||
if ft.container == c {
|
||||
visitedCurr = true
|
||||
}
|
||||
|
||||
if c.isLeaf() {
|
||||
if !visitedCurr {
|
||||
// Remember the last eligible container closest to the one
|
||||
// currently focused.
|
||||
prevCont = c
|
||||
}
|
||||
lastCont = c
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
if prevCont != nil {
|
||||
ft.setActive(prevCont)
|
||||
} else if lastCont != nil {
|
||||
ft.setActive(lastCont)
|
||||
}
|
||||
}
|
||||
|
||||
// mouse identifies mouse events that change the focused container and track
|
||||
// the focused container in the tree.
|
||||
// The argument c is the container onto which the mouse event landed.
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/keyboard"
|
||||
"github.com/mum4k/termdash/linestyle"
|
||||
"github.com/mum4k/termdash/mouse"
|
||||
"github.com/mum4k/termdash/private/event"
|
||||
@ -453,3 +454,333 @@ func TestFocusTrackerMouse(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// contDir represents a direction in which we want to change container focus.
|
||||
type contDir int
|
||||
|
||||
// String implements fmt.Stringer()
|
||||
func (cd contDir) String() string {
|
||||
if n, ok := contDirNames[cd]; ok {
|
||||
return n
|
||||
}
|
||||
return "contDirUnknown"
|
||||
}
|
||||
|
||||
// contDirNames maps contDir values to human readable names.
|
||||
var contDirNames = map[contDir]string{
|
||||
contDirNext: "contDirNext",
|
||||
contDirPrevious: "contDirPrevious",
|
||||
}
|
||||
|
||||
const (
|
||||
contDirUnknown contDir = iota
|
||||
contDirNext
|
||||
contDirPrevious
|
||||
)
|
||||
|
||||
func TestFocusTrackerNextAndPrevious(t *testing.T) {
|
||||
ft, err := faketerm.New(image.Point{10, 10})
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
const (
|
||||
keyNext keyboard.Key = keyboard.KeyTab
|
||||
keyPrevious keyboard.Key = '~'
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
container func(ft *faketerm.Terminal) (*Container, error)
|
||||
events []*terminalapi.Keyboard
|
||||
wantFocused contLoc
|
||||
wantProcessed int
|
||||
}{
|
||||
{
|
||||
desc: "initially the root is focused",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusNext(keyNext),
|
||||
)
|
||||
},
|
||||
wantFocused: contLocRoot,
|
||||
},
|
||||
{
|
||||
desc: "keyNext does nothing when only root exists",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
KeyFocusNext(keyNext),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyNext},
|
||||
},
|
||||
wantFocused: contLocRoot,
|
||||
wantProcessed: 1,
|
||||
},
|
||||
{
|
||||
desc: "keyNext focuses the first container",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusNext(keyNext),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyNext},
|
||||
},
|
||||
wantFocused: contLocLeft,
|
||||
wantProcessed: 1,
|
||||
},
|
||||
{
|
||||
desc: "two keyNext presses focuses the second container",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusNext(keyNext),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
},
|
||||
wantFocused: contLocRight,
|
||||
wantProcessed: 2,
|
||||
},
|
||||
{
|
||||
desc: "three keyNext presses focuses the first container again",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusNext(keyNext),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
},
|
||||
wantFocused: contLocLeft,
|
||||
wantProcessed: 3,
|
||||
},
|
||||
{
|
||||
desc: "four keyNext presses focuses the second container again",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusNext(keyNext),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
},
|
||||
wantFocused: contLocRight,
|
||||
wantProcessed: 4,
|
||||
},
|
||||
{
|
||||
desc: "five keyNext presses focuses the first container again",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusNext(keyNext),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
{Key: keyNext},
|
||||
},
|
||||
wantFocused: contLocLeft,
|
||||
wantProcessed: 5,
|
||||
},
|
||||
{
|
||||
desc: "keyPrevious does nothing when only root exists",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
KeyFocusPrevious(keyPrevious),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyPrevious},
|
||||
},
|
||||
wantFocused: contLocRoot,
|
||||
wantProcessed: 1,
|
||||
},
|
||||
{
|
||||
desc: "keyPrevious focuses the last container",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusPrevious(keyPrevious),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyPrevious},
|
||||
},
|
||||
wantFocused: contLocRight,
|
||||
wantProcessed: 1,
|
||||
},
|
||||
{
|
||||
desc: "two keyPrevious presses focuses the first container",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusPrevious(keyPrevious),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
},
|
||||
wantFocused: contLocLeft,
|
||||
wantProcessed: 2,
|
||||
},
|
||||
{
|
||||
desc: "three keyPrevious presses focuses the second container again",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusPrevious(keyPrevious),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
},
|
||||
wantFocused: contLocRight,
|
||||
wantProcessed: 3,
|
||||
},
|
||||
{
|
||||
desc: "four keyPrevious presses focuses the first container again",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusPrevious(keyPrevious),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
},
|
||||
wantFocused: contLocLeft,
|
||||
wantProcessed: 4,
|
||||
},
|
||||
{
|
||||
desc: "five keyPrevious presses focuses the second container again",
|
||||
container: func(ft *faketerm.Terminal) (*Container, error) {
|
||||
return New(
|
||||
ft,
|
||||
SplitVertical(
|
||||
Left(),
|
||||
Right(),
|
||||
),
|
||||
KeyFocusPrevious(keyPrevious),
|
||||
)
|
||||
},
|
||||
events: []*terminalapi.Keyboard{
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
{Key: keyPrevious},
|
||||
},
|
||||
wantFocused: contLocRight,
|
||||
wantProcessed: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
root, err := tc.container(ft)
|
||||
if err != nil {
|
||||
t.Fatalf("tc.container => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
eds := event.NewDistributionSystem()
|
||||
root.Subscribe(eds)
|
||||
for _, ev := range tc.events {
|
||||
eds.Event(ev)
|
||||
}
|
||||
if err := testevent.WaitFor(5*time.Second, func() error {
|
||||
if got, want := eds.Processed(), tc.wantProcessed; got != want {
|
||||
return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("testevent.WaitFor => %v", err)
|
||||
}
|
||||
|
||||
var wantFocused *Container
|
||||
switch wf := tc.wantFocused; wf {
|
||||
case contLocRoot:
|
||||
wantFocused = root
|
||||
case contLocLeft:
|
||||
wantFocused = root.first
|
||||
case contLocRight:
|
||||
wantFocused = root.second
|
||||
default:
|
||||
t.Fatalf("unsupported wantFocused value => %v", wf)
|
||||
}
|
||||
|
||||
if !root.focusTracker.isActive(wantFocused) {
|
||||
t.Errorf("isActive(%v) => false, want true, status: root(%v):%v, left(%v):%v, right(%v):%v",
|
||||
tc.wantFocused,
|
||||
contLocRoot, root.focusTracker.isActive(root),
|
||||
contLocLeft, root.focusTracker.isActive(root.first),
|
||||
contLocRight, root.focusTracker.isActive(root.second),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/keyboard"
|
||||
"github.com/mum4k/termdash/linestyle"
|
||||
"github.com/mum4k/termdash/private/area"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
@ -95,7 +96,15 @@ type options struct {
|
||||
// id is the identifier provided by the user.
|
||||
id string
|
||||
|
||||
// global are options that apply globally to all containers in the tree.
|
||||
// There is only one instance of these options in the entire tree, if any
|
||||
// of the child containers change their values, the new values apply to the
|
||||
// entire container tree.
|
||||
global *globalOptions
|
||||
|
||||
// inherited are options that are inherited by child containers.
|
||||
// After inheriting these options, the child container can set them to
|
||||
// different values.
|
||||
inherited inherited
|
||||
|
||||
// split identifies how is this container split.
|
||||
@ -181,11 +190,23 @@ type inherited struct {
|
||||
focusedColor cell.Color
|
||||
}
|
||||
|
||||
// globalOptions are options that can only have a single value across the
|
||||
// entire tree of containers.
|
||||
// Regardless of which container they get set on, the new value will take
|
||||
// effect on all the containers in the tree.
|
||||
type globalOptions struct {
|
||||
// keyFocusNext when set is the key that moves the focus to the next container.
|
||||
keyFocusNext *keyboard.Key
|
||||
// keyFocusPrevious when set is the key that moves the focus to the previous container.
|
||||
keyFocusPrevious *keyboard.Key
|
||||
}
|
||||
|
||||
// newOptions returns a new options instance with the default values.
|
||||
// Parent are the inherited options from the parent container or nil if these
|
||||
// options are for a container with no parent (the root).
|
||||
func newOptions(parent *options) *options {
|
||||
opts := &options{
|
||||
global: &globalOptions{},
|
||||
inherited: inherited{
|
||||
focusedColor: cell.ColorYellow,
|
||||
},
|
||||
@ -195,6 +216,7 @@ func newOptions(parent *options) *options {
|
||||
splitFixed: DefaultSplitFixed,
|
||||
}
|
||||
if parent != nil {
|
||||
opts.global = parent.global
|
||||
opts.inherited = parent.inherited
|
||||
}
|
||||
return opts
|
||||
@ -815,3 +837,39 @@ func Bottom(opts ...Option) BottomOption {
|
||||
return opts
|
||||
})
|
||||
}
|
||||
|
||||
// KeyFocusNext configures a key that moves the keyboard focus to the next
|
||||
// container when pressed.
|
||||
//
|
||||
// Containers are organized in a binary tree, when the focus moves to the next
|
||||
// container, it targets the next leaf container in a DFS (Depth-first search) traversal.
|
||||
// Non-leaf containers are skipped. If the currently focused container is the
|
||||
// last container, the focus moves back to the first container.
|
||||
//
|
||||
// This option is global and applies to all created containers.
|
||||
// If neither of (KeyFocusNext, KeyFocusPrevious) is specified, the keyboard
|
||||
// focus can only be changed by using the mouse.
|
||||
func KeyFocusNext(key keyboard.Key) Option {
|
||||
return option(func(c *Container) error {
|
||||
c.opts.global.keyFocusNext = &key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// KeyFocusPrevious configures a key that moves the keyboard focus to the
|
||||
// previous container when pressed.
|
||||
//
|
||||
// Containers are organized in a binary tree, when the focus moves to the previous
|
||||
// container, it targets the previous leaf container in a DFS (Depth-first search) traversal.
|
||||
// Non-leaf containers are skipped. If the currently focused container is the
|
||||
// first container, the focus moves back to the last container.
|
||||
//
|
||||
// This option is global and applies to all created containers.
|
||||
// If neither of (KeyFocusNext, KeyFocusPrevious) is specified, the keyboard
|
||||
// focus can only be changed by using the mouse.
|
||||
func KeyFocusPrevious(key keyboard.Key) Option {
|
||||
return option(func(c *Container) error {
|
||||
c.opts.global.keyFocusPrevious = &key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -100,6 +100,9 @@ func Diff(want, got *Terminal) string {
|
||||
for col := 0; col < size.X; col++ {
|
||||
got := got.BackBuffer()[col][row].Rune
|
||||
want := want.BackBuffer()[col][row].Rune
|
||||
if got == want {
|
||||
continue
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" cell(%v, %v) => got '%c' (rune %d), want '%c' (rune %d)\n", col, row, got, got, want, want))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user