Files
Ely-GPUI-Components/examples/gallery/pages/canvas/graphs.rs
T
ZacharyZhang-NY 4498ff09dc 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.
2026-09-27 08:22:54 -04:00

145 lines
5.3 KiB
Rust

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,
),
))
}