Canvas · Tools: select, move and snap, resize, draw, pen, text, brush

ToolLayer lies over an InfiniteCanvas and carries its tools. Select
takes the topmost shape a press reaches, Shift adds or drops one, a
drag moves the selection and snaps its nearest edge or middle to
another shape's within reach, a handle resizes it, Shift keeping a
corner's shape, and a drag on empty space draws a SelectionBox that
takes what lies wholly inside. The shape tools draw by a drag toward
any corner, Shift keeping them square; the pen adds a point per press
and a double press ends the path; text drops a line where pressed; the
brush strokes a path its width. A drag may end in the layer or out of
it; each gesture ends once and asks the owner. SelectionBox,
TransformHandles and SnapIndicator draw the marks.

ToolPalette picks a tool from icons named in tips; BrushSettings sets a
brush's width and hue; HistoryPanel lists the steps done, the undone
ones quiet, and takes the drawing to a step on Enter or a double press.

The model changes: lines and arrows end at a named corner, so they run
either way, and paths carry their width. Three Lucide icons: hexagon,
slash and move-up-right.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 07:27:27 -04:00
1 parent 50259aff77
commit c474a3eb33
20 files changed
+1812 -34

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
use std::rc::Rc;
use gpui::{App, Bounds, Entity, Pixels, SharedString, Window};
use super::{
edit::{Handle, drawn, inside, resized, snapped},
shape::{Shape, ShapeKind},
view::{Frame, Viewport},
};
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 brush's width in canvas units and its hue among the chart colors.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Brush {
pub size: f32,
pub hue: usize,
}
/// What a press set going, with its canvas points.
#[derive(Clone, Debug, Default, PartialEq)]
pub(super) enum Gesture {
#[default]
Idle,
Marquee {
from: (f32, f32),
to: (f32, f32),
},
Moving {
from: (f32, f32),
to: (f32, f32),
},
Resizing {
key: SharedString,
handle: Handle,
from: (f32, f32),
to: (f32, f32),
},
Drawing {
kind: ShapeKind,
from: (f32, f32),
to: (f32, f32),
},
Stroke {
points: Vec<(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.
#[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)>,
}
/// Shapes a small nudge still counts as a click by, in view pixels.
pub(super) const REACH: f32 = 4.0;
/// The least a drawn or resized shape spans, in canvas units.
pub(super) const LEAST: f32 = 2.0;
/// A pen path's width, in canvas units.
pub(super) const PEN: f32 = 2.0;
/// The frame the selected shapes span together.
pub(super) fn spanned(shapes: &[Shape], selected: &[SharedString]) -> Option<Frame> {
shapes
.iter()
.filter(|shape| selected.contains(&shape.key))
.map(|shape| shape.frame)
.reduce(|whole, frame| whole.union(&frame))
}
/// A path through canvas points, `width` wide: its kind and its frame, the points counted from the frame's corner.
pub(super) fn path(points: &[(f32, f32)], width: f32) -> (ShapeKind, Frame) {
let frame = points
.iter()
.map(|(x, y)| Frame::new(*x, *y, 0.0, 0.0))
.reduce(|whole, frame| whole.union(&frame))
.expect("a path has points");
let points = points
.iter()
.map(|(x, y)| (x - frame.x, y - frame.y))
.collect();
(ShapeKind::Path { points, width }, frame)
}
pub(super) fn stroke_shape(points: &[(f32, f32)], width: f32, hue: usize) -> Shape {
let (kind, frame) = path(points, width);
Shape::new("+stroke", "Stroke", kind, frame).hue(hue)
}
/// The gesture under way, taken so it ends once, with whether Shift was held.
pub(super) fn taken(hand: &Entity<Hand>, cx: &mut App) -> (Gesture, bool) {
hand.update(cx, |hand, cx| {
cx.notify();
(std::mem::take(&mut hand.gesture), hand.square)
})
}
/// The owner's handlers for what a gesture ends in.
pub(super) struct Handlers {
pub(super) on_select: Option<OnKeys>,
pub(super) on_draw: Option<OnDraw>,
pub(super) on_move: Option<OnMove>,
pub(super) on_resize: Option<OnResize>,
}
/// What a gesture ends against: the shapes, the selection, the frames to snap to, and the view.
pub(super) struct Scene {
pub(super) shapes: Rc<Vec<Shape>>,
pub(super) selected: Rc<Vec<SharedString>>,
pub(super) others: Vec<Frame>,
pub(super) view: Viewport,
}
/// Hands a finished gesture to the owner.
pub(super) fn commit(
gesture: Gesture,
square: bool,
scene: &Scene,
handlers: &Handlers,
brush: Brush,
window: &mut Window,
cx: &mut App,
) {
match gesture {
Gesture::Marquee { from, to } => {
let keys = inside(&scene.shapes, &Frame::spanning(from, to));
log::info!("tool layer: marquee takes {keys:?}");
if let Some(on_select) = &handlers.on_select {
on_select(&keys, window, cx);
}
}
Gesture::Moving { from, to } => {
let Some(whole) = spanned(&scene.shapes, &scene.selected) else {
return;
};
let raw = (to.0 - from.0, to.1 - from.1);
let moved = Frame::new(whole.x + raw.0, whole.y + raw.1, whole.w, whole.h);
let (nudge, _) = snapped(&moved, &scene.others, REACH / scene.view.zoom);
let by = (raw.0 + nudge.0, raw.1 + nudge.1);
if by == (0.0, 0.0) {
return;
}
log::info!("tool layer: move {:?} by {by:?}", scene.selected);
if let Some(on_move) = &handlers.on_move {
on_move(&scene.selected, by, window, cx);
}
}
Gesture::Resizing {
key,
handle,
from,
to,
} => {
let frame = scene
.shapes
.iter()
.find(|shape| shape.key == key)
.expect("the resized shape is listed")
.frame;
let next = resized(frame, handle, (to.0 - from.0, to.1 - from.1), LEAST, square);
log::info!("tool layer: resize {key} to {next:?}");
if let Some(on_resize) = &handlers.on_resize {
on_resize(&key, next, window, cx);
}
}
Gesture::Drawing { kind, from, to } => {
let (kind, frame) = drawn(kind, from, to, square);
if frame.w.max(frame.h) < LEAST {
return;
}
log::info!("tool layer: draw {kind:?} at {frame:?}");
if let Some(on_draw) = &handlers.on_draw {
on_draw(kind, frame, 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());
if let Some(on_draw) = &handlers.on_draw {
on_draw(kind, frame, window, cx);
}
}
_ => {}
}
}