From 1ae11a4bb1c3c5f7125ab16bf7b3c1c3838266a3 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 8 Oct 2026 03:17:44 -0400 Subject: [PATCH] Shape tools: Shift and Alt pressed during the drag constrain it, as in Photoshop (#668) (#984) Shape drags only read the modifiers held at the press, so pressing Shift mid-drag (the usual way) didn't square a rectangle or keep a polygon's proportions, and Alt mid-drag didn't draw from the centre. They now follow the modifiers held at each pointer event, like the marquees. Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 4 ++-- crates/ui-egui/src/vector_ui.rs | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 260d6a41..56e959f3 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2293,7 +2293,7 @@ fn draw_drag_preview(app: &mut PhotocraftApp, painter: &egui::Painter, xf: &View trail.feed(&d.points, app.session.tools.brush.size); trail.draw(painter, doc_rect, xf.flip, col); } - t if crate::vector_ui::is_shape_tool(t) => crate::vector_ui::draw_shape_preview(app, painter, xf, t, d.start, last, d.modifiers), + t if crate::vector_ui::is_shape_tool(t) => crate::vector_ui::draw_shape_preview(app, painter, xf, t, d.start, last, d.live), Tool::RectMarquee | Tool::EllipseMarquee | Tool::ObjectSelection => { // Marching ants, visible on any pixels (#172). let (a, b) = marquee.unwrap_or((d.start, last)); @@ -2714,7 +2714,7 @@ pub(crate) fn finish_gesture(app: &mut PhotocraftApp, d: Drag) { } match d.tool { Tool::ObjectSelection => crate::retouch_ui::finish_object_selection(app, d.start, [end[0], end[1]], d.modifiers), - t if crate::vector_ui::is_shape_tool(t) => crate::vector_ui::finish_shape(app, t, d.start, [end[0], end[1]], d.modifiers), + t if crate::vector_ui::is_shape_tool(t) => crate::vector_ui::finish_shape(app, t, d.start, [end[0], end[1]], d.live), Tool::PathSelection => crate::vector_ui::path_selection_finish(app, d.start, [end[0], end[1]]), Tool::Type | Tool::VerticalType => crate::type_tool::pointer_up(app, d.start, [end[0], end[1]]), Tool::Brush | Tool::Pencil | Tool::Eraser => { diff --git a/crates/ui-egui/src/vector_ui.rs b/crates/ui-egui/src/vector_ui.rs index 61951501..f6d258a9 100644 --- a/crates/ui-egui/src/vector_ui.rs +++ b/crates/ui-egui/src/vector_ui.rs @@ -938,6 +938,30 @@ mod tests { assert_eq!(app.session.active().unwrap().doc.layers.len(), 4); } + #[test] + fn shift_pressed_during_a_shape_drag_constrains_it() { + // #668: ⇧ only counted when held at the press; Photoshop constrains when it is pressed + // mid-drag (and stops when it is released). + use crate::canvas::{ToolEvent, tool_event}; + for (tool, sides) in [(Tool::Polygon, 6), (Tool::Rectangle, 4), (Tool::EllipseShape, 0)] { + let mut app = app(); + app.ui.tool = tool; + app.ui.tool_options.polygon_sides = 6; + let none = egui::Modifiers::NONE; + tool_event(&mut app, ToolEvent::Down { x: 10.0, y: 10.0, pressure: 1.0 }, none); + tool_event(&mut app, ToolEvent::Move { x: 40.0, y: 30.0, pressure: 1.0 }, none); + tool_event(&mut app, ToolEvent::Move { x: 70.0, y: 40.0, pressure: 1.0 }, egui::Modifiers::SHIFT); + tool_event(&mut app, ToolEvent::Up { x: 70.0, y: 40.0 }, egui::Modifiers::SHIFT); + let st = app.session.active().unwrap(); + let l = st.doc.layer(st.active_layer.unwrap()).unwrap(); + assert!(matches!(l.content, LayerContent::Shape(_)), "{tool:?} ({sides} sides): a shape layer"); + let b = l.surface().unwrap().content_bounds(); + // The drag is 60 × 30; constrained, the shape's box is 60 × 60 (a hexagon is narrower + // than its box, so the height tells). + assert!(b.height().abs_diff(60) <= 1 && b.width() <= 61, "{tool:?}: ⇧ mid-drag keeps proportions: {b:?}"); + } + } + #[test] fn shape_preview_is_the_committed_path() { let mut app = app();