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

Canvas

Enable first-class canvas support with one feature:

tui-pages = { version = "...", features = ["canvas"] }

The canvas feature intentionally enables the full canvas surface at once: form editor, ratatui rendering, suggestions, validation, computed fields, cursor style helpers, textarea, textinput, keymap handling, and crossterm input session helpers.

App Action Shape

Use a wrapper action and implement From<CanvasAction>:

#![allow(unused)]
fn main() {
use tui_pages::canvas::CanvasAction;

enum Action {
    Canvas(CanvasAction),
    Quit,
}

impl From<CanvasAction> for Action {
    fn from(action: CanvasAction) -> Self {
        Action::Canvas(action)
    }
}
}

Then install standard bindings and typed text routing:

#![allow(unused)]
fn main() {
let app = TuiPages::builder(View::Editor)
    .page_fn(page_spec)
    .handler(Handler)
    .canvas_defaults()
    .build();
}

PageSpec::canvas_editor(&editor) or PageSpec::canvas_mode(mode) keeps the active input mode stack aligned with the focused canvas widget.

For custom wiring, .canvas_keymaps() installs only the standard NORMAL, INSERT, and SELECT keymaps, while .canvas_text_actions() installs only the typed-character mapper that turns plain text into CanvasAction::InsertChar. For lower-level standalone textarea and textinput wiring, route raw key events through the helpers below.

If you want tui-pages to own the canvas key dispatch directly, implement CanvasWidgetState for your app state and use the widget builders:

#![allow(unused)]
fn main() {
impl tui_pages::canvas::CanvasWidgetState for State {
    fn canvas_form_editor(
        &mut self,
        id: usize,
    ) -> Option<&mut dyn tui_pages::canvas::CanvasFormEditorHost> {
        match id {
            0 => Some(&mut self.editor),
            _ => None,
        }
    }

    fn canvas_textarea(
        &mut self,
        focus_index: usize,
    ) -> Option<&mut dyn tui_pages::canvas::CanvasTextAreaHost> {
        match focus_index {
            0 => Some(&mut self.body),
            _ => None,
        }
    }

    fn canvas_textarea_entered(&mut self, focus_index: usize) -> Option<&mut bool> {
        match focus_index {
            0 => Some(&mut self.body_entered),
            _ => None,
        }
    }

    fn canvas_textinput_suggestion_suffix(
        &mut self,
        focus_index: usize,
        text: &str,
    ) -> Option<String> {
        match (focus_index, text) {
            (1, "adm") => Some("in".to_string()),
            _ => None,
        }
    }
}

let app = TuiPages::builder(View::Editor)
    .page_fn(page_spec)
    .handler(Handler)
    .canvas_form_editor(0)
    .canvas_textarea_widget(0)
    .build();
}

canvas_form_editor dispatches canvas actions into the FormEditor and returns focus effects at editor boundaries, so your application action enum does not need a Canvas(CanvasAction) variant. The textarea and textinput builders treat their focused widget as one top-level stop until Enter sets the supplied entered flag. Once entered, NORMAL-mode keys drive the widget's modal editor, INSERT-mode keys go directly to the text widget, Esc exits edit mode and then the widget, and boundary/submit keys hand focus back to tui-pages. For textinput widgets, canvas_textinput_suggestion_suffix is optional; when it returns Some(suffix), the widget shows that suffix inline and accepts it with Tab.

FormEditor Handoff

The app owns editor state. In the action handler, dispatch canvas actions and return focus effects when the editor reaches a boundary:

#![allow(unused)]
fn main() {
match tui_pages::dispatch_canvas_action(&mut state.editor, action) {
    CanvasDispatchOutcome::Focus(intent) => {
        ActionOutcome::effect(TuiEffect::Focus(intent))
    }
    CanvasDispatchOutcome::Applied(result) => {
        // handle messages, validation errors, persistence flags, recompute, etc.
        ActionOutcome::none()
    }
}
}

ExitCanvasForward and ExitCanvasBackward are handled by the normal FocusManager, so fields can sit beside buttons, sections, modals, buffers, and panes without custom focus code.

Textarea And Textinput

For top-level textarea and textinput widgets, route raw key events through the typed helpers:

#![allow(unused)]
fn main() {
let outcome = tui_pages::dispatch_canvas_text_area_key(&mut state.notes, key);
let outcome = tui_pages::dispatch_canvas_text_input_key(&mut state.command, key);
}

These return Handled, Submitted, NotHandled, or a focus intent. Tab and vertical boundary keys map to the same forward/backward canvas exits used by FormEditor.

Rendering

Canvas renderers are re-exported from tui_pages::canvas. The common form-editor path is:

#![allow(unused)]
fn main() {
tui_pages::render_canvas_with_suggestions_default(
    frame,
    frame.area(),
    form_area,
    &state.editor,
);
}

This draws the editor and, when suggestions are active, anchors the dropdown to the active input rectangle returned by the renderer. Textarea and textinput use the re-exported stateful widgets and expose cursor(area, block) helpers; set the ratatui cursor from those values after rendering the focused widget.

Cursor Style

Cursor style is opt-in. Call update_cursor_style_for_editor(&editor) or update_cursor_style_for_mode(mode) from your event/render loop when you want terminal cursor shape updates. tui-pages does not mutate terminal cursor style unless the host calls these helpers.

Example

See examples/canvas/minimal for the smallest form editor setup, examples/canvas/textarea for direct textinput/textarea dispatch, and examples/canvas/full for form editor, suggestions, validation, computed fields, cursor/session helpers, textarea, textinput, and canvas focus handoff together.