Add the key engine, which-key and keys.toml

Keys now work the way vim reads them. A key press becomes a Key (with vim
notation for writing keys: `<C-w>v`, `<leader>c`, `<<`), and the engine
collects counts (`12j`), registers (`"+p`), sequences (`gg`), operators
with their targets (`dd`, `dab`, `2d3d` multiplies to 6) and keys that take
a character (`ma`, `'A`), then hands over a typed Command. Every command
has a name and a help text in one table, and the default keys live in one
table per place they work (motions, normal, canvas, visual, move, connect,
lists).

keys.toml changes keys per section, with the leader configurable and
"nothing" to unbind. Each mistake is skipped and reported in words, with a
suggestion for mistyped command names, and a key that would hide a longer
one is reported too. The which-key popup reads the same tables: it shows
what can follow the keys typed so far, in skyshift's look and place, with
the leader menu instant and other prefixes after 300 ms. The mouse reader
turns button events into presses, double-clicks, drags and scrolls.

`Direction` moved from the glyphs to the model, since input needs it too.
The canvas example now runs on all of this, with a two-line keys.toml of
its own that makes `q` quit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
skkeyeandClaude Opus 5.5 committed 2026-10-01 01:54:06 -04:00
1 parent e097e53a85
commit c2e62de172
21 files changed
+2501 -204

No files matched your search

+316 -172
View File
@@ -1,50 +1,68 @@
//! A playground for the canvas engine (stage 2 of the build): a sample
//! document you can move around, zoom, fold and rearrange. The real keys,
//! modes and command line come in later stages; this has just enough to try
//! the canvas.
//! A playground for the canvas (stages 2 and 3 of the build): a sample
//! document to move around, zoom, fold and rearrange, with the real vim keys
//! and the which-key popup. The command line, text editing and the other
//! views come in later stages.
//!
//! Run it with `cargo run --example canvas`.
//! Run it with `cargo run --example canvas`. A few keys to start with:
//!
//! | Keys | What they do |
//! |----------------|-----------------------------------------------------|
//! | `hjkl`, arrows | move to the nearest node that way |
//! | `gh gl gj gk` | parent, first child, next / previous sibling |
//! | `gg` | the root |
//! | `HJKL` | pan the view |
//! | `+` `-` | zoom in / out |
//! | `za` `zf` `zi` | fold or unfold, auto-fold, focus |
//! | `zz` | center the view on the cursor |
//! | `a` `o` | add a child / a sibling below ("new idea") |
//! | `x` `dd` | delete the node (its children move up) / the branch |
//! | `M` | move mode: hjkl moves the branch, Enter or Esc ends |
//! | `u` `Ctrl-r` | undo / redo |
//! | `q` | quit |
//! | Keys | What they do |
//! |----------------|------------------------------------------------------|
//! | `hjkl`, arrows | move to the nearest node that way (`5j`: five times) |
//! | `g` | go to: `gh gl gj gk` the tree, `gg` the root |
//! | `z` | view & folds: `za` `zf` `zi` `zz` `zR` `zM` |
//! | `+` `-` | zoom in / out |
//! | `a` `o` `O` | add a child / a sibling below / above |
//! | `x` `dd` `dib` | delete the node / the branch / the children |
//! | `M` | move mode: hjkl moves the branch, Enter ends |
//! | `u` `Ctrl-r` | undo / redo |
//! | `<Space>` | the action menu (`<Space>w`: key hints on or off) |
//! | `q`, `ZZ` | quit |
//!
//! Mouse: click selects, dragging a node moves its branch, dragging empty
//! space pans, the wheel scrolls.
use std::io;
use std::time::{Duration, Instant};
use cumulus::input::bindings::Context;
use cumulus::input::command::{Bound, Command, Motion, Operator, Target, help_for};
use cumulus::input::engine::{KeyEngine, Outcome, Typed};
use cumulus::input::key::Key;
use cumulus::input::keymap::Keymap;
use cumulus::input::mouse::{Cell, Gesture, MouseReader};
use cumulus::model::{
CanvasOffset, Change, Document, Edit, EditError, Fold, History, Node, NodeId, Spot, Title, Visible,
CanvasOffset, Change, Direction, Document, Edit, EditError, Fold, History, Node, NodeId, Spot, Title,
Visible,
};
use cumulus::storage::document_file;
use cumulus::ui::glyphs::Direction;
use cumulus::ui::theme::Theme;
use cumulus::ui::which_key::WhichKey;
use cumulus::view::canvas::boxes::{NodeBox, box_rects};
use cumulus::view::canvas::navigate::nearest;
use cumulus::view::canvas::placement::{below_spot, child_spot};
use cumulus::view::canvas::{Camera, Scene, SceneOptions, ScreenPos};
use ratatui::crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
MouseButton, MouseEvent, MouseEventKind,
};
use ratatui::crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind};
use ratatui::crossterm::execute;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::{DefaultTerminal, Frame};
/// The playground's own keys.toml: `q` quits. (cumulus itself keeps `q`
/// free for macros; this shows how keys.toml changes a key.)
const KEYS_TOML: &str = r#"
[normal]
"q" = "close"
"#;
/// How long a sequence waits before its which-key popup shows (the leader's
/// shows right away).
const WHICH_KEY_DELAY: Duration = Duration::from_millis(300);
const NORMAL: &[Context] = &[Context::Canvas, Context::Normal, Context::Motions];
const MOVING: &[Context] = &[Context::Move];
fn main() -> io::Result<()> {
let doc = document_file::parse(include_str!("garden.cum")).map_err(io::Error::other)?;
let mut terminal = ratatui::try_init()?;
@@ -61,10 +79,20 @@ fn main() -> io::Result<()> {
result
}
enum Mode {
Normal,
/// Moving the cursor's node: its whole branch, or just the node. The
/// undo step for the whole move builds up as it goes.
Move {
whole: bool,
change: Option<Change>,
},
}
/// A mouse drag in progress.
enum Drag {
/// Moving a node's branch: where the drag started, how far the branch has
/// moved so far, and the undo step for the whole drag.
/// Moving a node's branch: how far it has moved so far, and the undo
/// step for the whole drag.
Node { id: NodeId, from: ScreenPos, moved: CanvasOffset, change: Option<Change> },
/// Panning the view: the cell the mouse was last on.
Pan { last: ScreenPos },
@@ -77,11 +105,11 @@ struct Playground {
camera: Camera,
options: SceneOptions,
theme: Theme,
/// The first key of a two-key command (`g`, `z`, `d`), waiting for the
/// second.
pending: Option<char>,
/// In move mode: the undo step for the whole move so far.
moving: Option<Option<Change>>,
keymap: Keymap,
keys: KeyEngine,
which_key: bool,
mode: Mode,
mouse: MouseReader,
drag: Option<Drag>,
/// The last frame drawn, to match clicks against.
scene: Scene,
@@ -93,6 +121,7 @@ struct Playground {
impl Playground {
fn new(doc: Document) -> Playground {
let (keymap, problems) = Keymap::from_toml(KEYS_TOML);
Playground {
cursor: doc.root(),
doc,
@@ -100,32 +129,50 @@ impl Playground {
camera: Camera::default(),
options: SceneOptions::default(),
theme: Theme::default(),
pending: None,
moving: None,
keymap,
keys: KeyEngine::default(),
which_key: true,
mode: Mode::Normal,
mouse: MouseReader::default(),
drag: None,
scene: Scene::default(),
canvas_area: Rect::default(),
follow: true,
message: String::new(),
message: problems.join(" · "),
}
}
fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
loop {
terminal.draw(|frame| self.draw(frame))?;
// While a sequence is half typed, wake up now and then: its
// which-key popup may be due.
let wait =
if self.keys.is_pending() { Duration::from_millis(50) } else { Duration::from_secs(3600) };
if !event::poll(wait)? {
continue;
}
match event::read()? {
// Windows reports key releases too: only presses and repeats count.
Event::Key(key) if key.kind != KeyEventKind::Release => {
if !self.key(key) {
Event::Key(event) if event.kind != KeyEventKind::Release => {
if let Some(key) = Key::from_event(event)
&& !self.key(key)
{
return Ok(());
}
}
Event::Mouse(mouse) => self.mouse(mouse),
Event::Mouse(event) => {
if let Some(gesture) = self.mouse.read(event, Instant::now()) {
self.gesture(gesture);
}
}
_ => {}
}
}
}
// ----- Drawing --------------------------------------------------------
fn draw(&mut self, frame: &mut Frame) {
let [canvas, status] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(frame.area());
@@ -140,131 +187,208 @@ impl Playground {
}
}
frame.render_widget(&self.scene, canvas);
frame.render_widget(self.status_line(), status);
if self.which_key_due()
&& let Some(hints) = self.keys.hints(&self.keymap.tables(self.contexts()))
{
frame.render_widget(WhichKey { hints: &hints, theme: &self.theme }, canvas);
}
self.draw_status(frame, status);
}
fn build_scene(&self, area: Rect) -> Scene {
Scene::build(&self.doc, self.cursor, self.camera, area.as_size(), &self.theme, self.options)
}
fn status_line(&self) -> Line<'static> {
let mode = if self.moving.is_some() { " MOVE " } else { " CANVAS " };
/// The bottom line: the mode, the zoom and the cursor's title, then a
/// message or key hints, and the keys typed so far on the right.
fn draw_status(&self, frame: &mut Frame, area: Rect) {
let (mode, hints) = match self.mode {
Mode::Normal => (
" NORMAL ",
"hjkl move · g go to · z folds · +/- zoom · a/o add · x/dd delete · M move · u undo · <Space> menu · q quit",
),
Mode::Move { whole: true, .. } => {
(" MOVE ", "hjkl moves the branch (HJKL by 5) · M just this node · Enter done")
}
Mode::Move { whole: false, .. } => {
(" MOVE ", "hjkl moves just this node (HJKL by 5) · M the whole branch · Enter done")
}
};
let title = self.doc.node(self.cursor).map(|node| node.title().to_string()).unwrap_or_default();
let focus = if self.options.focus { " · focus" } else { "" };
let hints = if self.message.is_empty() {
"hjkl move · gh/gl tree · +/- zoom · za/zf/zi fold · a/o add · x/dd delete · M move · u undo · q quit"
.to_string()
} else {
self.message.clone()
};
let pending = self.pending.map(|key| format!(" {key}")).unwrap_or_default();
Line::from(vec![
let middle = if self.message.is_empty() { hints.dark_gray() } else { self.message.clone().yellow() };
let line = Line::from(vec![
mode.black().on_cyan().bold(),
format!(" {}{focus} │ {title} │ ", self.camera.zoom.name()).into(),
hints.dark_gray(),
pending.cyan().bold(),
])
middle,
]);
frame.render_widget(Paragraph::new(line), area);
let typed = Line::from(format!("{} ", self.keys.shown()).cyan().bold());
frame.render_widget(Paragraph::new(typed).right_aligned(), area);
}
/// Handles a key. Returns false to quit.
fn key(&mut self, key: KeyEvent) -> bool {
fn which_key_due(&self) -> bool {
let delay = if self.keys.first_key() == Some(self.keymap.leader()) {
Duration::ZERO
} else {
WHICH_KEY_DELAY
};
self.which_key && self.keys.pending_since().is_some_and(|since| since.elapsed() >= delay)
}
// ----- Keys -----------------------------------------------------------
fn contexts(&self) -> &'static [Context] {
match self.mode {
Mode::Normal => NORMAL,
Mode::Move { .. } => MOVING,
}
}
/// Handles a key press. Returns false to quit.
fn key(&mut self, key: Key) -> bool {
self.message.clear();
if self.moving.is_some() {
self.move_key(key);
return true;
}
if let Some(first) = self.pending.take() {
if let KeyCode::Char(second) = key.code {
self.sequence(first, second);
}
return true;
}
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Char('q') => return false,
KeyCode::Char('c') if ctrl => return false,
KeyCode::Char('r') if ctrl => self.redo(),
KeyCode::Char('h') | KeyCode::Left => self.go(Direction::Left),
KeyCode::Char('j') | KeyCode::Down => self.go(Direction::Down),
KeyCode::Char('k') | KeyCode::Up => self.go(Direction::Up),
KeyCode::Char('l') | KeyCode::Right => self.go(Direction::Right),
KeyCode::Char('H') => self.camera.pan(-8, 0),
KeyCode::Char('J') => self.camera.pan(0, 4),
KeyCode::Char('K') => self.camera.pan(0, -4),
KeyCode::Char('L') => self.camera.pan(8, 0),
KeyCode::Char('+' | '=') => self.zoom(true),
KeyCode::Char('-') => self.zoom(false),
KeyCode::Char('a') => self.add_child(),
KeyCode::Char('o') => self.add_below(),
KeyCode::Char('x') => self.delete_node(),
KeyCode::Char('u') => self.undo(),
KeyCode::Char('M') => {
self.moving = Some(None);
self.message = "hjkl moves the branch (HJKL by 5) · Enter or Esc ends".into();
}
KeyCode::Char(first @ ('g' | 'z' | 'd')) => self.pending = Some(first),
_ => {}
let outcome = self.keys.press(key, &self.keymap.tables(self.contexts()));
match outcome {
Outcome::Run(typed) => return self.run_command(typed),
Outcome::Unknown(keys) => self.message = format!("{keys} does nothing here"),
Outcome::Waiting | Outcome::Cancelled => {}
}
true
}
/// The second key of `g…`, `z…` and `d…`.
fn sequence(&mut self, first: char, second: char) {
match (first, second) {
('g', 'g') => self.jump(Some(self.doc.root())),
('g', 'h') => self.jump(self.doc.parent(self.cursor)),
('g', 'l') => self.jump(self.doc.children(self.cursor).first().copied()),
('g', 'j') => self.jump(self.doc.next_sibling(self.cursor)),
('g', 'k') => self.jump(self.doc.previous_sibling(self.cursor)),
('z', 'a') => self.toggle_fold(),
('z', 'f') => self.set_fold(Fold::Auto),
('z', 'i') => {
/// Runs a command. Returns false to quit.
fn run_command(&mut self, typed: Typed) -> bool {
if let Mode::Move { .. } = self.mode {
self.move_command(typed);
return true;
}
let times = typed.times();
let steps = i32::try_from(times).unwrap_or(1);
match typed.command {
Command::Move(motion) => (0..times).for_each(|_| self.motion(motion)),
Command::Operate(Operator::Delete, Target::Current | Target::Branch) => self.delete_branch(),
Command::Operate(Operator::Delete, Target::Children) => self.delete_children(),
Command::DeleteNode => self.delete_node(),
Command::AddChild => self.add(Spot::LastChildOf(self.cursor)),
Command::AddBelow => self.add(Spot::After(self.cursor)),
Command::AddAbove => self.add(Spot::Before(self.cursor)),
Command::Undo => (0..times).for_each(|_| self.undo()),
Command::Redo => (0..times).for_each(|_| self.redo()),
Command::ZoomIn => self.zoom(true),
Command::ZoomOut => self.zoom(false),
Command::ToggleFold => self.toggle_fold(),
Command::OpenFold => self.set_fold(Fold::Open),
Command::CloseFold => self.set_fold(Fold::Closed),
Command::AutoFold => self.set_fold(Fold::Auto),
Command::OpenAllFolds => self.fold_all(Fold::Open),
Command::CloseAllFolds => self.fold_all(Fold::Closed),
Command::ToggleFocus => {
self.options.focus = !self.options.focus;
self.message = format!("focus {}", if self.options.focus { "on" } else { "off" });
}
('z', 'z') => self.center(),
('d', 'd') => self.delete_branch(),
_ => self.message = format!("{first}{second} does nothing here"),
Command::Center => self.center(),
Command::ScrollDown => self.camera.pan(0, 3 * steps),
Command::ScrollUp => self.camera.pan(0, -3 * steps),
Command::ScrollLeft => self.camera.pan(-6 * steps, 0),
Command::ScrollRight => self.camera.pan(6 * steps, 0),
Command::HalfPageDown => self.camera.pan(0, i32::from(self.canvas_area.height / 2)),
Command::HalfPageUp => self.camera.pan(0, -i32::from(self.canvas_area.height / 2)),
Command::MoveMode => self.mode = Mode::Move { whole: true, change: None },
Command::ToggleWhichKey => {
self.which_key = !self.which_key;
self.message = format!("key hints {}", if self.which_key { "on" } else { "off" });
}
Command::Escape => {}
Command::Close => return false,
other => {
let help = help_for(Bound::Command(other));
self.message = format!("\"{help}\" comes in a later stage of the build");
}
}
true
}
fn move_key(&mut self, key: KeyEvent) {
let (cols, rows) = match key.code {
KeyCode::Char('h') | KeyCode::Left => (-1, 0),
KeyCode::Char('l') | KeyCode::Right => (1, 0),
KeyCode::Char('j') | KeyCode::Down => (0, 1),
KeyCode::Char('k') | KeyCode::Up => (0, -1),
KeyCode::Char('H') => (-5, 0),
KeyCode::Char('L') => (5, 0),
KeyCode::Char('J') => (0, 5),
KeyCode::Char('K') => (0, -5),
KeyCode::Enter | KeyCode::Esc | KeyCode::Char('M') => {
if let Some(Some(change)) = self.moving.take() {
fn motion(&mut self, motion: Motion) {
let doc = &self.doc;
let target = match motion {
Motion::Left => return self.go(Direction::Left),
Motion::Down => return self.go(Direction::Down),
Motion::Up => return self.go(Direction::Up),
Motion::Right => return self.go(Direction::Right),
Motion::Parent => doc.parent(self.cursor),
Motion::FirstChild => doc.children(self.cursor).first().copied(),
Motion::NextSibling => doc.next_sibling(self.cursor),
Motion::PreviousSibling => doc.previous_sibling(self.cursor),
Motion::First => Some(doc.root()),
Motion::Next | Motion::Previous | Motion::Last => {
let order = Visible::new(doc, self.cursor, self.options.focus).order;
let here = order.iter().position(|&id| id == self.cursor).unwrap_or(0);
match motion {
Motion::Next => order.get(here + 1).copied(),
Motion::Previous => here.checked_sub(1).and_then(|index| order.get(index).copied()),
_ => order.last().copied(),
}
}
_ => {
self.message = "search comes in a later stage of the build".into();
return;
}
};
self.jump(target);
}
/// Commands in move mode: hjkl moves the node or its branch.
fn move_command(&mut self, typed: Typed) {
let step = i32::try_from(typed.times()).unwrap_or(1);
let (cols, rows) = match typed.command {
Command::Move(Motion::Left) => (-step, 0),
Command::Move(Motion::Right) => (step, 0),
Command::Move(Motion::Down) => (0, step),
Command::Move(Motion::Up) => (0, -step),
Command::FarLeft => (-5 * step, 0),
Command::FarRight => (5 * step, 0),
Command::FarDown => (0, 5 * step),
Command::FarUp => (0, -5 * step),
Command::ToggleWhole => {
if let Mode::Move { whole, .. } = &mut self.mode {
*whole = !*whole;
}
return;
}
_ => {
if let Mode::Move { change: Some(change), .. } =
std::mem::replace(&mut self.mode, Mode::Normal)
{
self.history.record(change);
}
return;
}
_ => return,
};
let (id, offset) = (self.cursor, self.camera.cells(cols, rows));
if let Ok(((), next)) = self.doc.edit(|edit| edit.shift_branch(id, offset)) {
let so_far = self.moving.take().flatten();
self.moving = Some(Some(match so_far {
Some(change) => change.then(next),
let whole = matches!(self.mode, Mode::Move { whole: true, .. });
let result = self.doc.edit(|edit| {
if whole { edit.shift_branch(id, offset) } else { edit.update(id, |node| node.pos += offset) }
});
if let (Ok(((), next)), Mode::Move { change, .. }) = (result, &mut self.mode) {
*change = Some(match change.take() {
Some(so_far) => so_far.then(next),
None => next,
}));
});
self.follow = true;
}
}
fn mouse(&mut self, mouse: MouseEvent) {
// ----- The mouse ------------------------------------------------------
fn gesture(&mut self, gesture: Gesture) {
let area = self.canvas_area;
let at = ScreenPos::new(
i32::from(mouse.column) - i32::from(area.x),
i32::from(mouse.row) - i32::from(area.y),
);
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
let screen = |cell: Cell| {
ScreenPos::new(i32::from(cell.col) - i32::from(area.x), i32::from(cell.row) - i32::from(area.y))
};
match gesture {
Gesture::Press { at, .. } => {
let at = screen(at);
self.drag = Some(match self.scene.node_at(at) {
Some(id) => {
self.cursor = id;
@@ -273,40 +397,49 @@ impl Playground {
None => Drag::Pan { last: at },
});
}
MouseEventKind::Drag(MouseButton::Left) => match &mut self.drag {
Some(Drag::Node { id, from, moved, change }) => {
let total = self.camera.cells(at.col - from.col, at.row - from.row);
let step = CanvasOffset::new(total.dx - moved.dx, total.dy - moved.dy);
let id = *id;
if step != CanvasOffset::default()
&& let Ok(((), next)) = self.doc.edit(|edit| edit.shift_branch(id, step))
{
*moved = total;
*change = Some(match change.take() {
Some(so_far) => so_far.then(next),
None => next,
});
}
}
Some(Drag::Pan { last }) => {
self.camera.pan(last.col - at.col, last.row - at.row);
*last = at;
}
None => {}
},
MouseEventKind::Up(MouseButton::Left) => {
Gesture::DoublePress { .. } => self.message = "editing titles comes in stage 6".into(),
Gesture::Drag { at, .. } => self.drag_to(screen(at)),
Gesture::Release { .. } => {
if let Some(Drag::Node { change: Some(change), .. }) = self.drag.take() {
self.history.record(change);
}
}
MouseEventKind::ScrollDown => self.camera.pan(0, 3),
MouseEventKind::ScrollUp => self.camera.pan(0, -3),
MouseEventKind::ScrollRight => self.camera.pan(6, 0),
MouseEventKind::ScrollLeft => self.camera.pan(-6, 0),
_ => {}
Gesture::Scroll { direction, .. } => match direction {
Direction::Up => self.camera.pan(0, -3),
Direction::Down => self.camera.pan(0, 3),
Direction::Left => self.camera.pan(-6, 0),
Direction::Right => self.camera.pan(6, 0),
},
Gesture::RightPress { .. } => self.message = "the right-click menu comes in stage 7".into(),
}
}
fn drag_to(&mut self, at: ScreenPos) {
match &mut self.drag {
Some(Drag::Node { id, from, moved, change }) => {
let total = self.camera.cells(at.col - from.col, at.row - from.row);
let step = CanvasOffset::new(total.dx - moved.dx, total.dy - moved.dy);
let id = *id;
if step != CanvasOffset::default()
&& let Ok(((), next)) = self.doc.edit(|edit| edit.shift_branch(id, step))
{
*moved = total;
*change = Some(match change.take() {
Some(so_far) => so_far.then(next),
None => next,
});
}
}
Some(Drag::Pan { last }) => {
self.camera.pan(last.col - at.col, last.row - at.row);
*last = at;
}
None => {}
}
}
// ----- What the commands do -------------------------------------------
/// Moves the cursor to the nearest node on show in a direction.
fn go(&mut self, direction: Direction) {
let visible = Visible::new(&self.doc, self.cursor, self.options.focus);
@@ -358,30 +491,27 @@ impl Playground {
self.message = format!("fold: {fold:?}");
}
fn add_child(&mut self) {
let Some(node) = self.new_idea() else { return };
let rects = box_rects(&self.doc, self.options.max_width, self.theme.glyphs);
let size = NodeBox::new(&node, String::new(), self.options.max_width, self.theme.glyphs).rect.size;
let parent = self.cursor;
let node = Node { pos: child_spot(&self.doc, parent, size, &rects), ..node };
let added = self.edit(|edit| edit.add(node, Spot::LastChildOf(parent)));
self.jump(added);
fn fold_all(&mut self, fold: Fold) {
let parents: Vec<NodeId> =
self.doc.outline().into_iter().filter(|&id| !self.doc.children(id).is_empty()).collect();
self.edit(|edit| parents.iter().try_for_each(|&id| edit.update(id, |node| node.fold = fold)));
}
fn add_below(&mut self) {
let Some(node) = self.new_idea() else { return };
/// Adds a "new idea" node at `spot`, placed next to its parent or
/// sibling, and moves the cursor to it.
fn add(&mut self, spot: Spot) {
let Ok(title) = Title::parse("new idea") else { return };
let node = Node::idea(title, Default::default());
let rects = box_rects(&self.doc, self.options.max_width, self.theme.glyphs);
let size = NodeBox::new(&node, String::new(), self.options.max_width, self.theme.glyphs).rect.size;
let sibling = self.cursor;
let node = Node { pos: below_spot(sibling, size, &rects), ..node };
let added = self.edit(|edit| edit.add(node, Spot::After(sibling)));
let pos = match spot {
Spot::LastChildOf(parent) => child_spot(&self.doc, parent, size, &rects),
_ => below_spot(self.cursor, size, &rects),
};
let added = self.edit(|edit| edit.add(Node { pos, ..node }, spot));
self.jump(added);
}
fn new_idea(&self) -> Option<Node> {
Title::parse("new idea").ok().map(|title| Node::idea(title, Default::default()))
}
fn delete_node(&mut self) {
let id = self.cursor;
let next = self.doc.children(id).first().copied().or_else(|| self.doc.parent(id));
@@ -399,6 +529,20 @@ impl Playground {
}
}
fn delete_children(&mut self) {
let children = self.doc.children(self.cursor).to_vec();
let removed = self.edit(|edit| {
let mut count = 0;
for &child in &children {
count += edit.remove_branch(child)?.len();
}
Ok(count)
});
if let Some(count) = removed {
self.message = format!("deleted {count} node(s) · u to undo");
}
}
fn undo(&mut self) {
let touched: Vec<NodeId> = match self.history.undo(&mut self.doc) {
Some(change) => change.nodes().collect(),