Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Focus

Focus is "which element is selected right now". The runtime owns it. You do three things:

  1. Declare what's focusable, in each page's PageSpec.
  2. Move focus by returning TuiEffect::Focus(intent) from your handler.
  3. 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)),
}
FocusIntentWhat it does
Next / Prevstep 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
LeaveSectionstep 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
ClearOverlayclose 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().