//! A playground for the canvas (stages 2 to 4 of the build): a sample //! document to move around, zoom, fold and rearrange, with the real vim keys, //! the which-key popup and the `:` command line. Text editing and the other //! views come in later stages. //! //! 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 (`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 | //! | `` | the action menu (`w`: key hints on or off) | //! | `:` | the command line: try `:fold auto`, `:color teal`, `:in an idea`, `:link rain`, `:find soil` (Tab completes) | //! | `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::command::history::History as CommandHistory; use cumulus::command::parse::{Call, Sources, TagChange, parse}; 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, CanvasPos, CanvasSize, Change, Content, Direction, Document, Edit, EditError, Fold, History, Link, LinkKey, Node, NodeId, Spot, Title, Visible, }; use cumulus::storage::document_file; use cumulus::ui::command_line::{CommandLine, LineOutcome}; 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, free_spot}; use cumulus::view::canvas::{Camera, Scene, SceneOptions, ScreenPos}; 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()?; execute!(io::stdout(), EnableMouseCapture)?; // On a crash, turn the mouse off too; ratatui's own hook restores the rest. let restore_the_rest = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { let _ = execute!(io::stdout(), DisableMouseCapture); restore_the_rest(info); })); let result = Playground::new(doc).run(&mut terminal); let _ = execute!(io::stdout(), DisableMouseCapture); ratatui::restore(); 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, }, } /// A mouse drag in progress. enum 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 }, /// Panning the view: the cell the mouse was last on. Pan { last: ScreenPos }, } struct Playground { doc: Document, history: History, cursor: NodeId, camera: Camera, options: SceneOptions, theme: Theme, keymap: Keymap, keys: KeyEngine, which_key: bool, mode: Mode, mouse: MouseReader, drag: Option, /// The command line, while it's open. command_line: Option, command_history: CommandHistory, /// The last frame drawn, to match clicks against. scene: Scene, canvas_area: Rect, /// Set when the cursor moved, so the next frame brings it into view. follow: bool, message: String, } impl Playground { fn new(doc: Document) -> Playground { let (keymap, problems) = Keymap::from_toml(KEYS_TOML); Playground { cursor: doc.root(), doc, history: History::default(), camera: Camera::default(), options: SceneOptions::default(), theme: Theme::default(), keymap, keys: KeyEngine::default(), which_key: true, mode: Mode::Normal, mouse: MouseReader::default(), drag: None, command_line: None, command_history: CommandHistory::default(), scene: Scene::default(), canvas_area: Rect::default(), follow: true, 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(event) if event.kind != KeyEventKind::Release => { if let Some(key) = Key::from_event(event) && !self.key(key) { return Ok(()); } } 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()); self.canvas_area = canvas; self.scene = self.build_scene(canvas); if self.follow { self.follow = false; if let Some(rect) = self.scene.rect_of(self.cursor) && self.camera.reveal(rect, canvas.as_size(), 3) { self.scene = self.build_scene(canvas); } } frame.render_widget(&self.scene, canvas); // While the command line is open, it takes the status line's place. if let Some(line) = &self.command_line { line.render(frame, status, canvas, &self.theme); return; } 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) } /// 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 · : command · 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 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(), 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); } 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.command_line.is_some() { return self.command_line_key(key); } 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 } /// 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" }); } 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::CommandLine => { self.command_line = Some(CommandLine::new(&self.sources(), &self.command_history)); } 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 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; } }; let (id, offset) = (self.cursor, self.camera.cells(cols, rows)); 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; } } // ----- The command line ----------------------------------------------- /// A key while the command line is open. Returns false to quit. fn command_line_key(&mut self, key: Key) -> bool { let sources = self.sources(); let Some(line) = &mut self.command_line else { return true }; match line.key(key, &sources, &self.command_history) { LineOutcome::Typing => true, LineOutcome::Close => { self.command_line = None; true } LineOutcome::Run(text) => { self.command_line = None; if text.trim().is_empty() { return true; } self.command_history.add(&text); match parse(&text, &sources) { Ok(call) => self.run_call(call), Err(error) => { self.message = error.to_string(); true } } } } } /// What the command line can name: this document, its nodes, the /// palette's colors, its tags. fn sources(&self) -> Sources { let doc = &self.doc; let nodes = doc.outline().into_iter().filter_map(|id| Some((id, doc.node(id)?.title().to_string()))); let mut tags: Vec = doc .outline() .into_iter() .filter_map(|id| doc.node(id)) .flat_map(|node| node.tags.iter().map(|tag| tag.name().to_string()).collect::>()) .collect(); tags.sort(); tags.dedup(); Sources { documents: vec![doc.title().to_string()], nodes: nodes.collect(), colors: self.theme.palette.keys().map(ToString::to_string).collect(), styles: self.theme.node_styles.keys().map(ToString::to_string).collect(), themes: vec!["default".into()], settings: Vec::new(), tags, folder: std::env::current_dir().unwrap_or_default(), } } /// Runs a command line. Returns false to quit. fn run_call(&mut self, call: Call) -> bool { let id = self.cursor; match call { Call::Quit { .. } | Call::QuitAll { .. } | Call::WriteQuit | Call::ClosePane => return false, Call::Fold(fold) => self.set_fold(fold), Call::Focus => { self.options.focus = !self.options.focus; self.message = format!("focus {}", if self.options.focus { "on" } else { "off" }); } Call::Color(color) => { self.edit(|edit| edit.update(id, |node| node.look.color = color)); } Call::Border(border) => { self.edit(|edit| edit.update(id, |node| node.look.border = Some(border))); } Call::Tags(changes) => { self.edit(|edit| { edit.update(id, |node| { for change in changes { match change { TagChange::Add(tag) => node.tags.insert(tag), TagChange::Remove(tag) => node.tags.remove(&tag), }; } }) }); } Call::Rename(title) => { let root = self.doc.root(); self.edit(|edit| { edit.update(root, |node| { if let Content::Idea(idea) = &mut node.content { idea.title = title; } }) }); } Call::Capture(text) => self.capture(&text), Call::Link(target) => match Link::new(id, target) { Some(link) => { self.edit(|edit| edit.link(link)); } None => self.message = "a node can't link to itself".into(), }, Call::Unlink(target) => { let keys: Vec = match target.and_then(|target| LinkKey::new(id, target)) { Some(key) => vec![key], None => self.doc.links_of(id).map(Link::key).collect(), }; self.edit(|edit| keys.iter().try_for_each(|&key| edit.unlink(key).map(drop))); } Call::Find(text) => self.find(&text), Call::Version => self.message = cumulus::version(), Call::Help(_) => { self.message = "the help screen comes in stage 7; Tab completes any command".into() } other => self.message = format!("{other:?} comes in a later stage of the build"), } true } /// `:in`: adds an idea under the Inbox, a loose node made the first time. fn capture(&mut self, text: &str) { let Ok(title) = Title::parse(text) else { return }; let inbox = self .doc .loose() .iter() .copied() .find(|&id| self.doc.node(id).is_some_and(|node| node.title().as_str() == "Inbox")); let rects = box_rects(&self.doc, self.options.max_width, self.theme.glyphs); let below_everything = rects.values().map(|rect| rect.bottom()).max().unwrap_or(0) + 2; let size = CanvasSize::new(30, 3); let added = self.edit(|edit| { let inbox = match inbox { Some(inbox) => inbox, None => { let pos = free_spot(CanvasPos::new(0, below_everything), size, &rects); let title = Title::parse("Inbox").map_err(|_| EditError::Full)?; edit.add(Node::idea(title, pos), Spot::Loose)? } }; let pos = child_spot(edit.doc(), inbox, size, &rects); edit.add(Node::idea(title, pos), Spot::LastChildOf(inbox)) }); if added.is_some() { self.message = format!("captured \"{text}\" to the Inbox"); } } /// `:find`: goes to the next node whose title has the text in it. fn find(&mut self, text: &str) { let wanted = text.to_lowercase(); let order = self.doc.outline(); let here = order.iter().position(|&id| id == self.cursor).unwrap_or(0); let matches: Vec = order .iter() .copied() .filter(|&id| { self.doc.node(id).is_some_and(|node| node.title().as_str().to_lowercase().contains(&wanted)) }) .collect(); let next = matches.iter().copied().find(|&id| order.iter().position(|&o| o == id) > Some(here)); match next.or_else(|| matches.first().copied()) { Some(id) => { self.jump(Some(id)); self.message = format!("{} match(es) for \"{text}\"", matches.len()); } None => self.message = format!("nothing matches \"{text}\""), } } // ----- The mouse ------------------------------------------------------ fn gesture(&mut self, gesture: Gesture) { let area = self.canvas_area; 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; Drag::Node { id, from: at, moved: CanvasOffset::default(), change: None } } None => Drag::Pan { last: at }, }); } 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); } } 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); let rects = box_rects(&self.doc, self.options.max_width, self.theme.glyphs); let Some(&from) = rects.get(&self.cursor) else { return }; let candidates = visible.order.iter().filter_map(|id| Some((*id, *rects.get(id)?))); match nearest(from, direction, candidates) { Some(id) => self.jump(Some(id)), None => self.message = "nothing that way".into(), } } fn jump(&mut self, target: Option) { match target { Some(id) => { self.cursor = id; self.follow = true; } None => self.message = "nothing there".into(), } } fn zoom(&mut self, closer: bool) { let zoom = if closer { self.camera.zoom.closer() } else { self.camera.zoom.further() }; if let Some(node) = self.doc.node(self.cursor) { self.camera.set_zoom(zoom, node.pos); } self.follow = true; } fn center(&mut self) { if let Some(node) = self.doc.node(self.cursor) { self.camera.center_on(node.pos, self.canvas_area.as_size()); } } fn toggle_fold(&mut self) { let Some(node) = self.doc.node(self.cursor) else { return }; let fold = match node.fold { Fold::Closed => Fold::Open, Fold::Open | Fold::Auto => Fold::Closed, }; self.set_fold(fold); } fn set_fold(&mut self, fold: Fold) { let id = self.cursor; self.edit(|edit| edit.update(id, |node| node.fold = fold)); self.message = format!("fold: {fold:?}"); } fn fold_all(&mut self, fold: Fold) { let parents: Vec = 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))); } /// 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 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 delete_node(&mut self) { let id = self.cursor; let next = self.doc.children(id).first().copied().or_else(|| self.doc.parent(id)); if self.edit(|edit| edit.remove_keeping_children(id)).is_some() { self.cursor = next.unwrap_or(self.doc.root()); } } fn delete_branch(&mut self) { let id = self.cursor; let next = self.doc.parent(id).or_else(|| self.doc.previous_sibling(id)); if let Some(removed) = self.edit(|edit| edit.remove_branch(id)) { self.cursor = next.unwrap_or(self.doc.root()); self.message = format!("deleted {} node(s) · u to undo", removed.len()); } } 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 = match self.history.undo(&mut self.doc) { Some(change) => change.nodes().collect(), None => { self.message = "nothing to undo".into(); return; } }; self.land_on(&touched); } fn redo(&mut self) { let touched: Vec = match self.history.redo(&mut self.doc) { Some(change) => change.nodes().collect(), None => { self.message = "nothing to redo".into(); return; } }; self.land_on(&touched); } /// After undo or redo: the cursor goes where the change happened. fn land_on(&mut self, touched: &[NodeId]) { let target = touched.iter().copied().find(|id| self.doc.contains(*id)).unwrap_or(self.doc.root()); self.jump(Some(target)); } /// Runs an edit and records it for undo; shows the error if it's refused. fn edit(&mut self, steps: impl FnOnce(&mut Edit<'_>) -> Result) -> Option { match self.doc.edit(steps) { Ok((value, change)) => { self.history.record(change); Some(value) } Err(error) => { self.message = error.to_string(); None } } } }