Files
Ely-GPUI-Components/src/canvas/tools.rs
T
ZacharyZhang-NYandClaude Opus 5.5 94deeaed52 Port · Build: Zed's gpui at 1a28cff
gpui and gpui_platform now come from Zed's repo by git at 1a28cff,
the code gpui-pre 0.3.7 republishes, so the gallery can later build
for the browser. gpui takes stacker, as 0.2.2 always did, and
gpui_platform takes font-kit, without which macOS draws no text;
runtime_shaders moves to gpui_platform.

The API moved: focus, blur and Tab steps take cx, Corner is Anchor,
shaped lines paint with an alignment, defer_draw takes a content
mask, the inspector registers a factory, async updates return their
value, and wrapped lines are shared, not cloned. Updates through a
window context go through the window, so a closed window still ends
them as it did.

Where the new gpui behaves differently, Ely keeps its old intent.
Holds and reveals under a resting pointer survive typing through
gpui's input-modality-independent hover; previews the keyboard also
moves keep the default, and the calendar's leave ignores a key press.
Enter reaches a button inside the code editor, since gpui now arms a
key click on the press. The event logger skips a lone modifier. A
video and an image grid keep their shape in a flex column under
taffy 0.13, as a picture does.

AGENTS.md records the new stack and the rules this port changes; the
rest of the old gpui and taffy rules are re-tested in T44c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:30:17 -04:00

469 lines
18 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, resized, snapped},
gesture::{
Brush, Gesture, Hand, Handlers, LEAST, OnDraw, OnKeys, OnMove, OnPair, OnResize, PEN,
REACH, Scene, commit, path, spanned, stroke_shape, taken,
},
marks::{SelectionBox, SnapIndicator, TransformHandles, placed},
paint::{in_view, paint_shape},
press::{Press, pressed as pressed_with},
shape::{Corner, Shape, ShapeKind},
view::{Frame, Viewport},
writing,
};
use crate::{forms::Editing, primitives::tab_stop, 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 note drops where pressed; the connector links the shape pressed to the one released on. A double press with Select writes on a shape: a text's or a note's words, or any other's label. A press focuses the layer, where Delete or Backspace asks to delete the selection and Escape lets it go. 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>,
on_link: Option<OnPair>,
on_text: Option<OnPair>,
on_delete: Option<OnKeys>,
}
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,
on_link: None,
on_text: None,
on_delete: 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
}
/// Gets the keys of two shapes the connector joined, from first.
pub fn on_link(
mut self,
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_link = Some(Rc::new(handler));
self
}
/// Gets the selected keys when Delete or Backspace is pressed on the layer.
pub fn on_delete(
mut self,
handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
) -> Self {
self.on_delete = Some(Rc::new(handler));
self
}
/// Gets a shape's key and the words written on it after a double press.
pub fn on_text(
mut self,
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_text = 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());
let editing =
window.use_keyed_state((self.id.clone(), "writing"), cx, |_, _| Editing::default());
if self.tool != Tool::Pen && !hand.read(cx).pen.is_empty() {
let points = hand.update(cx, |hand, _| std::mem::take(&mut hand.pen));
end_path(points, self.on_draw.clone(), window, cx);
}
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)]
}
Gesture::Linking { from, to } => {
let start = shapes
.iter()
.find(|shape| shape.key == *from)
.expect("a link starts on a listed shape")
.frame
.center();
let (kind, frame) = drawn(ShapeKind::Arrow(Corner::BottomRight), start, *to, false);
vec![Shape::new("+link", "Link", kind, frame)]
}
_ if tool == Tool::Pen && 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 chosen = (tool == Tool::Select && matches!(gesture, Gesture::Idle))
.then_some(whole)
.flatten()
.map(|frame| in_view(&view, &frame));
let handles = chosen
.filter(|_| selected.len() == 1)
.map(TransformHandles::new);
let outline = chosen
.filter(|_| selected.len() > 1)
.map(|frame| placed(&frame).border_1().border_color(palette.accent));
let snap = (!guides.is_empty()).then(|| SnapIndicator::new(guides.clone(), view));
let writing_on = hand.read(cx).writing.clone();
let field = writing_on
.as_ref()
.and_then(|key| shapes.iter().find(|shape| shape.key == *key))
.and_then(|shape| writing::field(&editing, in_view(&view, &shape.frame), cx));
if field.is_none() && writing_on.is_some() && editing.read(cx).field().is_some() {
let closing = editing.clone();
log::info!("tool layer: the shape written on left the canvas");
window.defer(cx, move |window, cx| {
closing.update(cx, |editing, cx| editing.finish(true, window, cx))
});
}
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 focus = tab_stop((self.id.clone(), "focus").into(), true, window, cx);
let (ring, held_focus) = (
if focus.is_focused(window) {
palette.focus
} else {
gpui::transparent_black()
},
focus.clone(),
);
let (gone, on_delete, let_go) = (
selected.clone(),
self.on_delete.clone(),
self.on_select.clone(),
);
let (pressed, moved, dropped, released) =
(hand.clone(), hand.clone(), hand.clone(), hand.clone());
let handlers = Handlers {
on_select: self.on_select,
on_draw: self.on_draw,
on_move: self.on_move,
on_resize: self.on_resize,
on_link: self.on_link,
on_text: self.on_text,
};
let scene = Rc::new(Scene {
shapes,
selected,
others,
view,
});
let handlers = Rc::new(handlers);
let select_on_up = handlers.on_select.clone();
let (press_scene, press_handlers, press_editing) =
(scene.clone(), handlers.clone(), editing.clone());
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)
.track_focus(&focus)
.absolute()
.inset_0()
.child(div().absolute().inset_0().border_1().border_color(ring))
.when(tool != Tool::Select, |layer| layer.cursor_crosshair())
.on_key_down(
move |event, window, cx| match event.keystroke.key.as_str() {
"backspace" | "delete" if !gone.is_empty() => {
cx.stop_propagation();
log::info!("tool layer: delete {gone:?}");
if let Some(on_delete) = &on_delete {
on_delete(&gone, window, cx);
}
}
"escape" if !gone.is_empty() => {
cx.stop_propagation();
log::info!("tool layer: nothing selected");
if let Some(on_select) = &let_go {
on_select(&[], window, cx);
}
}
_ => {}
},
)
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
cx.stop_propagation();
window.focus(&held_focus, cx);
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 press = Press {
tool,
at,
view_at,
count: event.click_count,
shift: event.modifiers.shift,
side,
back: held_focus.clone(),
};
let gesture = pressed_with(
&press,
&press_scene,
&press_handlers,
&pressed,
&press_editing,
window,
cx,
);
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, .. }
| Gesture::Linking { 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(outline)
.children(field)
.children(snap)
.into_any_element()
}
}
/// Ends a pen path the tool left: two points or more go to the owner after this frame, a lone point is dropped.
fn end_path(points: Vec<(f32, f32)>, on_draw: Option<OnDraw>, window: &mut Window, cx: &mut App) {
if points.len() < 2 {
log::info!("tool layer: the pen left a lone point, dropped");
return;
}
window.defer(cx, move |window, cx| {
let (kind, frame) = path(&points, PEN);
log::info!("tool layer: the pen left a path of {} points", points.len());
if let Some(on_draw) = &on_draw {
on_draw(kind, frame, window, cx);
}
});
}