diff --git a/TASKS.md b/TASKS.md index 1a5397a..a05e3d1 100644 --- a/TASKS.md +++ b/TASKS.md @@ -378,3 +378,4 @@ Per-component lines live in `tasks/`. Tags there: | T32b | 1 | FAIL | handles drawn for a multi-selection fell through to a marquee; the palette ran past a 280px box; an open pen path outlived the tool | | T32b | 2 | PASS | | | T32c | 1 | PASS | | +| T32d | 1 | FAIL | Tab in a mind map's rename field added a topic; the replaced wire, the connector released on its own shape and a deleted shape's links went untested | diff --git a/src/canvas/mindmap_view.rs b/src/canvas/mindmap_view.rs index 33faa06..9b204cb 100644 --- a/src/canvas/mindmap_view.rs +++ b/src/canvas/mindmap_view.rs @@ -23,7 +23,7 @@ use crate::{ type OnTopic = Rc, &mut Window, &mut App)>; type OnKey = Rc; -/// Topics branching from a root on an endless plane, half to each side. A press selects a topic; arrows walk, Tab asks for a child, Enter for a sibling, Delete or Backspace to remove one, and F2 or a double press rewrites it; Escape lets the selection go, so Tab moves on. The owner keeps the map and answers each ask with `Topic::adding`, `removing` and `renaming`. +/// Topics branching from a root on an endless plane, half to each side. A press selects a topic; arrows walk, Tab asks for a child, Enter for a sibling, Delete or Backspace to remove one, and F2 or a double press rewrites it; Escape lets the selection go, so Tab moves on. While a topic is being rewritten its field takes every key; Tab keeps the words and hands focus back to the map. The owner keeps the map and answers each ask with `Topic::adding`, `removing` and `renaming`. #[derive(IntoElement)] pub struct MindMap { id: ElementId, @@ -245,7 +245,9 @@ impl RenderOnce for MindMap { None => plane, }; let (keys, tabbed, parent_of) = (placed.clone(), selected.clone(), self.root.clone()); - let adding = on_add.clone(); + let (adding, writing_tab, writing_keys) = + (on_add.clone(), editing.clone(), editing.clone()); + let back = focus.clone(); div() .id(id) .track_focus(&focus) @@ -256,6 +258,12 @@ impl RenderOnce for MindMap { let (Some(key), Some(on_add)) = (&tabbed, &adding) else { return; }; + if writing_tab.read(cx).field().is_some() { + cx.stop_propagation(); + writing_tab.update(cx, |editing, cx| editing.finish(false, window, cx)); + window.focus(&back); + return; + } cx.stop_propagation(); log::info!("mind map: a child under {key}"); on_add(key, window, cx); @@ -265,6 +273,9 @@ impl RenderOnce for MindMap { let Some(at) = &selected else { return; }; + if writing_keys.read(cx).field().is_some() { + return; + } let key = event.keystroke.key.as_str(); match key { "up" | "down" | "left" | "right" => { diff --git a/src/canvas/tests/boards.rs b/src/canvas/tests/boards.rs index b3707b9..7eb21de 100644 --- a/src/canvas/tests/boards.rs +++ b/src/canvas/tests/boards.rs @@ -5,7 +5,7 @@ use super::{ tools::{drag, layer, pair, press}, }; use crate::canvas::{ - Board, Edge, MindMap, Node, NodeGraph, Port, Tool, Topic, Viewport, Whiteboard, + Board, Edge, Link, MindMap, Node, NodeGraph, Port, Tool, Topic, Viewport, Whiteboard, }; fn tap(key: &str, cx: &mut VisualTestContext) { @@ -91,12 +91,13 @@ fn map(_: &Stage, owner: Entity) -> AnyElement { .child(Topic::new("a", "Site")) .child(Topic::new("b", "Press")) .child(Topic::new("c", "Budget")); - let [picked, added, removed] = [(); 3].map(|_| owner.clone()); + let [picked, added, removed, renamed] = [(); 4].map(|_| owner.clone()); MindMap::new("map", root, Viewport::new(-60.0, -150.0, 1.0)) .selected(Some("a")) .on_select(move |key, _, cx| say(&picked, format!("select {key:?}"), cx)) .on_add(move |key, _, cx| say(&added, format!("add under {key}"), cx)) .on_remove(move |key, _, cx| say(&removed, format!("remove {key}"), cx)) + .on_rename(move |key, text, _, cx| say(&renamed, format!("rename {key} {text}"), cx)) .into_any_element() } @@ -125,7 +126,7 @@ fn board(_: &Stage, owner: Entity) -> AnyElement { "board", Board { shapes: pair(), - links: Vec::new(), + links: vec![Link::new("ab", "a", "b")], }, ) .on_change(move |board, _, cx| { @@ -134,7 +135,11 @@ fn board(_: &Stage, owner: Entity) -> AnyElement { .iter() .map(|shape| shape.key.to_string()) .collect(); - say(&owner, format!("shapes {keys:?}"), cx) + say( + &owner, + format!("shapes {keys:?} links {}", board.links.len()), + cx, + ) }) .into_any_element() } @@ -144,5 +149,61 @@ fn backspace_takes_the_selection_off_the_board(cx: &mut TestAppContext) { let (host, cx) = stage(board, cx); press(250.0, 50.0, 1, cx); tap("backspace", cx); - assert_eq!(said(&host, cx), ["shapes [\"a\"]"]); + assert_eq!( + said(&host, cx), + ["shapes [\"a\"] links 0"], + "a link goes with its shape" + ); +} + +#[gpui::test] +fn tab_in_a_topic_being_rewritten_keeps_the_words_and_adds_nothing(cx: &mut TestAppContext) { + let (host, cx) = stage(map, cx); + press(276.0, 126.0, 1, cx); + tap("f2", cx); + cx.simulate_input("Web"); + tap("tab", cx); + assert_eq!(said(&host, cx), ["select Some(\"a\")", "rename a Web"]); +} + +fn three(_: &Stage, owner: Entity) -> AnyElement { + let (joined, parted) = (owner.clone(), owner); + let gain = Node::new("gain", "Gain", (0.0, 150.0)).output(Port::new("image", "Image", "image")); + let wired = Edge::new(("load", "image"), ("blur", "image")); + let edge = |edge: &Edge| { + format!( + "{}.{} {}.{}", + edge.from.0, edge.from.1, edge.to.0, edge.to.1 + ) + }; + NodeGraph::new( + "graph", + Viewport::new(0.0, 0.0, 1.0), + nodes().into_iter().chain([gain]), + [wired], + ) + .on_connect(move |made, _, cx| say(&joined, format!("connect {}", edge(made)), cx)) + .on_disconnect(move |gone, _, cx| say(&parted, format!("disconnect {}", edge(gone)), cx)) + .into_any_element() +} + +/// Gain's output sits on its right side, a title and half a row down: 150 + 32 + 14. +#[gpui::test] +fn a_new_wire_into_a_wired_input_replaces_the_old(cx: &mut TestAppContext) { + let (host, cx) = stage(three, cx); + drag((200.0, 196.0), (301.0, 47.0), cx); + assert_eq!( + said(&host, cx), + [ + "disconnect load.image blur.image", + "connect gain.image blur.image" + ] + ); +} + +#[gpui::test] +fn a_link_released_on_its_own_shape_links_nothing(cx: &mut TestAppContext) { + let (host, cx) = stage(|_, owner| layer(Tool::Connector, &[], owner), cx); + drag((50.0, 50.0), (60.0, 70.0), cx); + assert!(said(&host, cx).is_empty()); }