Global Edit: a Shift-drag from a highlighted match selects it and moves it

A Shift-press on a highlighted object of the Global Edit waits for the release
to leave it out of the edit. A drag from it now selects it when the drag starts
and moves it with the selection, as a Shift-drag from an unselected object did.
The snap targets are gathered after the object joins the selection.
This commit is contained in:
dotCooCoo
2026-11-02 01:41:48 -08:00
parent 1cd5bc4016
commit c88d88548c
3 changed files with 56 additions and 13 deletions
+5 -1
View File
@@ -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");
+23 -12
View File
@@ -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<NodeId>,
/// Shift-pressed on this highlighted object of the Global Edit: released without a drag,
/// it leaves the Global Edit (`select.globalEditExclude`).
exclude: Option<NodeId>,
/// 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<NodeId> {
/// 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<Action>) -> (Option<NodeId>, Option<NodeId>) {
/// 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<Action>) -> (Option<NodeId>, 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<NodeId>,
key: Option<NodeId>,
exclude: Option<NodeId>,
exclude: Option<(NodeId, NodeId)>,
) -> Vec<Action> {
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<Action> {
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);
+28
View File
@@ -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.