Move tool: ⌘-drag temporary Move gets the drag box and snapping (#2681) (#2684)

snap_ui::begin chose the snap gesture from app.ui.tool, the selected tool,
not the tool the press acts as. A ⌘-drag with a painting tool (temporary
Move) or with a selection tool outside the selection (moves the layer)
got no moving box and no smart-guide/guide/grid snapping.

The ⌘ resolution in canvas::tool_event moves into canvas::event_tool,
which begin now uses together with canvas::command_moves_layer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-10 16:10:12 -04:00
1 parent 7dc8bbd84a
commit dfd63b0945
2 files changed
+40 -16

No files matched your search

+14 -9
View File
@@ -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`).
+26 -7
View File
@@ -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).