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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 08:22:54 -04:00
1 parent 5d58ee4577
commit f3822a8d98
28 files changed
+3068 -240

No files matched your search

+25 -2
View File
@@ -3,7 +3,7 @@ use std::rc::Rc;
use gpui::{App, Bounds, Entity, Pixels, SharedString, Window};
use super::{
edit::{Handle, drawn, inside, resized, snapped},
edit::{Handle, drawn, hit, inside, resized, snapped},
shape::{Shape, ShapeKind},
view::{Frame, Viewport},
};
@@ -12,6 +12,8 @@ pub(super) type OnKeys = Rc<dyn Fn(&[SharedString], &mut Window, &mut App)>;
pub(super) type OnDraw = Rc<dyn Fn(ShapeKind, Frame, &mut Window, &mut App)>;
pub(super) type OnMove = Rc<dyn Fn(&[SharedString], (f32, f32), &mut Window, &mut App)>;
pub(super) type OnResize = Rc<dyn Fn(&SharedString, Frame, &mut Window, &mut App)>;
/// A handler for two keys, or a key and its words.
pub(crate) type OnPair = Rc<dyn Fn(&SharedString, &SharedString, &mut Window, &mut App)>;
/// A brush's width in canvas units and its hue among the chart colors.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -47,15 +49,20 @@ pub(super) enum Gesture {
Stroke {
points: Vec<(f32, f32)>,
},
Linking {
from: SharedString,
to: (f32, f32),
},
}
/// The layer's own state: the gesture under way, whether Shift is held, the box it covers, and the pen's points so far.
/// The layer's own state: the gesture under way, whether Shift is held, the box it covers, the pen's points so far, and the shape being written on.
#[derive(Default)]
pub(super) struct Hand {
pub(super) gesture: Gesture,
pub(super) square: bool,
pub(super) bounds: Bounds<Pixels>,
pub(super) pen: Vec<(f32, f32)>,
pub(super) writing: Option<SharedString>,
}
/// Shapes a small nudge still counts as a click by, in view pixels.
@@ -109,6 +116,8 @@ pub(super) struct Handlers {
pub(super) on_draw: Option<OnDraw>,
pub(super) on_move: Option<OnMove>,
pub(super) on_resize: Option<OnResize>,
pub(super) on_link: Option<OnPair>,
pub(super) on_text: Option<OnPair>,
}
/// What a gesture ends against: the shapes, the selection, the frames to snap to, and the view.
@@ -181,6 +190,20 @@ pub(super) fn commit(
on_draw(kind, frame, window, cx);
}
}
Gesture::Linking { from, to } => {
let Some(ix) = hit(&scene.shapes, to, REACH / scene.view.zoom) else {
log::info!("tool layer: the link from {from} lands on nothing");
return;
};
let target = &scene.shapes[ix].key;
if *target == from {
return;
}
log::info!("tool layer: link {from} to {target}");
if let Some(on_link) = &handlers.on_link {
on_link(&from, target, window, cx);
}
}
Gesture::Stroke { points } if points.len() > 1 => {
let (kind, frame) = path(&points, brush.size);
log::info!("tool layer: a stroke of {} points", points.len());