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.