mirror of https://github.com/mum4k/termdash.git
Ability to move focus to the next container using a key.
This commit is contained in:
parent
fbd21e7904
commit
0771a92dc2
|
@ -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,15 @@ 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) {
|
||||
if c.opts.global.keyFocusNext != nil && *c.opts.global.keyFocusNext == k.Key {
|
||||
c.focusTracker.next()
|
||||
}
|
||||
}
|
||||
|
||||
// processEvent processes events delivered to the container.
|
||||
func (c *Container) processEvent(ev terminalapi.Event) error {
|
||||
// This is done in two stages.
|
||||
|
@ -293,7 +308,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 {
|
||||
|
@ -309,6 +324,8 @@ func (c *Container) prepareEvTargets(ev terminalapi.Event) (func() error, error)
|
|||
}, nil
|
||||
|
||||
case *terminalapi.Keyboard:
|
||||
c.updateFocusFromKeyboard(ev.(*terminalapi.Keyboard))
|
||||
|
||||
targets := c.keyEvTargets()
|
||||
return func() error {
|
||||
for _, w := range targets {
|
||||
|
|
|
@ -78,6 +78,49 @@ func (ft *focusTracker) setActive(c *Container) {
|
|||
ft.container = c
|
||||
}
|
||||
|
||||
// next moves focus to the next container.
|
||||
func (ft *focusTracker) next() {
|
||||
var (
|
||||
errStr string
|
||||
first *Container
|
||||
cont *Container
|
||||
focusNext bool
|
||||
)
|
||||
postOrder(rootCont(ft.container), &errStr, visitFunc(func(c *Container) error {
|
||||
if cont != nil {
|
||||
// Already found the next container, nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.isLeaf() && first == nil {
|
||||
// Remember the first eligible container in case we "wrap" over,
|
||||
// i.e. finish the iteration before finding the next container.
|
||||
first = c
|
||||
}
|
||||
|
||||
if ft.container == c {
|
||||
// Visiting the currently focused container, going to focus the
|
||||
// next one.
|
||||
focusNext = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.isLeaf() && focusNext {
|
||||
cont = c
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
if cont == nil && first != nil {
|
||||
// If the traversal finishes without finding the next container, move
|
||||
// focus back to the first container.
|
||||
cont = first
|
||||
}
|
||||
if cont != nil {
|
||||
ft.setActive(cont)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,219 @@ 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,
|
||||
},
|
||||
}
|
||||
|
||||
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,38 @@ 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 traversal that
|
||||
// contains a widget. Non-leaf containers and containers without widgets 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 not specified, keyboard the focused container 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 traversal that
|
||||
// contains a widget. Non-leaf containers and containers without widgets 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.
|
||||
func KeyFocusPrevious(key keyboard.Key) Option {
|
||||
return option(func(c *Container) error {
|
||||
c.opts.global.keyFocusPrevious = &key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue