Pull the view interface, actions and modes out of the canvas example

The canvas example did everything itself; now it runs the way the app
will. The key engine turns keys into commands, the focused view answers
with actions, and the commands every view shares (undo, marks, adding,
deleting, folds, tree moves, modes) answer the rest. A view can still
give a key its own meaning, since it gets the first say. `:` lines become
actions the same way (app::calls).

- View trait: kind, contexts (its key tables), layout, render, command
  and mouse. layout comes before render because a view keeps the frame
  it drew, to match clicks against, and follows the cursor with its
  camera; drawing changes nothing. CanvasView owns the camera, the last
  scene and the mouse drag; it reads the rest from a ViewContext.
- Edits travel as data (EditOp), so views never touch the document.
  Buffer::apply is the one place a document changes: the edit, its undo
  step, where the cursor goes, marks and `''`.
- Mode holds each mode's own state: move mode carries its Grab (whole
  branch or not), the command line lives in Mode::Command.
- Undo groups: a whole move-mode session or mouse drag is one undo step,
  built up as it goes. After an undo the cursor lands on a node the
  change added or removed, before the others it touched.
- Actions run in order, and a problem stops the rest.

Small improvements on the way: zz centers the middle of the node, :find
says which match it's on, :unlink with no links says so, folding a node
without children says why nothing happened, and a captured idea is
placed next to a new Inbox even on the first :in.

The example has end-to-end tests now: they type key sequences and mouse
gestures into it on a test terminal, and `cargo test` runs them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-10-01 04:46:05 -04:00
1 parent 57ba2c275d
commit 6eedbe0ce2
21 files changed
+2085 -763

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
//! Actions: what views, keys and the command line ask for. Views never
//! change the document, the cursor or the mode themselves. They answer with
//! actions, and the app carries them out: [`Buffer::apply`] for a document
//! and its cursor, the app for the rest.
//!
//! [`Buffer::apply`]: super::buffer::Buffer::apply
use crate::command::parse::SetArg;
use crate::model::{EditOp, Group, NodeId};
/// How a status-bar message is meant.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tone {
/// Something to know.
Info,
/// Something got done.
Done,
/// Something didn't work.
Problem,
}
/// A message for the status bar.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub text: String,
pub tone: Tone,
}
impl Message {
pub fn info(text: impl Into<String>) -> Message {
Message { text: text.into(), tone: Tone::Info }
}
pub fn done(text: impl Into<String>) -> Message {
Message { text: text.into(), tone: Tone::Done }
}
pub fn problem(text: impl Into<String>) -> Message {
Message { text: text.into(), tone: Tone::Problem }
}
}
/// Where the cursor goes after an edit.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorAfter {
/// Where it is (or to the root, if its node is gone).
Stay,
/// To the node the edit added.
Added,
/// To this node.
To(NodeId),
}
/// What happens to a document and its cursor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DocAction {
/// Move the cursor. A `jump` is one that `''` can come back from.
MoveCursor {
to: NodeId,
jump: bool,
},
/// Change the document, as one undo step (or as part of its group's).
/// The op is boxed because it can hold whole nodes, which would make
/// every action that size.
Edit {
op: Box<EditOp>,
cursor: CursorAfter,
group: Option<Group>,
},
Undo(u32),
Redo(u32),
SetMark(char),
/// Go to a mark; `'` and `` ` `` go back to before the last jump.
JumpToMark(char),
}
/// What happens around the documents.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppAction {
/// Move mode: hjkl moves the cursor's branch.
EnterMoveMode,
/// In move mode: the whole branch, or just the node.
ToggleWhole,
/// Back to normal mode.
LeaveMode,
OpenCommandLine,
/// Focus mode: Open branches act like Auto ones.
ToggleFocus,
ToggleWhichKey,
/// Change a setting.
Set(SetArg),
Quit,
Say(Message),
}
/// Anything views, keys and the command line ask for.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
Doc(DocAction),
App(AppAction),
}
impl Action {
/// Move the cursor to a node.
pub fn cursor(to: NodeId) -> Action {
Action::Doc(DocAction::MoveCursor { to, jump: false })
}
/// Jump to a node: `''` comes back.
pub fn jump(to: NodeId) -> Action {
Action::Doc(DocAction::MoveCursor { to, jump: true })
}
/// Change the document, as one undo step; the cursor stays.
pub fn edit(op: EditOp) -> Action {
Action::Doc(DocAction::Edit { op: Box::new(op), cursor: CursorAfter::Stay, group: None })
}
/// Change the document, as one undo step, then move the cursor.
pub fn edit_then(op: EditOp, cursor: CursorAfter) -> Action {
Action::Doc(DocAction::Edit { op: Box::new(op), cursor, group: None })
}
/// Change the document as part of a group's undo step.
pub fn edit_in(op: EditOp, group: Group) -> Action {
Action::Doc(DocAction::Edit { op: Box::new(op), cursor: CursorAfter::Stay, group: Some(group) })
}
/// Show a message.
pub fn say(message: Message) -> Action {
Action::App(AppAction::Say(message))
}
}