Fix #4890: Alt with Direct Selection marquee-selects whole paths

This commit is contained in:
bflatastic
2026-10-31 22:22:17 +07:00
parent ca0785573d
commit 427de74f37
2 changed files with 41 additions and 11 deletions
+20 -11
View File
@@ -8,7 +8,9 @@
//! Group Selection's clicks do), drag a direction handle to
//! reshape (Shift keeps it at 45° steps round its anchor, Alt moves it alone; smart guides snap
//! it), marquee to select anchors (Shift-drag toggles them: the selected ones inside are
//! deselected, the others selected), drag a corner widget of any path (a star, a pen path) to round its corners
//! deselected, the others selected; with Alt held at the press, as Group Selection for the drag,
//! it selects every object it touches, whole), drag a corner widget of any path (a star, a pen
//! path) to round its corners
//! (the selected ones when anchors are selected; Alt-click cycles their kind, double-click opens
//! the Corners dialog).
//! Group Selection: click selects the leaf; each further click on it adds the next enclosing group;
@@ -103,6 +105,9 @@ enum State {
/// Group Selection, E pressed an odd number of times during the drag: the marquee selects
/// only the objects that lie entirely inside it, whole.
enclosed: bool,
/// Direct Selection with Alt held at the press, which makes it the Group Selection tool
/// for the drag: the marquee selects every object it touches, whole (#4890).
whole: bool,
},
Corner(CornerDrag),
/// Dragging a bracket of type on a path.
@@ -756,7 +761,7 @@ impl Tool for DirectSelectionTool {
if let Some(out) = self.press_hidden(cx, ev, p) {
return out;
}
self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false };
self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false, whole: ev.mods.alt };
vec![]
}
(PointerKind::Drag, State::MoveAnchors { start, grab, began }) => self.drag_move_anchors(cx, ev, p, (start, grab, began)),
@@ -839,8 +844,8 @@ impl Tool for DirectSelectionTool {
self.state = State::Idle;
self.mesh.release(cx).unwrap_or_default()
}
(PointerKind::Drag, State::Marquee { start, toggle, enclosed, .. }) => {
self.state = State::Marquee { start, cur: p, toggle, enclosed };
(PointerKind::Drag, State::Marquee { start, toggle, enclosed, whole, .. }) => {
self.state = State::Marquee { start, cur: p, toggle, enclosed, whole };
vec![]
}
(PointerKind::Drag, State::Corner(mut c)) => {
@@ -875,7 +880,7 @@ impl Tool for DirectSelectionTool {
self.guides.clear();
vec![Action::Commit]
}
(PointerKind::Up, State::Marquee { start, toggle, enclosed, .. }) => self.release_marquee(cx, p, (start, toggle, enclosed)),
(PointerKind::Up, State::Marquee { start, toggle, enclosed, whole, .. }) => self.release_marquee(cx, p, (start, toggle, enclosed, whole)),
_ => vec![],
}
}
@@ -913,6 +918,8 @@ impl Tool for DirectSelectionTool {
match self.state {
// Alt-dragging leaves a copy behind.
State::MoveObject { began: true, .. } if m.alt => return Cursor::SelectionCopy,
// An Alt-marquee is Group Selection's until it is released.
State::Marquee { whole: true, .. } => return Cursor::GroupSelection,
// Bending a segment keeps the cursor that offered it.
State::Segment { began: true, .. } | State::SpineSegment { began: true, .. } => return Cursor::ReshapeSegment,
// A drag shows the filled arrow, hollow while it snaps to a point or a guide.
@@ -1102,7 +1109,7 @@ impl DirectSelectionTool {
if let Some(out) = self.press_hidden(cx, ev, p) {
return out;
}
self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false };
self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false, whole: false };
return vec![];
};
// A compound path's paths are picked one at a time, below the compound path (#2441).
@@ -1381,7 +1388,7 @@ impl DirectSelectionTool {
(false, None, None, None) => group.map(|g| Action::Exec("select.add".into(), json!({"ids": [g.0]}))).into_iter().collect(),
}
}
fn release_marquee(&mut self, cx: &ToolContext, p: Point, (start, toggle, enclosed): (Point, bool, bool)) -> Vec<Action> {
fn release_marquee(&mut self, cx: &ToolContext, p: Point, (start, toggle, enclosed, whole): (Point, bool, bool, bool)) -> Vec<Action> {
self.state = State::Idle;
let r = Rect::from_points(start, p);
if r.width() < cx.tol(3.0) && r.height() < cx.tol(3.0) {
@@ -1392,14 +1399,16 @@ impl DirectSelectionTool {
return vec![Action::Exec("select.none".into(), json!({}))];
}
// With Group Selection's enclosed marquee, the tool selects the objects inside it
// (members of groups too), each whole, as a click selects one.
if enclosed {
// (members of groups too), each whole, as a click selects one; with an Alt-marquee, every
// object it touches, each whole.
if enclosed || whole {
let mut touched = vectorcraft_doc::hit::marquee(cx.doc, r, cx.isolation, true);
let editable = cx.doc.editable();
touched.retain(|id| editable(*id));
let ids: Vec<Value> = crate::select::inside(cx, r, touched).into_iter().map(|id| json!(id.0)).collect();
let picked = if enclosed { crate::select::inside(cx, r, touched) } else { touched };
let ids: Vec<Value> = picked.into_iter().map(|id| json!(id.0)).collect();
let art = Action::Exec(if toggle { "select.toggle" } else { "select.set" }.into(), json!({ "ids": ids }));
return std::iter::once(art).chain(crate::rulerguide::marquee_select(cx, r, true)).collect();
return std::iter::once(art).chain(crate::rulerguide::marquee_select(cx, r, enclosed)).collect();
}
// Anchors inside the rect, on editable paths inside isolation mode.
let mut sel: Vec<(NodeId, Vec<AnchorRef>)> = vec![];
+21
View File
@@ -922,6 +922,27 @@ fn shift_marquee_toggles_anchors() {
}
}
/// #4890: Alt held at the press makes a Direct Selection marquee Group Selection's for the drag:
/// it selects each path it touches whole (with Shift, toggles it), not the anchors inside it, and
/// leaves the paths it misses; the Group Selection cursor stays until the release.
#[test]
fn an_alt_marquee_selects_the_paths_it_touches_whole() {
let (d, a, _) = two_squares();
let (s, p) = (Selection::default(), paint());
let cx = cx(&d, &s, &p);
let alt = Mods { alt: true, ..Default::default() };
let alt_shift = Mods { shift: true, ..alt };
for (mods, cmd) in [(alt, "select.set"), (alt_shift, "select.toggle")] {
let mut t = DirectSelectionTool::new(false);
// Round A's top-right corner only; B (300..400) is left out.
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0).with_mods(mods));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 150.0).with_mods(mods));
assert_eq!(t.cursor(&cx, Point::new(250.0, 150.0), mods), Cursor::GroupSelection, "{mods:?}");
let up = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0).with_mods(mods));
assert_eq!(up, vec![Action::Exec(cmd.into(), json!({"ids": [a.0]}))], "{mods:?}");
}
}
/// Group Selection: E pressed during a marquee drag makes it select the objects entirely inside
/// it whole, a group's member without its group. Direct Selection does not take E.
#[test]