diff --git a/crates/engine/src/tests/globaledit.rs b/crates/engine/src/tests/globaledit.rs index 786a351ac..60bf81a06 100644 --- a/crates/engine/src/tests/globaledit.rs +++ b/crates/engine/src/tests/globaledit.rs @@ -434,11 +434,15 @@ fn a_shift_click_on_a_highlighted_object_leaves_it_out() { let mut s = session(); let [a, b, c, ..] = squares(&mut s); start(&mut s, a, json!({})); - // A Shift-drag that starts on the copy moves the selection and leaves the copy in. + // A Shift-drag that starts on the copy selects it and moves it with the selection, and leaves + // the copy in. shift_drag(&mut s, (350.0, 50.0), (390.0, 50.0)); assert_eq!(members(&s), Some(vec![a, b, c]), "a drag leaves nothing out"); + assert_eq!(s.doc().unwrap().selection.objects, [a, b]); assert!(bounds(&s, a).x0 > 0.0, "the selection moved: {:?}", bounds(&s, a)); + assert!(bounds(&s, b).x0 > 300.0, "the copy moved with it: {:?}", bounds(&s, b)); s.execute("edit.undo", &json!({})).unwrap(); + sel(&mut s, &[a]); shift_click(&mut s, 350.0, 50.0); assert_eq!(members(&s), Some(vec![a, c]), "the copy is left out"); assert_eq!(s.doc().unwrap().selection.objects, [a], "and not selected"); diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 36aca6092..f2f4229de 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -74,9 +74,11 @@ enum State { /// guides: released without a drag, it becomes the key object that Align aligns to if /// `can_be_key` allows it (the key again: no key). key: Option, - /// Shift-pressed on this highlighted object of the Global Edit: released without a drag, - /// it leaves the Global Edit (`select.globalEditExclude`). - exclude: Option, + /// Shift-pressed on this highlighted object of the Global Edit, and the object the press + /// hit: released without a drag, the first leaves the Global Edit + /// (`select.globalEditExclude`); a drag selects the second and moves it with the + /// selection. + exclude: Option<(NodeId, NodeId)>, }, Scaling { handle: Handle, @@ -755,8 +757,8 @@ impl Tool for SelectionTool { } } } - (PointerKind::Drag, State::Moving { start, press, hold, began, .. }) => { - self.drag_moving(cx, p, m, ev.backquote, (start, press, hold, began)) + (PointerKind::Drag, State::Moving { start, press, hold, began, exclude, .. }) => { + self.drag_moving(cx, p, m, ev.backquote, (start, press, hold, began), exclude) } (PointerKind::Drag, State::Scaling { handle, bx, areas }) => { self.last_backquote = ev.backquote; @@ -979,12 +981,12 @@ fn object_behind(cx: &ToolContext, p: Point) -> Option { /// A Shift-press on `top` (hit by `h`): a Shift-click takes a selected object out of the selection /// when it is released, and a Shift-drag moves the selection, constrained. On an object of the -/// Global Edit that is highlighted, the release leaves that object out of the edit instead. -/// Returns the object to deselect and the object to leave out on release; adds to `out` the -/// action that selects `top` at once. -fn shift_press(cx: &ToolContext, h: &Hit, top: NodeId, out: &mut Vec) -> (Option, Option) { +/// Global Edit that is highlighted, the release leaves that object out of the edit instead, and a +/// drag selects `top` when it starts. Returns the object to deselect and the object to leave out +/// on release (with `top`); adds to `out` the action that selects `top` at once. +fn shift_press(cx: &ToolContext, h: &Hit, top: NodeId, out: &mut Vec) -> (Option, Option<(NodeId, NodeId)>) { if let Some(member) = global_edit_member(cx, h) { - (None, Some(member)) + (None, Some((member, top))) } else if cx.selection.contains(top) { (Some(top), None) } else { @@ -1088,14 +1090,14 @@ impl SelectionTool { began: bool, deselect: Option, key: Option, - exclude: Option, + exclude: Option<(NodeId, NodeId)>, ) -> Vec { self.state = State::Idle; self.measure = None; self.guides.clear(); self.moving = None; // A Shift-click (no drag) on a highlighted object of the Global Edit leaves it out. - if let (false, Some(id)) = (began, exclude) { + if let (false, Some((id, _))) = (began, exclude) { self.last_move = None; return vec![Action::Exec("select.globalEditExclude".into(), json!({ "id": id.0 }))]; } @@ -1305,13 +1307,22 @@ impl SelectionTool { m: Mods, backquote: bool, (start, press, hold, began): (Point, Point, Point, bool), + exclude: Option<(NodeId, NodeId)>, ) -> Vec { let mut out = vec![]; if !began { if p.distance(press) < Self::drag_threshold(cx) { 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 }))); + } 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); diff --git a/crates/tools/src/select/tests.rs b/crates/tools/src/select/tests.rs index 4b41a8592..4555eee7a 100644 --- a/crates/tools/src/select/tests.rs +++ b/crates/tools/src/select/tests.rs @@ -1004,6 +1004,34 @@ fn shift_click_toggles_an_object_on_release() { assert!(t.pointer(&cx2, &shift(PointerKind::Up, 150.0, 150.0)).is_empty()); } +/// A Shift-drag that starts on a highlighted object of the Global Edit selects that object and +/// moves it; only a Shift-click leaves it out of the edit. +#[test] +fn shift_drag_on_a_global_edit_match_selects_and_moves_it() { + 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, ..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 mut actions = t.pointer(&cx, &shift(PointerKind::Drag, 390.0, 150.0)); + assert_eq!(actions[..2], [Action::Exec("select.toggle".into(), json!({"id": b.0})), Action::Begin("Move".into())]); + actions.extend(t.pointer(&cx, &shift(PointerKind::Drag, 395.0, 150.0))); + actions.extend(t.pointer(&cx, &shift(PointerKind::Up, 395.0, 150.0))); + assert!( + actions.iter().any(|a| matches!(a, Action::Preview(c, v) if c == "object.transform" && v["matrix"][4].as_f64().is_some_and(|x| x > 39.0))), + "moved right: {actions:?}" + ); + assert_eq!(actions.last(), Some(&Action::Commit)); + assert!(!actions.iter().any(|a| matches!(a, Action::Exec(c, _) if c == "select.globalEditExclude")), "{actions:?}"); + // A Shift-click on it leaves it out instead. + let mut t = SelectionTool::default(); + assert!(t.pointer(&cx, &shift(PointerKind::Down, 350.0, 150.0)).is_empty()); + assert_eq!(t.pointer(&cx, &shift(PointerKind::Up, 350.0, 150.0)), vec![Action::Exec("select.globalEditExclude".into(), json!({"id": b.0}))]); +} + /// #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.