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,240 @@
|
||||
use ely_gpui_component::{
|
||||
canvas::{
|
||||
FlowChart, Frame, Link, MindMap, Step, StepKind, Tool, ToolLayer, ToolPalette, Topic,
|
||||
Viewport,
|
||||
},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section},
|
||||
};
|
||||
|
||||
/// The flow demo's steps, links, view, tool and selection.
|
||||
struct Flow {
|
||||
steps: Vec<Step>,
|
||||
links: Vec<Link>,
|
||||
view: Viewport,
|
||||
tool: Tool,
|
||||
selected: Vec<SharedString>,
|
||||
}
|
||||
|
||||
impl Flow {
|
||||
fn new() -> Self {
|
||||
let step = |key, kind, text, x, y, w| Step::new(key, kind, text, Frame::new(x, y, w, 56.0));
|
||||
Self {
|
||||
steps: vec![
|
||||
step("start", StepKind::Terminal, "Start", 20.0, 132.0, 110.0),
|
||||
step(
|
||||
"draft",
|
||||
StepKind::Process,
|
||||
"Write the draft",
|
||||
180.0,
|
||||
132.0,
|
||||
150.0,
|
||||
),
|
||||
step(
|
||||
"check",
|
||||
StepKind::Decision,
|
||||
"Reviewed?",
|
||||
380.0,
|
||||
122.0,
|
||||
140.0,
|
||||
),
|
||||
step("ship", StepKind::Process, "Publish", 580.0, 132.0, 120.0),
|
||||
step("end", StepKind::Terminal, "End", 580.0, 250.0, 120.0),
|
||||
step(
|
||||
"revise",
|
||||
StepKind::Process,
|
||||
"Ask for changes",
|
||||
370.0,
|
||||
250.0,
|
||||
160.0,
|
||||
),
|
||||
],
|
||||
links: vec![
|
||||
Link::new("l1", "start", "draft"),
|
||||
Link::new("l2", "draft", "check"),
|
||||
Link::new("l3", "check", "ship").label("Yes"),
|
||||
Link::new("l4", "check", "revise").label("No"),
|
||||
Link::new("l5", "ship", "end"),
|
||||
],
|
||||
view: Viewport::new(0.0, 0.0, 0.8),
|
||||
tool: Tool::Select,
|
||||
selected: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn boxed(width: f32, height: f32, child: impl IntoElement, cx: &App) -> gpui::Div {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.w(px(width))
|
||||
.h(px(height))
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.overflow_hidden()
|
||||
.child(child)
|
||||
}
|
||||
|
||||
pub fn flow(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("canvas-flow", Flow::new, window, cx);
|
||||
let now = state.read(cx);
|
||||
let (steps, links, view, tool, selected) = (
|
||||
now.steps.clone(),
|
||||
now.links.clone(),
|
||||
now.view,
|
||||
now.tool,
|
||||
now.selected.clone(),
|
||||
);
|
||||
let [picked, chosen, moved, sized, linked, written, viewed] = [(); 7].map(|_| state.clone());
|
||||
let at = |steps: &mut Vec<Step>, key: &SharedString| {
|
||||
steps
|
||||
.iter()
|
||||
.position(|step| step.key == *key)
|
||||
.expect("a step of the flow")
|
||||
};
|
||||
let layer = ToolLayer::new(
|
||||
"canvas-flow-tools",
|
||||
tool,
|
||||
view,
|
||||
steps.iter().map(Step::shape),
|
||||
)
|
||||
.selected(selected)
|
||||
.on_select(move |keys, _, cx| change(&chosen, cx, |flow| flow.selected = keys.to_vec()))
|
||||
.on_move(move |keys, (dx, dy), _, cx| {
|
||||
change(&moved, cx, |flow| {
|
||||
for step in flow
|
||||
.steps
|
||||
.iter_mut()
|
||||
.filter(|step| keys.contains(&step.key))
|
||||
{
|
||||
step.frame = Frame::new(
|
||||
step.frame.x + dx,
|
||||
step.frame.y + dy,
|
||||
step.frame.w,
|
||||
step.frame.h,
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
.on_resize(move |key, frame, _, cx| {
|
||||
change(&sized, cx, |flow| {
|
||||
let ix = at(&mut flow.steps, key);
|
||||
flow.steps[ix].frame = frame;
|
||||
})
|
||||
})
|
||||
.on_link(move |from, to, _, cx| {
|
||||
change(&linked, cx, |flow| {
|
||||
let key = format!("made-{}", flow.links.len() + 1);
|
||||
flow.links.push(Link::new(key, from.clone(), to.clone()));
|
||||
})
|
||||
})
|
||||
.on_text(move |key, text, _, cx| {
|
||||
change(&written, cx, |flow| {
|
||||
let ix = at(&mut flow.steps, key);
|
||||
flow.steps[ix].text = text.clone();
|
||||
})
|
||||
});
|
||||
section(
|
||||
"FlowChart / DiagramEditor · Connector Line",
|
||||
"Steps joined by links that follow them: ends as ellipses, work as boxes, choices as diamonds, and words on a link. With Select a step moves, resizes, and takes new words on a double press; the connector draws a link from one step to another.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.child(
|
||||
ToolPalette::new("canvas-flow-palette", tool)
|
||||
.tools([Tool::Select, Tool::Connector])
|
||||
.on_change(move |next, _, cx| change(&picked, cx, |flow| flow.tool = next)),
|
||||
)
|
||||
.child(probe(
|
||||
"canvas-flow",
|
||||
boxed(
|
||||
620.0,
|
||||
300.0,
|
||||
FlowChart::new("canvas-flow-chart", view, steps, links)
|
||||
.layer(layer)
|
||||
.on_viewport(move |next, _, cx| change(&viewed, cx, |flow| flow.view = next)),
|
||||
cx,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
/// The mind map demo's topics, selection, view and count of topics made.
|
||||
struct Thinking {
|
||||
root: Topic,
|
||||
selected: Option<SharedString>,
|
||||
view: Viewport,
|
||||
made: usize,
|
||||
}
|
||||
|
||||
impl Thinking {
|
||||
fn new() -> Self {
|
||||
let topic = Topic::new;
|
||||
Self {
|
||||
root: topic("root", "Product launch")
|
||||
.child(
|
||||
topic("site", "Website")
|
||||
.child(topic("copy", "Copy"))
|
||||
.child(topic("shots", "Screenshots")),
|
||||
)
|
||||
.child(topic("press", "Press").child(topic("kit", "Press kit")))
|
||||
.child(topic("budget", "Budget"))
|
||||
.child(topic("team", "Team").child(topic("roles", "Roles"))),
|
||||
selected: Some("site".into()),
|
||||
view: Viewport::fitting(
|
||||
Frame::new(-520.0, -140.0, 1040.0, 280.0),
|
||||
(620.0, 300.0),
|
||||
16.0,
|
||||
),
|
||||
made: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mind(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("canvas-mind", Thinking::new, window, cx);
|
||||
let now = state.read(cx);
|
||||
let (root, selected, view) = (now.root.clone(), now.selected.clone(), now.view);
|
||||
let [picked, added, removed, renamed, viewed] = [(); 5].map(|_| state.clone());
|
||||
section(
|
||||
"MindMap",
|
||||
"Topics branching from a root, half to each side. A press selects; arrows walk, Tab adds a child and Enter a sibling, Delete removes one, and F2 or a double press rewrites it. The owner keeps the map.",
|
||||
cx,
|
||||
)
|
||||
.child(boxed(
|
||||
620.0,
|
||||
300.0,
|
||||
MindMap::new("canvas-mind-map", root, view)
|
||||
.selected(selected)
|
||||
.on_select(move |key, _, cx| change(&picked, cx, |mind| mind.selected = key.cloned()))
|
||||
.on_add(move |parent, _, cx| {
|
||||
change(&added, cx, |mind| {
|
||||
mind.made += 1;
|
||||
let key = SharedString::from(format!("idea-{}", mind.made));
|
||||
mind.root = mind.root.adding(parent, Topic::new(key.clone(), "New idea"));
|
||||
mind.selected = Some(key);
|
||||
})
|
||||
})
|
||||
.on_remove(move |key, _, cx| {
|
||||
change(&removed, cx, |mind| {
|
||||
mind.selected = mind.root.parent_of(key);
|
||||
mind.root = mind.root.removing(key);
|
||||
})
|
||||
})
|
||||
.on_rename(move |key, text, _, cx| {
|
||||
change(&renamed, cx, |mind| mind.root = mind.root.renaming(key, text.clone()))
|
||||
})
|
||||
.on_viewport(move |next, _, cx| change(&viewed, cx, |mind| mind.view = next)),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use ely_gpui_component::{
|
||||
canvas::{
|
||||
Board, Edge, Frame, Link, Node, NodeGraph, Port, Shape, ShapeKind, Viewport, Whiteboard,
|
||||
},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{change, keep, section},
|
||||
};
|
||||
|
||||
/// The node demo's nodes, wires, view and selection.
|
||||
struct Pipeline {
|
||||
nodes: Vec<Node>,
|
||||
edges: Vec<Edge>,
|
||||
view: Viewport,
|
||||
selected: Vec<SharedString>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
fn new() -> Self {
|
||||
let port = Port::new;
|
||||
Self {
|
||||
nodes: vec![
|
||||
Node::new("load", "Load image", (20.0, 40.0))
|
||||
.output(port("image", "Image", "image")),
|
||||
Node::new("prompt", "Prompt", (20.0, 170.0)).output(port("text", "Text", "text")),
|
||||
Node::new("model", "Model", (290.0, 60.0))
|
||||
.input(port("image", "Image", "image"))
|
||||
.input(port("text", "Prompt", "text"))
|
||||
.input(port("steps", "Steps", "number"))
|
||||
.output(port("image", "Image", "image")),
|
||||
Node::new("save", "Save image", (560.0, 90.0))
|
||||
.input(port("image", "Image", "image")),
|
||||
],
|
||||
edges: vec![
|
||||
Edge::new(("load", "image"), ("model", "image")),
|
||||
Edge::new(("prompt", "text"), ("model", "text")),
|
||||
],
|
||||
view: Viewport::new(0.0, 0.0, 0.8),
|
||||
selected: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn boxed(width: f32, height: f32, child: impl IntoElement, cx: &App) -> gpui::Div {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.w(px(width))
|
||||
.h(px(height))
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.overflow_hidden()
|
||||
.child(child)
|
||||
}
|
||||
|
||||
pub fn nodes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("canvas-nodes", Pipeline::new, window, cx);
|
||||
let now = state.read(cx);
|
||||
let (nodes, edges, view, selected) = (
|
||||
now.nodes.clone(),
|
||||
now.edges.clone(),
|
||||
now.view,
|
||||
now.selected.clone(),
|
||||
);
|
||||
let [picked, moved, joined, parted, viewed] = [(); 5].map(|_| state.clone());
|
||||
section(
|
||||
"NodeEditor / NodeGraph · Node / Port / Edge / Connection",
|
||||
"Nodes joined by wires, as in a pipeline editor. A title drags its node; a drag out of an output draws a wire that lands on an input of the same kind, each kind in its own hue. An input takes one wire, so a new one replaces the old; a press on a wired input lifts the wire to move it or let it go.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"canvas-nodes",
|
||||
boxed(
|
||||
620.0,
|
||||
300.0,
|
||||
NodeGraph::new("canvas-node-graph", view, nodes, edges)
|
||||
.selected(selected)
|
||||
.on_select(move |keys, _, cx| change(&picked, cx, |graph| graph.selected = keys.to_vec()))
|
||||
.on_move(move |key, (dx, dy), _, cx| {
|
||||
change(&moved, cx, |graph| {
|
||||
let node = graph.nodes.iter_mut().find(|node| node.key == *key).expect("a node of the graph");
|
||||
node.at = (node.at.0 + dx, node.at.1 + dy);
|
||||
})
|
||||
})
|
||||
.on_connect(move |edge, _, cx| change(&joined, cx, |graph| graph.edges.push(edge.clone())))
|
||||
.on_disconnect(move |edge, _, cx| change(&parted, cx, |graph| graph.edges.retain(|each| each != edge)))
|
||||
.on_viewport(move |next, _, cx| change(&viewed, cx, |graph| graph.view = next)),
|
||||
cx,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn first_board() -> Board {
|
||||
let note = |key: &str, words: &str, x, y, hue| {
|
||||
Shape::new(
|
||||
key.to_string(),
|
||||
"Note",
|
||||
ShapeKind::Note(words.to_string().into()),
|
||||
Frame::new(x, y, 150.0, 150.0),
|
||||
)
|
||||
.hue(hue)
|
||||
};
|
||||
Board {
|
||||
shapes: vec![
|
||||
note("why", "Why do people leave in week two?", 30.0, 80.0, 2),
|
||||
note("guess", "Setup takes too long", 230.0, 80.0, 5),
|
||||
note("try", "Try a sample project", 430.0, 80.0, 1),
|
||||
Shape::new(
|
||||
"goal",
|
||||
"Goal",
|
||||
ShapeKind::Text("Week two retention".into()),
|
||||
Frame::new(30.0, 270.0, 220.0, 32.0),
|
||||
),
|
||||
],
|
||||
links: vec![
|
||||
Link::new("because", "why", "guess"),
|
||||
Link::new("so", "guess", "try"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn board(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = keep("canvas-board", first_board, window, cx);
|
||||
let now = state.read(cx).clone();
|
||||
let kept = state.clone();
|
||||
section(
|
||||
"Whiteboard · StickyNote",
|
||||
"A board to think on, with its tools over its corner and its zoom at its foot. The note tool drops a sticky note, a double press writes on it, the connector links two notes, and Delete removes the selection. It keeps its own tool and view and hands the owner the whole board after each change.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"canvas-board",
|
||||
boxed(
|
||||
620.0,
|
||||
340.0,
|
||||
Whiteboard::new("canvas-whiteboard", now).on_change(move |next, _, cx| change(&kept, cx, |board| *board = next)),
|
||||
cx,
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use gpui::{AnyElement, App, Window, div, prelude::*};
|
||||
|
||||
mod boards;
|
||||
mod graphs;
|
||||
mod looks;
|
||||
mod panels;
|
||||
mod plane;
|
||||
@@ -47,5 +49,9 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(panels::arrange(window, cx))
|
||||
.child(looks::styles(window, cx))
|
||||
.child(looks::rules(window, cx))
|
||||
.child(graphs::nodes(window, cx))
|
||||
.child(boards::flow(window, cx))
|
||||
.child(boards::mind(window, cx))
|
||||
.child(graphs::board(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
Reference in new issue
Block a user