Documentation
¶
Overview ¶
Package focus is a small focus-traversal helper: a Ring tracks which of n items has focus and moves it with Tab and Shift+Tab, wrapping at the ends and skipping disabled items. It replaces the modulo arithmetic every multi-field screen otherwise hand-rolls (see examples/focus).
Route and Bind go one step further: they blur the old widget, focus the new one and send every other message to the focused widget only, so a screen needs no per-widget switch (see examples/focus).
Tab follows index order unless the Ring is given another with WithOrder. LayoutOrder derives one from a layout.Node, reading order of where each named field is drawn, and Zones gives RouteAuto the matching click regions, so neither tab order nor mouse focus is maintained by hand.
Ring is a value type like the widget Models: every method returns the new Ring and leaves the receiver alone, so copies held elsewhere are never changed behind your back.
Example ¶
A form with three fields: Tab cycles forward, Shift+Tab back.
ring := New(3)
for _, k := range []tui.Key{tab(), tab(), tab(), shiftTab()} {
ring, _ = ring.Update(k)
fmt.Print(ring.Current(), " ")
}
Output: 1 2 0 2
Index ¶
- func LayoutOrder(root layout.Node, s layout.Size, names ...string) []int
- func Zones(root layout.Node, s layout.Size, names ...string) hittest.Map[int]
- type Field
- type Focusable
- type Ring
- func (r Ring) Current() int
- func (r Ring) Depth() int
- func (r Ring) Enabled(i int) bool
- func (r Ring) Focused(i int) bool
- func (r Ring) Len() int
- func (r Ring) Next() Ring
- func (r Ring) Order() []int
- func (r Ring) Pop() Ring
- func (r Ring) Prev() Ring
- func (r Ring) Push(n int) Ring
- func (r Ring) Route(msg tui.Msg, fields ...Field) (Ring, tui.Cmd)
- func (r Ring) RouteAuto(msg tui.Msg, zones hittest.Map[int], fields ...Field) (Ring, tui.Cmd)
- func (r Ring) Set(i int) Ring
- func (r Ring) SetDisabled(i int, disabled bool) Ring
- func (r Ring) Sync(fields ...Field) tui.Cmd
- func (r Ring) Update(msg tui.Msg) (Ring, bool)
- func (r Ring) WithOrder(order []int) Ring
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LayoutOrder ¶
LayoutOrder returns the indices of names (item i is names[i]) in the order a reader meets them on screen: by where root, laid out at size s, draws the node labelled with that name (see layout.Named), top to bottom and then left to right. Items whose name is not drawn (absent, or clipped to nothing) follow the drawn ones in index order, so every index appears once. Pass the result to WithOrder:
ring = ring.WithOrder(focus.LayoutOrder(root, size, "name", "email", "submit"))
Recompute it when the layout changes shape (a resize past a layout.Responsive breakpoint, say); focus itself is kept by WithOrder.
Example ¶
Tab order from the layout: fields are numbered in the order the model keeps them, and Tab visits them in the order they appear on screen.
package main
import (
"fmt"
"github.com/ows4444/tui/focus"
"github.com/ows4444/tui/layout"
)
func main() {
cell := func(name string) layout.FlexChild {
return layout.FlexChild{Node: layout.Named(name, layout.Block(name)), Basis: 1}
}
form := layout.Row(2,
layout.FlexChild{Node: layout.Column(0, cell("name"), cell("email")), Basis: 8},
layout.FlexChild{Node: layout.Column(0, cell("city"), cell("submit")), Basis: 8},
)
names := []string{"name", "email", "city", "submit"}
size := layout.Size{W: 20, H: 2}
ring := focus.New(len(names)).WithOrder(focus.LayoutOrder(form, size, names...))
for range names {
fmt.Print(names[ring.Current()], " ")
ring = ring.Next()
}
fmt.Println()
}
Output: name city email submit
func Zones ¶
Zones returns the click regions of names, laid out as LayoutOrder does, with each region's ID the item's index: the Map RouteAuto takes, so a left click focuses the item under it. Where named nodes overlap the later one in drawing order wins, as in hittest.HitMap. Names not drawn have no region.
Types ¶
type Field ¶
type Field struct {
Focus func() tui.Cmd
Blur func()
Update func(tui.Msg) tui.Cmd
// Consumes reports whether the item wants msg for itself, so RouteAuto
// gives it the message instead of moving focus (a Tab that indents, a
// completion popup). Nil means the item never consumes anything.
Consumes func(tui.Msg) bool
}
Field is one ring item as the Ring drives it: how to focus it, blur it, and hand it a message. Build one with Bind, or fill the funcs yourself for a widget that does not follow the Model conventions. Nil funcs are skipped.
func Bind ¶
Bind returns the Field for the widget p points to, typically a field of your model:
fields := []focus.Field{focus.Bind(&m.name), focus.Bind(&m.bio)}
Focus and Blur are p's methods, and Update runs p's Update and stores the new Model back through p, returning its Cmd. Bind the fields of the model value you are about to return, inside Update (models are values, so the pointer must point into the copy you keep).
type Focusable ¶
Focusable is what a widget offers so a Ring can move focus onto and off it: Focus gives it focus (returning any Cmd it needs, such as a cursor blink) and Blur takes focus away. textinput, passwordinput, textarea and the other input widgets satisfy it through a pointer to their Model.
type Ring ¶
type Ring struct {
// contains filtered or unexported fields
}
Ring is a set of n focusable items, numbered 0 to n-1, with one focused. The zero Ring has no items.
func (Ring) Depth ¶
Depth is the number of open scopes pushed over the root: 0 for a Ring that never had Push called.
func (Ring) Next ¶
Next moves focus to the next enabled item, wrapping from the last to the first. With no enabled item other than the current one, or none at all, focus stays where it is.
func (Ring) Order ¶
Order returns the traversal order Next follows: the order given to WithOrder, completed and cleaned, or 0..Len()-1 when none was set. The slice is a copy.
func (Ring) Pop ¶
Pop closes the innermost scope and returns the Ring as it was when Push was called, including which item had focus and which were disabled, so focus goes back to the widget that had it before the scope opened. Call Sync (or focus the item yourself) to tell the widget. Pop on a Ring with no open scope returns it unchanged.
func (Ring) Prev ¶
Prev moves focus to the previous enabled item, wrapping from the first to the last.
func (Ring) Push ¶
Push opens a new focus scope of n items over the receiver, with item 0 focused, and returns it. While the scope is open Next, Prev, Set, Update and Route act on its items only, so Tab cannot reach the items of the scopes beneath it; this is how a modal traps focus. n <= 0 gives an empty scope. Pop returns to the scope the Push was made from, exactly as it was.
func (Ring) Route ¶
Route is the whole focus dispatch for a screen of several widgets. On Tab and Shift+Tab it moves focus to the next or previous enabled item, calls Blur on the item that had it and Focus on the item that gets it, and returns the Focus Cmd. Every other message goes to the focused item's Update only and its Cmd is returned; a message the focused item does not want costs nothing. Route never touches a disabled item or an index outside fields.
ring, cmd := m.ring.Route(msg, fields...) m.ring = ring return m, cmd
fields must have one entry per ring item, in order. Messages that every widget needs, such as cursor blink ticks for an unfocused field, are not broadcast: forward those yourself.
func (Ring) RouteAuto ¶
RouteAuto is Route with the two rules a screen otherwise hand-writes:
- Tab and Shift+Tab move focus (wrapping, skipping disabled items) unless the focused item's Consumes reports it wants the key, in which case the key goes to that item's Update and focus stays.
- A left-button press on a zones region focuses the item whose index is the region's ID (blurring the old one), then delivers the press to it. A press outside every region, or on a disabled or unknown item, goes to the focused item as any other message would.
Every other message goes to the focused item only, as in Route. Pass a zero hittest.Map to turn mouse focusing off. Programs that do not call RouteAuto are unaffected.
func (Ring) Set ¶
Set moves focus to item i. An index out of range, or a disabled item, leaves focus unchanged.
func (Ring) SetDisabled ¶
SetDisabled marks item i disabled (skipped by Next and Prev) or enabled again. Disabling the focused item moves focus to the next enabled one, if there is one. An out-of-range i is ignored.
func (Ring) Sync ¶
Sync makes the fields agree with the Ring: it blurs every item but the focused one and focuses that one, returning its Focus Cmd. Call it once at start-up (from the initial model, returning the Cmd from Init) and after SetDisabled or Set changes which item has focus.
func (Ring) Update ¶
Update moves focus on Tab (next) and Shift+Tab (previous) and reports whether msg was one of those keys, so a caller can stop handling it:
if ring, moved := m.ring.Update(msg); moved {
m.ring = ring
return m, nil
}
Shift+Tab arrives as a Tab key with the Shift modifier, which the input reader decodes from both ESC [ Z and the kitty keyboard protocol.
func (Ring) WithOrder ¶
WithOrder returns r with Next and Prev (and so Tab, Shift+Tab, Update, Route and RouteAuto) moving along order instead of index order: order[0] is first, and the last wraps to it. Indices out of range and repeats are ignored, and items order leaves out are visited after the listed ones in index order, so every item stays reachable. A nil or empty order restores index order. Which item has focus, and which are disabled, do not change. LayoutOrder computes an order from a layout. A scope opened with Push starts in index order; Pop brings back the order of the scope beneath.