-
Notifications
You must be signed in to change notification settings - Fork 0
Mouse and focus
Keyboard and mouse input meet in the same element tree. Focus determines where key events begin; rendered rectangles determine where mouse events land. User handlers run before built-in widget behavior, and an event can bubble toward its parent when it is not consumed.
On each render glyphora walks the current Element tree depth-first and records
focusable elements. Inputs, buttons, toggles, selects, lists, text areas, and other
interactive elements opt in automatically.
-
Tabmoves to the next focusable element; -
Shift+Tabmoves to the previous one; - clicking an interactive element focuses it first;
- the active theme's
focusstyle decorates the focused element; - opening a modal removes everything below it from the tab order.
Make any custom element focusable with .focusable:
panel("Custom control")(text("press Enter"))
.focusable
.onKey(Key.Enter) { activate() }Without an explicit key, focus is positional. Conditional content inserted before a control can make the same numeric position refer to a different element. Assign a stable identity when tree shape changes:
column(
input(search, placeholder = "search").key("search"),
if showAdvanced.get then input(pattern).key("advanced-pattern")
else spacer(1),
button("Apply") { apply() }.key("apply"),
)During the next focus pass glyphora finds the same key in the new tree and moves the focus index to it.
.onKey consumes an event only when one of its keys matches and composes with other
.onKey calls:
panel("Counter")(text(count.get.toString))
.onKey(Key.char('+'), Key.Up) { count.update(_ + 1) }
.onKey(Key.char('-'), Key.Down) { count.update(_ - 1) }
.onKey(Key.Home) { count.set(0) }The Key vocabulary includes arrows, Enter, Escape, Tab, paging and editing
keys, f(1) through f(12), ctrl, alt, shift, and common constants such as
CtrlS, CtrlP, and CtrlQ.
Drop to .onKeyEvent when you need the event's modifiers or want to decline it:
text("q quits only from this mode").onKeyEvent {
case KeyEvent(KeyCode.Char('q'), _) if canQuit.peek =>
quit()
true
case _ =>
false // bubble to the parent, then app bindings
}For the focused path, routing order is:
- focused element's user handler;
- focused element's built-in behavior;
- each ancestor's user handler, inner to outer;
- the app's
KeyBindings; - built-ins such as
Tab,Ctrl+P, and unconsumedCtrl+C.
This lets an input consume normal text and editing keys while a parent still handles
Escape and the app still handles global commands.
MouseEvent carries absolute terminal coordinates, a MouseEventKind, and keyboard
modifiers:
panel("Canvas")(canvasView).onMouseEvent {
case MouseEvent(x, y, MouseEventKind.Down, _) =>
selectedCell.set((x, y))
true
case MouseEvent(_, _, MouseEventKind.ScrollDown, _) =>
zoom.update(value => math.max(1, value - 1))
true
case _ =>
false
}Kinds are Down, Up, Drag, Moved, ScrollUp, and ScrollDown. Coordinates
are absolute screen cells; custom widgets should compare them with the element's
known model or use built-in interactive elements when possible.
You do not need custom handlers for common interactions:
| Element | Mouse behavior |
|---|---|
| input, list, select, controls | click focuses; click may position/select/activate |
| button, checkbox, toggle | click activates |
| scroll view and scrollable lists | wheel changes offset |
| slider | click or drag positions the value |
| split pane | drag moves the divider |
A user .onMouseEvent runs first. Return false when the widget's built-in behavior
should still run.
TuiApp and JLine3Backend negotiate mouse capture for you. A custom runner owns
that backend lifecycle itself. Input decoding belongs in the backend—application
code should never parse escape sequences.
Headless tests can click exact cells and post any mouse kind. See Testing for examples, and Unicode & accessibility for keyboard-equivalent and focus guidance.
Documentation is maintained in website/docs. Read the styled guide · API reference · MIT license