diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index 4fd0df21b..d635fd291 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -1608,6 +1608,36 @@ fn pen_with_cmd_borrows_direct_selection_and_goes_on_drawing() { assert_eq!(s.tool_id(), "rectangle"); } +#[test] +fn cmd_alt_lends_group_selection_while_direct_selection_is_active() { + let (cmd, cmd_alt) = (Mods { cmd: true, ..Mods::default() }, Mods { cmd: true, alt: true, ..Mods::default() }); + let v = view(); + let mut s = session(); + // Cmd+Alt while the Pen borrows Direct Selection keeps Direct Selection (Alt is copy/subtract). + s.select_tool("directSelection", v).unwrap(); + s.select_tool("pen", v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 50.0).with_mods(cmd_alt), v).unwrap(); + assert_eq!(s.tool_id(), "directSelection"); + s.pointer(&PointerEvent::new(PointerKind::Up, 50.0, 50.0).with_mods(cmd_alt), v).unwrap(); + assert_eq!(s.tool_id(), "pen"); + // Cmd alone still lends Direct Selection. + s.pointer(&PointerEvent::new(PointerKind::Down, 60.0, 60.0).with_mods(cmd), v).unwrap(); + assert_eq!(s.tool_id(), "directSelection"); + s.pointer(&PointerEvent::new(PointerKind::Up, 60.0, 60.0).with_mods(cmd), v).unwrap(); + // While Direct Selection is active, Cmd+Alt lends Group Selection for the gesture. + s.select_tool("directSelection", v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Down, 70.0, 70.0).with_mods(cmd_alt), v).unwrap(); + assert_eq!(s.tool_id(), "groupSelection"); + s.pointer(&PointerEvent::new(PointerKind::Up, 70.0, 70.0).with_mods(cmd_alt), v).unwrap(); + assert_eq!(s.tool_id(), "directSelection"); + // Selection last: Cmd+Alt still lends Selection, not Group Selection. + s.select_tool("selection", v).unwrap(); + s.select_tool("pen", v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Down, 80.0, 80.0).with_mods(cmd_alt), v).unwrap(); + assert_eq!(s.tool_id(), "selection"); + s.pointer(&PointerEvent::new(PointerKind::Up, 80.0, 80.0).with_mods(cmd_alt), v).unwrap(); +} + #[test] fn the_pen_drops_the_new_path_asterisk_once_a_path_is_started() { // The Pen's asterisk means a click starts a new path: it shows until the first anchor is diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index 3a90d47ed..3a868a390 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -104,10 +104,18 @@ impl Session { /// The selection tool Cmd lends tool `id` for a drag: the one chosen last; until one is, /// Direct Selection to the tools that edit anchors (the Pen) and Selection to the others. - /// None for the selection tools themselves and the view tools. - fn lent_tool(&self, id: &str) -> Option<&'static str> { + /// Cmd+Alt lends Group Selection while Direct Selection is active. Cmd still lends the last + /// selection tool from other tools (Alt on the lent Direct Selection gesture stays copy/subtract). + /// None for the other selection tools and the view tools. + fn lent_tool(&self, id: &str, mods: &Mods) -> Option<&'static str> { use vectorcraft_tools::catalog::{edits_anchors, is_selection_tool}; - if is_selection_tool(id) || matches!(id, "hand" | "zoom" | "rotateView") { + if matches!(id, "hand" | "zoom" | "rotateView") { + return None; + } + if id == "directSelection" && mods.cmd && mods.alt { + return Some("groupSelection"); + } + if is_selection_tool(id) { return None; } Some(self.last_selection_tool.unwrap_or(if edits_anchors(id) { "directSelection" } else { "selection" })) @@ -120,7 +128,7 @@ impl Session { if ev.kind != PointerKind::Down || !ev.mods.cmd || self.lender.is_some() { return *ev; } - let Some(id) = self.lent_tool(self.tool.id()) else { return *ev }; + let Some(id) = self.lent_tool(self.tool.id(), &ev.mods) else { return *ev }; // Typing in progress is kept as its own step first: the lent tool's drag previews from the // document its interaction started from, which would drop the typing (#1881). if crate::cmd::typing_in_progress(self) { @@ -431,7 +439,7 @@ impl Session { /// tools' a crosshair, [`Cursor::precise`]). With Cmd held, the pointer of the selection tool a /// press would borrow ([`Self::lend_selection_tool`]). pub fn cursor(&mut self, p: Point, mods: Mods, view: ViewInfo) -> Cursor { - let lent = if mods.cmd && self.lender.is_none() { self.lent_tool(self.tool.id()) } else { None }; + let lent = if mods.cmd && self.lender.is_none() { self.lent_tool(self.tool.id(), &mods) } else { None }; let c = match lent { Some(id) => { let lent = self.make_tool(id); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index ec067fcc5..db0d7ed1c 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -168,7 +168,7 @@ tools, never checked side by side) isn't captured row by row. |---|---|---| | Space: Hand; Cmd+Space / Cmd+Alt+Space: zoom in/out; middle button pans | done | | | Document window scroll bars along the right and bottom edges (Normal and Full Screen Mode with Menu Bar; none in Full Screen and Presentation Mode): a thumb as long as the view against the art and the artboards; dragging it scrolls, a click on the track pages, the wheel over a bar scrolls along it; in a rotated view they follow the screen's axes | partial | #2322. The span is the art and the artboards with half a window around them, not Illustrator's 227 in canvas: VectorCraft has no bounded drawing area (#2284). A track click pages rather than jumping to the click; the page size and the rotated-view behaviour are our own choice (the public documentation gives neither) | -| Cmd/Ctrl: last-used selection tool; Cmd+Alt: Group Selection inside Direct Selection | partial | | +| Cmd/Ctrl: last-used selection tool; Cmd+Alt: Group Selection inside Direct Selection | done | Cmd+Alt lends Group Selection only while Direct Selection is active (Cmd borrow from the Pen keeps Direct Selection so Alt stays copy/subtract); engine pointer regressions | | Eyedropper reads visible sources while their layer/object is locked or outside isolation | done | Ordinary/Shift/Shift+Alt and gradient-stop reads preserve the source and selection; Alt reverse remains editable-only; hidden art stays excluded (`eyedropper_sources.rs`, #1251) | | Add a visible stroke after drawing with inherited None/zero-width paint | done | Paint and Swap Fill/Stroke restore the existing 1 pt default; positive widths and painted zero widths stay as set; Undo/Redo and targeted appearance items tested (`stroke_visibility.rs`, #1253) | | Double-click a stroked segment to create a width point in Width Point Edit | done | Finite-arithmetic follow-up: engine regressions and full CI passed; [Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area). Command and pointer regressions cover opening, sampled widths, adjoining adjustment on/off through checkbox/OK, compound paths, Cancel/Delete and Undo/Redo; dialog layout reviewed. [Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area) |