Navigation & Buffers - tui-pages

## 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

# Navigation, Buffers & Panes

Same rule as focus: you describe intent with a `TuiEffect`, the runtime owns the state. There is no `BufferState` API you call into — you return effects, and the runtime mutates `tui.buffer` for you. You only ever read `tui.buffer`, and only to render it.

There are two layers:

- Buffer history — the list of open views you cycle through (like editor tabs or buffers).
- Panes — how the visible area is split into regions.

## Driving it: effects

Every navigation action in your handler maps to one effect. From the `buffers` example:

    #![allow(unused)]
    fn main() {
    let effect = match action {
        Action::Open(view)    => TuiEffect::Navigate(view),   // open / switch to a view
        Action::NextBuffer    => TuiEffect::NextBuffer,        // cycle history forward
        Action::PrevBuffer    => TuiEffect::PreviousBuffer,    // cycle history back
        Action::CloseBuffer   => TuiEffect::CloseBuffer,       // drop the active buffer
        Action::Split(split)  => TuiEffect::SplitPane(split),  // split the active pane
        Action::NextPane      => TuiEffect::NextPane,          // move pane focus
        Action::PrevPane      => TuiEffect::PreviousPane,
        Action::ClosePane     => TuiEffect::ClosePane,
        Action::Quit          => TuiEffect::Quit,
    };
    Ok(ActionOutcome::effect(effect))
    }

That's the whole handler for a multi-buffer, multi-pane app. Bind keys to those actions and you're done:

    #![allow(unused)]
    fn main() {
    TuiPages::builder(View::Editor)
        .focus_wrap(FocusWrap::Wrap)   // NextBuffer past the end wraps to the first
        .bind(modes::GENERAL, "tab", Action::NextBuffer)
        .bind(modes::GENERAL, "w",   Action::CloseBuffer)
        .bind(modes::GENERAL, "v",   Action::Split(PaneSplit::Vertical))
        .bind(modes::GENERAL, "s",   Action::Split(PaneSplit::Horizontal))
        .bind(modes::GENERAL, "o",   Action::NextPane)
        .bind(modes::GENERAL, "x",   Action::ClosePane)
        // ...
    }

`PaneSplit` is just:

    #![allow(unused)]
    fn main() {
    pub enum PaneSplit {
        Horizontal,  // stacked top/bottom
        Vertical,    // side by side
    }
    }

`TuiEffect::Navigate(view)` and buffer/pane cycling honour your `FocusWrap` policy. Buffer history is per-view: when you navigate back to a buffer, its focus is restored. After a navigation effect the runtime re-runs your page function, so the new view's `PageSpec` takes effect automatically.

## Reading it: rendering

Your renderer reads `tui.buffer` (a `BufferState<V>`) to lay out the screen. These are the read accessors:

    #![allow(unused)]
    fn main() {
    tui.buffer.get_active_view()    // Option<&V> — the focused buffer's view
    tui.buffer.is_split()           // bool — is the area split into panes?
    tui.buffer.split_direction()    // Option<PaneSplit>
    tui.buffer.panes()              // &[PaneSession<V>] — one per pane
    tui.buffer.active_pane_index()  // usize — which pane has focus
    }

A render function that handles both the single and split cases (from the `buffers` example) looks like:

    #![allow(unused)]
    fn main() {
    pub fn render(frame: &mut Frame, buffer: &BufferState<View>) {
        if buffer.is_split() {
            let dir = match buffer.split_direction() {
                Some(PaneSplit::Vertical) => Direction::Horizontal,
                _                         => Direction::Vertical,
            };
            let areas = Layout::default().direction(dir)
                .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
                .split(frame.area());

            for (i, pane) in buffer.panes().iter().enumerate() {
                let active = i == buffer.active_pane_index();
                draw_pane(frame, areas[i], pane, active);
            }
        } else {
            draw_single(frame, frame.area(), buffer.get_active_view());
        }
    }
    }

The shortcut `tui.current_view()` returns the active buffer's view directly — handy when you don't care about panes.

## When do you need this?

Only if your app has tabs/buffers or split panes. A single-page app ignores all of it: there's always exactly one buffer and one pane, and you just render `tui.current_view()`. See the `buffers` example for the full thing.