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.