Input and focus
Mouse input goes to whatever is under the pointer. Keyboard input goes to the widget that has focus. willowui tracks focus for you, moves it with Tab and the arrow keys, and runs your key binds. All of that lives in one object, the FocusManager.
The focus manager
Every Screen has a focusManager, which is DEFAULT_FOCUS_MANAGER unless you set another. Screen.update (called by DEFAULT_STAGE.update) runs it once per frame, so there is nothing to wire up.
| Member | Does |
|---|---|
focused | The widget with focus, or null. |
setFocus(c) | Gives focus to c. setFocus(null) or clearFocus() removes it. |
tabNext(), tabPrev() | What Tab and Shift+Tab do. |
tabOrder | The widgets that can take focus, in Tab order. |
bind(combo, fn) | A global key bind. Returns a handle. |
bindScoped(comp, combo, fn) | A key bind active only while comp, or something inside it, has focus. |
unbind(handle) | Removes a global bind. |
Clicking a widget focuses it. Clicking empty space clears focus.
Which widgets take focus
Interactive widgets (buttons, inputs, sliders, lists, and so on) call enableFocusNavigation() when they are made. That marks them focusable and adds them to the end of tabOrder. So Tab order is creation order. If you want a different order, create the widgets in that order.
A widget is skipped while it is disabled, hidden, or inside a disabled or hidden parent. Two flags take it out of one kind of navigation only:
| Flag | When false |
|---|---|
allowTab | Tab and Shift+Tab skip it. |
allowSpatial | The arrow keys skip it. |
Disposing a widget removes it from the tab order and drops its scoped binds.
This console program shows the rules without opening a window:
ts
import { Button, Component, DEFAULT_FOCUS_MANAGER, FocusManager } from "willowui";
const fm: FocusManager = DEFAULT_FOCUS_MANAGER;
const play: Button = new Button("play", "Play", "default", 16.0);
const options: Button = new Button("options", "Options", "default", 16.0);
const quit: Button = new Button("quit", "Quit", "default", 16.0);
options.setEnabled(false);
function focusedName(): string {
const f: Component | null = fm.focused;
return f == null ? "nothing" : (f as Component).name;
}
console.log("in tab order: " + fm.tabOrder.length + ", first is " + play.name);
fm.tabNext();
console.log("Tab: " + focusedName());
fm.tabNext();
console.log("Tab: " + focusedName());
fm.tabPrev();
console.log("Shift+Tab: " + focusedName());
quit.allowTab = false;
fm.tabNext();
console.log("Tab: " + focusedName());Output:
text
in tab order: 3, first is play
Tab: play
Tab: quit
Shift+Tab: play
Tab: playOptions is disabled, so Tab goes from Play straight to Quit. Once Quit has allowTab = false, Play is the only stop left.
Arrow keys: spatial navigation
When no widget claims an arrow key, the focus manager moves focus in that direction:
- Inside a VBox or HBox, Up and Down (in a VBox) or Left and Right (in an HBox) go to the next eligible sibling. A menu laid out as a VBox works with no extra code.
- Otherwise, it picks the nearest focusable widget on screen in that direction, scoring the gap between the two widgets plus how far off-line the other one is.
Some widgets use the arrows themselves: a slider changes its value, a list moves its selection. Those widgets set interceptArrows, and they get the key first.
Key binds
A key bind runs a function when a key combination is pressed. Make the combination with key(k, mods): k is a Key constant and mods is a mask of willow's KeyModifiers (CTRL, SHIFT, ALT), or MOD_NONE. The modifiers must match exactly. CTRL also matches the Command key.
The bind function returns a boolean. Return true to consume the key, so nothing later in the frame sees it.
ts
import { Button, DEFAULT_FOCUS_MANAGER, DEFAULT_STAGE, Label, MOD_NONE, Panel, Screen, key } from "willowui";
import { KeyModifiers } from "willow";
const screen: Screen = new Screen();
const status: Label = new Label("status", "Ctrl+S saves. F1 works inside the panel.", "default", 14.0);
status.setPosition(20.0, 20.0);
screen.add(status);
const panel: Panel = new Panel("tools");
panel.setPosition(20.0, 60.0);
panel.setSize(220.0, 80.0);
screen.add(panel);
const help: Button = new Button("help", "Help", "default", 14.0);
help.setPosition(12.0, 12.0);
panel.addChild(help);
// Global: works whatever has focus.
const saveBind: int = DEFAULT_FOCUS_MANAGER.bind(key(Key.S, KeyModifiers.CTRL), (): boolean => {
status.setText("Saved");
return true;
});
// Scoped: only while the panel or a widget inside it has focus.
DEFAULT_FOCUS_MANAGER.bindScoped(panel, key(Key.F1, MOD_NONE), (): boolean => {
status.setText("Help for the tools panel");
return true;
});
// DEFAULT_FOCUS_MANAGER.unbind(saveBind) removes the global bind again.
DEFAULT_STAGE.setScene(engine.root2d);
DEFAULT_STAGE.add(screen);
App.onFrame((dt: float): void => { DEFAULT_STAGE.update(dt); });The order each frame
Each frame the focus manager handles keys in this order. Once a key is handled, later steps skip it.
- A click outside every widget clears focus.
- The focused widget takes the arrow keys it wants (
interceptArrows). - Scoped binds whose widget is focused.
- Global binds.
- Tab and Shift+Tab.
- Spatial navigation for the arrow keys nobody took.
So a scoped bind beats a global bind on the same key, and a global bind on an arrow key beats spatial navigation.
UI keys and game keys
Game input, such as moving a character, reads the global input classes directly: Keyboard.isDown(Key.RIGHT), Gamepad, Mouse. Those are ThicketScript built-ins, not part of willowui. Use focus and key binds for UI, and the input classes for play.
To let players rebind controls, use the KeybindInput widget. The settings screen recipe shows one.
See also
- Recipe: a main menu uses spatial navigation in a VBox.
- Widgets lists which widgets take focus.