Focus
Focus is "which element is selected right now". The runtime owns it. You do three things:
- Declare what's focusable, in each page's
PageSpec. - Move focus by returning
TuiEffect::Focus(intent)from your handler. - Read the current focus when rendering, to highlight it.
You never mutate a focus manager yourself. (There is one — tui.focus — but you
read it, you don't drive it.)
1. Declaring focus targets
A FocusTarget is an element that can hold focus. You list them with
PageFocusBuilder:
#![allow(unused)] fn main() { PageFocusBuilder::new() .button(0) .button(1) .section_with_items(NOTES_SECTION, notes.len()) }
The target variants:
#![allow(unused)] fn main() { pub enum FocusTarget<O = ()> { Button(usize), // a button, by index Section(usize), // a collapsible group / list header SectionItem { section: usize, item: usize }, // an item inside a section CanvasField(usize), // a text/edit field InternalCanvasField(usize), // a field skipped by Tab order Overlay(O), // one of your overlays (O = your type) ModalItem(usize), // a button inside a modal Custom(String), // an escape hatch } }
Button/Section/SectionItem/CanvasField cover almost everything. Overlay
carries your overlay type — see the full example's command bar.
2. Moving focus
Return a FocusIntent wrapped in a focus effect. The ones you'll actually use:
#![allow(unused)] fn main() { // from your handler: Action::FocusNext => ActionOutcome::effect(TuiEffect::Focus(FocusIntent::Next)), Action::FocusPrev => ActionOutcome::effect(TuiEffect::Focus(FocusIntent::Prev)), }
FocusIntent | What it does |
|---|---|
Next / Prev | step through targets (and through a section's items when you're inside one) |
Activate | "enter" the focused target — steps into a section, presses a button |
LeaveSection | step back out of a section (no-op if you're not in one) |
Set(target) | jump focus to a specific target |
Open(target) | open an overlay target |
ClearOverlay | close whatever overlay is open |
The key thing about Next/Prev: they're smart about sections. When focus is on
a section and you Activate, the runtime steps into its items; Next/Prev
then move between items, and stepping off the end moves back out to the next
top-level target. You bind the same FocusNext action to both Tab and j and
it just works — your handler never inspects focus to decide what a movement key
should do.
Sections, end to end
This is the pattern that makes the section machinery pay off. Declare a section with its item count:
#![allow(unused)] fn main() { fn page_spec(view: &View, _state: &State, _focus: Option<&FocusTarget>) -> PageSpec { let mut focus = PageFocusBuilder::new(); match view { View::Notes => focus = focus.section_with_items(NOTES_SECTION, NOTES.len()).button(0), _ => {} } PageSpec::new().focus(focus).modes(vec![modes::GENERAL, modes::GLOBAL]) } }
Because the item count travels with the section (via .focus(builder)), the
runtime can enter and walk the list itself. Your handler only describes
intent — move, activate, leave — plus the genuinely app-specific bits:
#![allow(unused)] fn main() { fn select(ctx: ActionContext<View>, state: &mut State) -> ActionOutcome<View> { match (ctx.current_view, ctx.focus) { // Enter on a real item: do the app thing. (View::Notes, Some(FocusTarget::SectionItem { section: NOTES_SECTION, item })) => { state.selected_note = Some(item); ActionOutcome::effect(TuiEffect::RefreshPage) } // Enter on anything else (e.g. the section header) → let the runtime // activate it: it steps into the section using the declared item count. _ => ActionOutcome::effect(TuiEffect::Focus(FocusIntent::Activate)), } } }
Bind esc to FocusIntent::LeaveSection and you have full list navigation
without a single focus inspection in your movement code. (This is the full
example.)
3. Reading focus when rendering
tui.focus.current() returns the focused target (owned Option<FocusTarget<O>>),
which is exactly what your renderer needs to highlight the right thing:
#![allow(unused)] fn main() { pub fn render(frame: &mut Frame, view: View, focus: Option<FocusTarget>) { let focused = matches!(focus, Some(FocusTarget::Button(i)) if i == 0); // ...draw the button highlighted when `focused`. } }
Other read-only queries on tui.focus:
#![allow(unused)] fn main() { tui.focus.current() // Option<FocusTarget<O>> — the focused target tui.focus.has_overlay() // bool — is an overlay/modal open? tui.focus.is_focused(&t) // bool — is this exact target focused? }
Wrap behaviour
FocusWrap (set on the builder) decides what happens at the ends of a list:
Clamp stops, Wrap cycles. It applies to page focus, section items, buffers,
and panes alike. You can read the current policy with tui.focus.focus_wrap().