From a5c015359608fa26fc40d75397c2f061d4396f3a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 02:15:14 -0800 Subject: [PATCH] Global Edit: a Shift-drag from a highlighted match snaps from its first move The snap targets of a Shift-drag that starts on a highlighted Global Edit match are gathered when the drag begins, for the selection with the match added, before the first preview moves it. The first preview is now snapped to the grid or to points, and later previews snap from the original position. --- crates/tools/src/select.rs | 18 ++++++++++-------- crates/tools/src/select/tests.rs | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index f2f4229de..cac9e6adf 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -1315,15 +1315,17 @@ impl SelectionTool { return out; } // A Shift-drag from a highlighted object of the Global Edit selects it and moves it - // with the selection. `cx` still holds the selection without it, so the snap targets - // are gathered at the next drag event. - if let Some((_, top)) = exclude { - out.push(Action::Exec("select.toggle".into(), json!({ "id": top.0 }))); - } + // with the selection: its snap targets are gathered for the selection with it. + self.moving = Some(match exclude { + Some((_, top)) => { + out.push(Action::Exec("select.toggle".into(), json!({ "id": top.0 }))); + let mut selection = cx.selection.clone(); + selection.add(top); + MoveSnap::new(&ToolContext { selection: &selection, ..*cx }) + } + None => MoveSnap::new(cx), + }); out.push(Action::Begin(if m.alt { "Copy".into() } else { "Move".into() })); - self.moving = exclude.is_none().then(|| MoveSnap::new(cx)); - } else if self.moving.is_none() { - self.moving = Some(MoveSnap::new(cx)); } let mut d = move_delta(start, p, m.shift, cx.constrain_angle); let unsnapped = d; diff --git a/crates/tools/src/select/tests.rs b/crates/tools/src/select/tests.rs index 4555eee7a..8f6299f06 100644 --- a/crates/tools/src/select/tests.rs +++ b/crates/tools/src/select/tests.rs @@ -1032,6 +1032,25 @@ fn shift_drag_on_a_global_edit_match_selects_and_moves_it() { assert_eq!(t.pointer(&cx, &shift(PointerKind::Up, 350.0, 150.0)), vec![Action::Exec("select.globalEditExclude".into(), json!({"id": b.0}))]); } +/// The first preview of a Shift-drag from a Global Edit match snaps, the match's bounds taken in +/// with the selection's: with Snap to Grid, a move of 13 right brings the right edge (413) onto +/// the gridline 414. +#[test] +fn shift_drag_on_a_global_edit_match_snaps_from_its_first_event() { + let (d, a, b) = two_rects(Rect::new(300.0, 100.0, 400.0, 200.0)); + let p = paint(); + let s = Selection::default(); + let members = [(a, Affine::IDENTITY), (b, Affine::IDENTITY)]; + let cx = ToolContext { global_edit: &members, snap_to_grid: true, ..cx(&d, &s, &p) }; + let shift = |kind, x, y| ev(kind, x, y).with_mods(Mods { shift: true, ..Mods::default() }); + let mut t = SelectionTool::default(); + assert!(t.pointer(&cx, &shift(PointerKind::Down, 350.0, 150.0)).is_empty()); + let actions = t.pointer(&cx, &shift(PointerKind::Drag, 363.0, 150.0)); + let Some(Action::Preview(_, v)) = actions.get(2) else { panic!("{actions:?}") }; + let m = Vec2::new(v["matrix"][4].as_f64().unwrap(), v["matrix"][5].as_f64().unwrap()); + assert!(moved_by(m, (14.0, 0.0)), "{m:?}"); +} + /// #541: a click on one object of a selection of several makes it the key object; a click on /// the key again lets it go. A drag moves them all, a lone object has no key unless ruler /// guides are selected with it.