# 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:
```rust
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:
```rust
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:
```rust
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:
```rust
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:
```rust
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.