Canvas · Graphs and boards: node graph, flow chart, mind map, whiteboard
NodeGraph joins Nodes by wires between typed Ports: a title drags a node, a drag out of an output lands on an input of the same kind, an input takes one wire, and a press on a wired input lifts its wire. Links join shapes on the plane and follow them, straight or at right angles, with an arrow and words; the Connector tool draws them and the Note tool drops a sticky note. A double press with Select writes on a shape. FlowChart draws Steps as ellipses, boxes and diamonds with their words. MindMap lays topics out from a root, half to each side, and asks the owner to add, remove and rename by key. Whiteboard keeps its own tool, view and selection over a board it hands back after each change. Painting moves to canvas::paint; a press on the tool layer goes through canvas::press, and the layer focuses itself and asks to delete.
This commit is contained in:
1 parent
ec1988cd6f
commit
4498ff09dc
28 files changed
+3068
-240
No files matched your search
@@ -0,0 +1,148 @@
|
||||
use gpui::{AnyElement, Entity, IntoElement, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::{
|
||||
Stage, said, say, settle, stage,
|
||||
tools::{drag, layer, pair, press},
|
||||
};
|
||||
use crate::canvas::{
|
||||
Board, Edge, MindMap, Node, NodeGraph, Port, Tool, Topic, Viewport, Whiteboard,
|
||||
};
|
||||
|
||||
fn tap(key: &str, cx: &mut VisualTestContext) {
|
||||
cx.simulate_keystrokes(key);
|
||||
cx.simulate_event(gpui::KeyUpEvent {
|
||||
keystroke: gpui::Keystroke::parse(key).expect("a key"),
|
||||
});
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_connector_links_the_shape_pressed_to_the_one_released_on(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(|_, owner| layer(Tool::Connector, &[], owner), cx);
|
||||
drag((50.0, 50.0), (250.0, 60.0), cx);
|
||||
drag((50.0, 50.0), (150.0, 200.0), cx);
|
||||
assert_eq!(
|
||||
said(&host, cx),
|
||||
["link a b"],
|
||||
"a release on nothing links nothing"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_double_press_writes_on_a_shape_and_enter_keeps_the_words(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(|_, owner| layer(Tool::Select, &["a"], owner), cx);
|
||||
press(50.0, 50.0, 1, cx);
|
||||
press(50.0, 50.0, 2, cx);
|
||||
cx.simulate_input("Start");
|
||||
tap("enter", cx);
|
||||
assert_eq!(said(&host, cx), ["text a Start"]);
|
||||
}
|
||||
|
||||
fn nodes() -> Vec<Node> {
|
||||
vec![
|
||||
Node::new("load", "Load", (0.0, 0.0)).output(Port::new("image", "Image", "image")),
|
||||
Node::new("blur", "Blur", (300.0, 0.0))
|
||||
.input(Port::new("image", "Image", "image"))
|
||||
.output(Port::new("image", "Image", "image")),
|
||||
]
|
||||
}
|
||||
|
||||
fn graph(wired: bool, owner: Entity<Stage>) -> AnyElement {
|
||||
let (joined, parted) = (owner.clone(), owner);
|
||||
let edges = wired.then(|| Edge::new(("load", "image"), ("blur", "image")));
|
||||
NodeGraph::new("graph", Viewport::new(0.0, 0.0, 1.0), nodes(), edges)
|
||||
.on_connect(move |edge, _, cx| {
|
||||
say(
|
||||
&joined,
|
||||
format!(
|
||||
"connect {}.{} {}.{}",
|
||||
edge.from.0, edge.from.1, edge.to.0, edge.to.1
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_disconnect(move |edge, _, cx| {
|
||||
say(
|
||||
&parted,
|
||||
format!("disconnect {}.{}", edge.to.0, edge.to.1),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Sockets sit on the node's sides a title and half a row down: 32 + 14.
|
||||
#[gpui::test]
|
||||
fn a_drag_from_an_output_wires_it_to_an_input(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(|_, owner| graph(false, owner), cx);
|
||||
drag((200.0, 46.0), (301.0, 47.0), cx);
|
||||
assert_eq!(said(&host, cx), ["connect load.image blur.image"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_press_on_a_wired_input_lifts_its_wire(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(|_, owner| graph(true, owner), cx);
|
||||
drag((300.0, 46.0), (250.0, 250.0), cx);
|
||||
assert_eq!(said(&host, cx), ["disconnect blur.image"]);
|
||||
}
|
||||
|
||||
fn map(_: &Stage, owner: Entity<Stage>) -> AnyElement {
|
||||
let root = Topic::new("root", "Launch")
|
||||
.child(Topic::new("a", "Site"))
|
||||
.child(Topic::new("b", "Press"))
|
||||
.child(Topic::new("c", "Budget"));
|
||||
let [picked, added, removed] = [(); 3].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))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn keys_on_a_mind_map_ask_for_children_siblings_and_removals(cx: &mut TestAppContext) {
|
||||
let (host, cx) = stage(map, cx);
|
||||
press(276.0, 126.0, 1, cx);
|
||||
for key in ["tab", "enter", "down", "backspace"] {
|
||||
tap(key, cx);
|
||||
}
|
||||
assert_eq!(
|
||||
said(&host, cx),
|
||||
[
|
||||
"select Some(\"a\")",
|
||||
"add under a",
|
||||
"add under root",
|
||||
"select Some(\"b\")",
|
||||
"remove a",
|
||||
],
|
||||
"a press selects and focuses; the keys act on the owner's selection"
|
||||
);
|
||||
}
|
||||
|
||||
fn board(_: &Stage, owner: Entity<Stage>) -> AnyElement {
|
||||
Whiteboard::new(
|
||||
"board",
|
||||
Board {
|
||||
shapes: pair(),
|
||||
links: Vec::new(),
|
||||
},
|
||||
)
|
||||
.on_change(move |board, _, cx| {
|
||||
let keys: Vec<String> = board
|
||||
.shapes
|
||||
.iter()
|
||||
.map(|shape| shape.key.to_string())
|
||||
.collect();
|
||||
say(&owner, format!("shapes {keys:?}"), cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
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\"]"]);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use gpui::{
|
||||
use super::{Frame, InfiniteCanvas, MiniMap, Tool, Viewport, ZoomControls};
|
||||
use crate::{primitives::FocusNext, theme::Theme};
|
||||
|
||||
mod boards;
|
||||
mod panels;
|
||||
mod tools;
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ use crate::canvas::{
|
||||
Frame, InfiniteCanvas, Shape, ShapeKind, Tool, ToolLayer, ToolPalette, Viewport,
|
||||
};
|
||||
|
||||
fn at(x: f32, y: f32) -> gpui::Point<gpui::Pixels> {
|
||||
pub(super) fn at(x: f32, y: f32) -> gpui::Point<gpui::Pixels> {
|
||||
point(px(x), px(y))
|
||||
}
|
||||
|
||||
/// Presses at `from`, drags in steps to `to`, and lets go.
|
||||
fn drag(from: (f32, f32), to: (f32, f32), cx: &mut VisualTestContext) {
|
||||
pub(super) fn drag(from: (f32, f32), to: (f32, f32), cx: &mut VisualTestContext) {
|
||||
cx.simulate_mouse_move(at(from.0, from.1), None, Modifiers::none());
|
||||
cx.simulate_mouse_down(at(from.0, from.1), MouseButton::Left, Modifiers::none());
|
||||
for step in 1..=4 {
|
||||
@@ -27,7 +27,7 @@ fn drag(from: (f32, f32), to: (f32, f32), cx: &mut VisualTestContext) {
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn press(x: f32, y: f32, count: usize, cx: &mut VisualTestContext) {
|
||||
pub(super) fn press(x: f32, y: f32, count: usize, cx: &mut VisualTestContext) {
|
||||
cx.simulate_event(MouseDownEvent {
|
||||
button: MouseButton::Left,
|
||||
position: at(x, y),
|
||||
@@ -44,7 +44,7 @@ fn press(x: f32, y: f32, count: usize, cx: &mut VisualTestContext) {
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
fn pair() -> Vec<Shape> {
|
||||
pub(super) fn pair() -> Vec<Shape> {
|
||||
vec![
|
||||
Shape::new(
|
||||
"a",
|
||||
@@ -61,9 +61,9 @@ fn pair() -> Vec<Shape> {
|
||||
]
|
||||
}
|
||||
|
||||
fn layer(tool: Tool, selected: &[&'static str], owner: Entity<Stage>) -> AnyElement {
|
||||
pub(super) fn layer(tool: Tool, selected: &[&'static str], owner: Entity<Stage>) -> AnyElement {
|
||||
let view = Viewport::new(0.0, 0.0, 1.0);
|
||||
let (drawn, chosen, moved, sized) = (owner.clone(), owner.clone(), owner.clone(), owner);
|
||||
let [drawn, chosen, moved, sized, linked, written] = [(); 6].map(|_| owner.clone());
|
||||
let layer = ToolLayer::new("layer", tool, view, pair())
|
||||
.selected(selected.iter().copied())
|
||||
.on_draw(move |kind, frame, _, cx| {
|
||||
@@ -84,7 +84,9 @@ fn layer(tool: Tool, selected: &[&'static str], owner: Entity<Stage>) -> AnyElem
|
||||
.on_move(move |keys, by, _, cx| say(&moved, format!("move {keys:?} {by:?}"), cx))
|
||||
.on_resize(move |key, frame, _, cx| {
|
||||
say(&sized, format!("resize {key} {} {}", frame.w, frame.h), cx)
|
||||
});
|
||||
})
|
||||
.on_link(move |from, to, _, cx| say(&linked, format!("link {from} {to}"), cx))
|
||||
.on_text(move |key, text, _, cx| say(&written, format!("text {key} {text}"), cx));
|
||||
InfiniteCanvas::new("plane", view)
|
||||
.shapes(pair())
|
||||
.layer(layer)
|
||||
|
||||
Reference in new issue
Block a user