Files
Ely-GPUI-Components/src/canvas/tools.rs
T
ZacharyZhang-NY c474a3eb33 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.
2026-09-27 07:27:27 -04:00

409 lines
16 KiB
Rust

use std::rc::Rc;
use gpui::{
App, AppContext as _, Bounds, ElementId, EmptyView, Entity, EntityId, InteractiveElement,
IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString,
StatefulInteractiveElement, Styled, Window, canvas, div, prelude::*,
};
use super::{
edit::{Tool, drawn, hit, resized, snapped},
gesture::{
Brush, Gesture, Hand, Handlers, LEAST, OnDraw, OnKeys, OnMove, OnResize, PEN, REACH, Scene,
commit, path, spanned, stroke_shape, taken,
},
marks::{SelectionBox, SnapIndicator, TransformHandles, handle_boxes},
plane::{in_view, paint_shape},
shape::{Shape, ShapeKind},
view::{Frame, Viewport},
};
use crate::theme::ActiveTheme;
/// A drag on a tool layer, and whose layer it is.
struct Tracing {
owner: EntityId,
}
/// A layer over an InfiniteCanvas where its tools work. Select presses a shape, Shift adds to the selection, a drag moves it and snaps its edges to others, a handle resizes it, and a drag on empty space draws a SelectionBox. The shape tools draw by a drag, 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. A drag may end anywhere, in the layer or out of it. Everything asks the owner; the hand leaves the canvas to pan.
#[derive(IntoElement)]
pub struct ToolLayer {
id: ElementId,
tool: Tool,
viewport: Viewport,
shapes: Vec<Shape>,
selected: Vec<SharedString>,
brush: Brush,
on_select: Option<OnKeys>,
on_draw: Option<OnDraw>,
on_move: Option<OnMove>,
on_resize: Option<OnResize>,
}
impl ToolLayer {
pub fn new(
id: impl Into<ElementId>,
tool: Tool,
viewport: Viewport,
shapes: impl IntoIterator<Item = Shape>,
) -> Self {
Self {
id: id.into(),
tool,
viewport,
shapes: shapes.into_iter().collect(),
selected: Vec::new(),
brush: Brush { size: 4.0, hue: 0 },
on_select: None,
on_draw: None,
on_move: None,
on_resize: None,
}
}
pub fn selected(mut self, keys: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
self.selected = keys.into_iter().map(Into::into).collect();
self
}
pub fn brush(mut self, brush: Brush) -> Self {
self.brush = brush;
self
}
/// Gets the keys selected after each press or marquee.
pub fn on_select(
mut self,
handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
/// Gets a new shape's kind and frame, for the owner to key and keep.
pub fn on_draw(
mut self,
handler: impl Fn(ShapeKind, Frame, &mut Window, &mut App) + 'static,
) -> Self {
self.on_draw = Some(Rc::new(handler));
self
}
/// Gets the selection's keys and how far it moved, snaps included.
pub fn on_move(
mut self,
handler: impl Fn(&[SharedString], (f32, f32), &mut Window, &mut App) + 'static,
) -> Self {
self.on_move = Some(Rc::new(handler));
self
}
/// Gets a resized shape's key and new frame.
pub fn on_resize(
mut self,
handler: impl Fn(&SharedString, Frame, &mut Window, &mut App) + 'static,
) -> Self {
self.on_resize = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ToolLayer {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let hand: Entity<Hand> =
window.use_keyed_state((self.id.clone(), "hand"), cx, |_, _| Hand::default());
if self.tool == Tool::Hand {
return div().into_any_element();
}
let (view, tool) = (self.viewport, self.tool);
let shapes = Rc::new(self.shapes);
let selected = Rc::new(self.selected);
let others: Vec<Frame> = shapes
.iter()
.filter(|shape| !selected.contains(&shape.key))
.map(|shape| shape.frame)
.collect();
let whole = spanned(&shapes, &selected);
let state = hand.read(cx);
let gesture = state.gesture.clone();
let square = state.square;
let pen = state.pen.clone();
let theme = cx.theme();
let (palette, stroke) = (
theme.colors.clone(),
theme.canvas().stroke.to_pixels(window.rem_size()),
);
let side = f32::from(theme.canvas().handle.to_pixels(window.rem_size()));
let (nudge, guides) = match &gesture {
Gesture::Moving { from, to } => {
let moved = whole.map(|frame| {
Frame::new(
frame.x + to.0 - from.0,
frame.y + to.1 - from.1,
frame.w,
frame.h,
)
});
moved.map_or(((0.0, 0.0), Vec::new()), |moved| {
snapped(&moved, &others, REACH / view.zoom)
})
}
_ => ((0.0, 0.0), Vec::new()),
};
let preview: Vec<Shape> = match &gesture {
Gesture::Drawing { kind, from, to } => {
let (kind, frame) = drawn(kind.clone(), *from, *to, square);
vec![Shape::new("+draft", "Draft", kind, frame)]
}
Gesture::Moving { from, to } => shapes
.iter()
.filter(|shape| selected.contains(&shape.key))
.map(|shape| {
let mut moved = shape.clone();
moved.frame.x += to.0 - from.0 + nudge.0;
moved.frame.y += to.1 - from.1 + nudge.1;
moved
})
.collect(),
Gesture::Resizing {
key,
handle,
from,
to,
} => shapes
.iter()
.filter(|shape| shape.key == *key)
.map(|shape| {
let mut sized = shape.clone();
sized.frame = resized(
shape.frame,
*handle,
(to.0 - from.0, to.1 - from.1),
LEAST,
square,
);
sized
})
.collect(),
Gesture::Stroke { points } if points.len() > 1 => {
vec![stroke_shape(points, self.brush.size, self.brush.hue)]
}
_ if pen.len() > 1 => vec![stroke_shape(&pen, PEN, self.brush.hue)],
_ => Vec::new(),
};
let marquee = match &gesture {
Gesture::Marquee { from, to } => Some(SelectionBox::new(in_view(
&view,
&Frame::spanning(*from, *to),
))),
_ => None,
};
let handles = (tool == Tool::Select && matches!(gesture, Gesture::Idle))
.then_some(whole)
.flatten()
.map(|frame| TransformHandles::new(in_view(&view, &frame)));
let snap = (!guides.is_empty()).then(|| SnapIndicator::new(guides.clone(), view));
let point_of = move |at: Point<Pixels>, bounds: Bounds<Pixels>| {
let local = at - bounds.origin;
view.to_canvas((f32::from(local.x), f32::from(local.y)))
};
let owner = hand.entity_id();
let (pressed, moved, dropped, released) =
(hand.clone(), hand.clone(), hand.clone(), hand.clone());
let (press_shapes, press_selected) = (shapes.clone(), selected.clone());
let (select_on_press, draw_on_press, select_on_up) = (
self.on_select.clone(),
self.on_draw.clone(),
self.on_select.clone(),
);
let handlers = Handlers {
on_select: self.on_select,
on_draw: self.on_draw,
on_move: self.on_move,
on_resize: self.on_resize,
};
let scene = Rc::new(Scene {
shapes,
selected,
others,
view,
});
let handlers = Rc::new(handlers);
let brush = self.brush;
let (out_hand, out_scene, out_handlers) = (hand.clone(), scene.clone(), handlers.clone());
let measured = hand.clone();
div()
.id(self.id)
.absolute()
.inset_0()
.when(tool != Tool::Select, |layer| layer.cursor_crosshair())
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
cx.stop_propagation();
let bounds = pressed.read(cx).bounds;
let at = point_of(event.position, bounds);
let local = event.position - bounds.origin;
let view_at = (f32::from(local.x), f32::from(local.y));
let gesture = match tool {
Tool::Select => {
let held = (press_selected.len() == 1)
.then(|| spanned(&press_shapes, &press_selected))
.flatten()
.and_then(|frame| {
handle_boxes(&in_view(&view, &frame), side)
.into_iter()
.find(|(_, square)| square.contains(view_at))
});
match (held, hit(&press_shapes, at, REACH / view.zoom)) {
(Some((handle, _)), _) => Gesture::Resizing {
key: press_selected[0].clone(),
handle,
from: at,
to: at,
},
(None, Some(ix)) => {
let key = press_shapes[ix].key.clone();
let next: Vec<SharedString> =
match (event.modifiers.shift, press_selected.contains(&key)) {
(true, true) => press_selected
.iter()
.filter(|each| **each != key)
.cloned()
.collect(),
(true, false) => {
press_selected.iter().cloned().chain([key]).collect()
}
(false, true) => press_selected.to_vec(),
(false, false) => vec![key],
};
if next != *press_selected {
log::info!("tool layer: select {next:?}");
if let Some(on_select) = &select_on_press {
on_select(&next, window, cx);
}
}
Gesture::Moving { from: at, to: at }
}
(None, None) => Gesture::Marquee { from: at, to: at },
}
}
Tool::Pen => {
let ends = event.click_count >= 2;
let points = pressed.update(cx, |hand, _| {
if !ends {
hand.pen.push(at);
}
match ends {
true => std::mem::take(&mut hand.pen),
false => Vec::new(),
}
});
if ends && points.len() > 1 {
let (kind, frame) = path(&points, PEN);
log::info!("tool layer: a path of {} points", points.len());
if let Some(on_draw) = &draw_on_press {
on_draw(kind, frame, window, cx);
}
}
Gesture::Idle
}
Tool::Text => {
log::info!("tool layer: text at {at:?}");
if let Some(on_draw) = &draw_on_press {
on_draw(
ShapeKind::Text("Text".into()),
Frame::new(at.0, at.1, 160.0, 32.0),
window,
cx,
);
}
Gesture::Idle
}
Tool::Brush => Gesture::Stroke { points: vec![at] },
drawer => Gesture::Drawing {
kind: drawer.draws().expect("the rest draw shapes"),
from: at,
to: at,
},
};
pressed.update(cx, |hand, cx| {
hand.gesture = gesture;
hand.square = event.modifiers.shift;
cx.notify();
});
})
.on_drag(Tracing { owner }, |_, _, _, cx| cx.new(|_| EmptyView))
.on_drag_move::<Tracing>(move |event, _, cx| {
if event.drag(cx).owner != owner {
return;
}
let bounds = moved.read(cx).bounds;
let at = point_of(event.event.position, bounds);
moved.update(cx, |hand, cx| {
hand.square = event.event.modifiers.shift;
match &mut hand.gesture {
Gesture::Marquee { to, .. }
| Gesture::Moving { to, .. }
| Gesture::Resizing { to, .. }
| Gesture::Drawing { to, .. } => *to = at,
Gesture::Stroke { points } => points.push(at),
Gesture::Idle => {}
}
cx.notify();
});
})
.on_drop(move |_: &Tracing, window, cx| {
let (gesture, square) = taken(&dropped, cx);
commit(gesture, square, &scene, &handlers, brush, window, cx);
})
.on_mouse_up_out(MouseButton::Left, move |_, window, cx| {
let (gesture, square) = taken(&out_hand, cx);
commit(
gesture,
square,
&out_scene,
&out_handlers,
brush,
window,
cx,
);
})
.on_mouse_up(MouseButton::Left, move |_, window, cx| {
if cx.has_active_drag() {
return;
}
let gesture = released.update(cx, |hand, cx| {
cx.notify();
std::mem::take(&mut hand.gesture)
});
if matches!(gesture, Gesture::Marquee { .. }) {
log::info!("tool layer: nothing selected");
if let Some(on_select) = &select_on_up {
on_select(&[], window, cx);
}
}
})
.child(
canvas(
move |bounds, window, cx| {
if measured.read(cx).bounds != bounds {
measured.update(cx, |hand, _| hand.bounds = bounds);
window.request_animation_frame();
}
},
move |bounds, _, window, _| {
for shape in &preview {
paint_shape(shape, &view, bounds.origin, &palette, stroke, window);
}
},
)
.absolute()
.top_0()
.left_0()
.size_full(),
)
.children(marquee)
.children(handles)
.children(snap)
.into_any_element()
}
}