diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index ae7b63650b..aed97a4f76 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3812,14 +3812,7 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) if crate::magnetic_lasso_ui::pointer(app, ev, mods) { return; } - // ⌘ held is the Move tool (`hold_keys::cmd_moves`). The canvas resolves the held key before - // the event (`tool_override`); automation and tests send the modifier with the event. - let tool = match app.active_tool() { - t if app.tool_override.is_none() && mods.command && crate::hold_keys::cmd_moves(t) && app.ui.transform.is_none() && app.ui.text_edit.is_none() => { - Tool::Move - } - t => t, - }; + let tool = event_tool(app, mods); if tool == Tool::Eyedropper { match ev { ToolEvent::Down { x, y, .. } | ToolEvent::Move { x, y, .. } => { @@ -3864,7 +3857,7 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) let _ = app.run("layer.pickAt", json!({"x": x, "y": y, "target": target, "mode": mode})); // Snapping (and the drag's box) started before the pick: point it at what moves. if app.session.active().map(|st| st.selected_layers()) != before { - crate::snap_ui::retarget_move(app, [x, y]); + crate::snap_ui::retarget_move(app, [x, y], mods); } } // A locked layer: no drag, and Photoshop's message once the pointer moves (`move_lock`). @@ -4119,6 +4112,18 @@ pub fn selection_drag_kind(app: &PhotocraftApp, tool: Tool, p: [f64; 2], mods: e (!clicky && !mods.command && selection_mode(app, mods) == "replace").then_some(false) } +/// The tool a pointer event with `mods` goes to: ⌘ held is the Move tool (`hold_keys::cmd_moves`). +/// The canvas resolves the held key before the event (`tool_override`); automation and tests send +/// the modifier with the event. +pub(crate) fn event_tool(app: &PhotocraftApp, mods: egui::Modifiers) -> Tool { + match app.active_tool() { + t if app.tool_override.is_none() && mods.command && crate::hold_keys::cmd_moves(t) && app.ui.transform.is_none() && app.ui.text_edit.is_none() => { + Tool::Move + } + t => t, + } +} + /// Does a ⌘ (⌘⌥) press with selection tool `tool` at `p` move the whole layer (a duplicate with /// ⌥), as the Move tool would? Outside the selection or without one; inside it, ⌘ drags the /// selected pixels instead (`selection_drag_kind`). diff --git a/crates/ui-egui/src/snap_ui.rs b/crates/ui-egui/src/snap_ui.rs index edfb0e11c9..cb4d8ad71a 100644 --- a/crates/ui-egui/src/snap_ui.rs +++ b/crates/ui-egui/src/snap_ui.rs @@ -143,18 +143,21 @@ fn override_held(mods: egui::Modifiers) -> bool { /// The Move tool's press at `p` picked other layers (Auto-Select): the drag moves those, so its /// box, what it snaps and what it leaves out of the targets are theirs, not the ones selected /// before (a group or an empty layer selected before had no box at all). -pub(crate) fn retarget_move(app: &mut PhotocraftApp, p: [f64; 2]) { - begin(app, p); +pub(crate) fn retarget_move(app: &mut PhotocraftApp, p: [f64; 2], mods: egui::Modifiers) { + begin(app, p, mods); } -/// Start snapping for a drag beginning at `p` (called on pointer down). -fn begin(app: &mut PhotocraftApp, p: [f64; 2]) { +/// Start snapping for a drag beginning at `p` with `mods` held (called on pointer down). +fn begin(app: &mut PhotocraftApp, p: [f64; 2], mods: egui::Modifiers) { app.prefs_rt.snap = None; app.prefs_rt.snap_lines.clear(); if app.session.active().is_none() { return; } - let tool = app.ui.tool; + // The tool the press acts as: ⌘ makes a painting tool the Move tool, and a selection tool + // outside the selection moves the layer as the Move tool does (#2681). + let tool = crate::canvas::event_tool(app, mods); + let moves_layer = crate::canvas::command_moves_layer(app, tool, p, mods); let tol = tolerance(app); let gesture = if let Some(t) = &app.ui.transform { if t.mode == crate::state::TransformMode::Distort { @@ -170,7 +173,7 @@ fn begin(app: &mut PhotocraftApp, p: [f64; 2]) { } } else if tool == Tool::Move && crate::rulers::guide_at(app, p[0], p[1]).is_some() { Some((Gesture::Guide, Vec::new())) - } else if tool == Tool::Move { + } else if tool == Tool::Move || moves_layer { let exclude = app.session.active().map(|s| s.selected_layers()).unwrap_or_default(); moving_rect(app).map(|rect| (Gesture::Move { rect }, exclude)) } else if tool == Tool::Crop @@ -242,7 +245,7 @@ fn apply(app: &mut PhotocraftApp, p: [f64; 2]) -> [f64; 2] { pub fn filter_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) -> ToolEvent { match ev { ToolEvent::Down { x, y, pressure } => { - begin(app, [x, y]); + begin(app, [x, y], mods); // Moves snap their delta, which is zero at the start: keep the press point. let p = match app.prefs_rt.snap.as_ref().map(|s| &s.gesture) { Some(Gesture::Point) => { @@ -409,6 +412,22 @@ mod tests { assert!(app.prefs_rt.snap.is_none()); } + /// ⌘ turns a painting tool, or a marquee outside the selection, into the Move tool for the + /// drag: it gets the Move tool's box and smart-guide snapping, not the selected tool's. + #[test] + fn command_drag_temporary_move_snaps_like_the_move_tool() { + for tool in [Tool::Brush, Tool::RectMarquee] { + let mut app = app_with_box(); + app.ui.tool = tool; + let cmd = egui::Modifiers { mac_cmd: true, command: true, ..Default::default() }; + crate::canvas::tool_event(&mut app, ToolEvent::Down { x: 60.0, y: 60.0, pressure: 1.0 }, cmd); + let gesture = app.prefs_rt.snap.as_ref().map(|s| s.gesture.clone()); + assert_eq!(gesture, Some(Gesture::Move { rect: [40.0, 40.0, 90.0, 80.0] }), "{tool:?}: the mover's box"); + crate::canvas::tool_event(&mut app, ToolEvent::Up { x: 323.0, y: 100.0 }, cmd); + assert_eq!(mover_bounds(&app).x0, 300, "{tool:?}: snapped to the target's edge"); + } + } + /// The drag's box and snapping belong to the layer Auto-Select picks on the press, not to the /// one selected before: another layer (its box would move instead), or an empty layer (no box, /// no snapping at all).