diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index c27466935..c1aae51a4 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -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 { + fn release_marquee(&mut self, cx: &ToolContext, p: Point, (start, toggle, enclosed, whole): (Point, bool, bool, bool)) -> Vec { 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 = 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 = 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)> = vec![]; diff --git a/crates/tools/src/direct/tests.rs b/crates/tools/src/direct/tests.rs index cb852c553..6a7be3ecc 100644 --- a/crates/tools/src/direct/tests.rs +++ b/crates/tools/src/direct/tests.rs @@ -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]