Canvas · Graphs and boards: Tab keeps a topic's words; the untested rules tested
Tab in a mind map topic being rewritten keeps the words and hands focus back to the map; it asks for no child while the field is open, and the map's keys wait for the field. Tests cover a new wire into a wired input replacing the old, a connector released on its own shape linking nothing, and a deleted whiteboard shape taking its links with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3822a8d98
commit
bd0285bd37
3 files changed
+80
-7
No files matched your search
@@ -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 | 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 | |
|
| T32b | 2 | PASS | |
|
||||||
| T32c | 1 | 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 |
|
||||||
@@ -23,7 +23,7 @@ use crate::{
|
|||||||
type OnTopic = Rc<dyn Fn(Option<&SharedString>, &mut Window, &mut App)>;
|
type OnTopic = Rc<dyn Fn(Option<&SharedString>, &mut Window, &mut App)>;
|
||||||
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
|
||||||
|
|
||||||
/// 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)]
|
#[derive(IntoElement)]
|
||||||
pub struct MindMap {
|
pub struct MindMap {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
@@ -245,7 +245,9 @@ impl RenderOnce for MindMap {
|
|||||||
None => plane,
|
None => plane,
|
||||||
};
|
};
|
||||||
let (keys, tabbed, parent_of) = (placed.clone(), selected.clone(), self.root.clone());
|
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()
|
div()
|
||||||
.id(id)
|
.id(id)
|
||||||
.track_focus(&focus)
|
.track_focus(&focus)
|
||||||
@@ -256,6 +258,12 @@ impl RenderOnce for MindMap {
|
|||||||
let (Some(key), Some(on_add)) = (&tabbed, &adding) else {
|
let (Some(key), Some(on_add)) = (&tabbed, &adding) else {
|
||||||
return;
|
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();
|
cx.stop_propagation();
|
||||||
log::info!("mind map: a child under {key}");
|
log::info!("mind map: a child under {key}");
|
||||||
on_add(key, window, cx);
|
on_add(key, window, cx);
|
||||||
@@ -265,6 +273,9 @@ impl RenderOnce for MindMap {
|
|||||||
let Some(at) = &selected else {
|
let Some(at) = &selected else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if writing_keys.read(cx).field().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let key = event.keystroke.key.as_str();
|
let key = event.keystroke.key.as_str();
|
||||||
match key {
|
match key {
|
||||||
"up" | "down" | "left" | "right" => {
|
"up" | "down" | "left" | "right" => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use super::{
|
|||||||
tools::{drag, layer, pair, press},
|
tools::{drag, layer, pair, press},
|
||||||
};
|
};
|
||||||
use crate::canvas::{
|
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) {
|
fn tap(key: &str, cx: &mut VisualTestContext) {
|
||||||
@@ -91,12 +91,13 @@ fn map(_: &Stage, owner: Entity<Stage>) -> AnyElement {
|
|||||||
.child(Topic::new("a", "Site"))
|
.child(Topic::new("a", "Site"))
|
||||||
.child(Topic::new("b", "Press"))
|
.child(Topic::new("b", "Press"))
|
||||||
.child(Topic::new("c", "Budget"));
|
.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))
|
MindMap::new("map", root, Viewport::new(-60.0, -150.0, 1.0))
|
||||||
.selected(Some("a"))
|
.selected(Some("a"))
|
||||||
.on_select(move |key, _, cx| say(&picked, format!("select {key:?}"), cx))
|
.on_select(move |key, _, cx| say(&picked, format!("select {key:?}"), cx))
|
||||||
.on_add(move |key, _, cx| say(&added, format!("add under {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_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()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ fn board(_: &Stage, owner: Entity<Stage>) -> AnyElement {
|
|||||||
"board",
|
"board",
|
||||||
Board {
|
Board {
|
||||||
shapes: pair(),
|
shapes: pair(),
|
||||||
links: Vec::new(),
|
links: vec![Link::new("ab", "a", "b")],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on_change(move |board, _, cx| {
|
.on_change(move |board, _, cx| {
|
||||||
@@ -134,7 +135,11 @@ fn board(_: &Stage, owner: Entity<Stage>) -> AnyElement {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|shape| shape.key.to_string())
|
.map(|shape| shape.key.to_string())
|
||||||
.collect();
|
.collect();
|
||||||
say(&owner, format!("shapes {keys:?}"), cx)
|
say(
|
||||||
|
&owner,
|
||||||
|
format!("shapes {keys:?} links {}", board.links.len()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -144,5 +149,61 @@ fn backspace_takes_the_selection_off_the_board(cx: &mut TestAppContext) {
|
|||||||
let (host, cx) = stage(board, cx);
|
let (host, cx) = stage(board, cx);
|
||||||
press(250.0, 50.0, 1, cx);
|
press(250.0, 50.0, 1, cx);
|
||||||
tap("backspace", 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<Stage>) -> 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());
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user