From c1ee3b77c5ef277a809474cfa38147668bc9d2fb Mon Sep 17 00:00:00 2001 From: wstock <1394687+wstock@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:18:11 +0000 Subject: [PATCH] Tools: Cmd held with another tool borrows the Selection tool for one gesture (#919) In InDesign, holding Cmd (Ctrl) with the Type or a drawing tool makes a press select, move or resize as the Selection tool would, without switching tools; the Pen family gets Direct Selection. The engine now borrows that tool from a Cmd-held press to the release, keeps the chosen tool, and shows the borrowed tool's cursor while Cmd is held. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh --- crates/engine/src/lib.rs | 4 ++++ crates/engine/src/tests.rs | 39 ++++++++++++++++++++++++++++++++ crates/engine/src/tooling.rs | 44 +++++++++++++++++++++++++++++++++--- docs/ui-parity.md | 2 +- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 745e4564..90912d93 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -275,6 +275,9 @@ pub struct Session { pub prefs: Prefs, pub cache: Arc, pub(crate) tool: Box, + /// The Selection (or Direct Selection) tool a Cmd-held press borrows for one gesture while + /// another tool stays chosen, as InDesign's Cmd/Ctrl does (#919). Dropped on release. + pub(crate) temp_tool: Option>, pub journal: Vec<(String, Value)>, pub(crate) find_context: Option, pub clipboard: Option>, @@ -324,6 +327,7 @@ impl Session { prefs: Prefs::default(), cache: Arc::new(Cache::new()), tool: designcraft_tools::create("selection"), + temp_tool: None, journal: vec![], find_context: None, clipboard: None, diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 616a8639..50b1c3e0 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -41,6 +41,45 @@ fn smart_quotes_and_formatting() { assert_eq!(s.execute("type.selectionAttrs", &json!({})).unwrap()["para"]["align"], "center"); } +/// Issue #919: with Cmd held, a press with the Type (or any drawing) tool selects and drags like +/// the Selection tool, and the chosen tool stays; the Pen family borrows Direct Selection. +#[test] +fn cmd_held_press_borrows_the_selection_tool_for_one_gesture() { + use designcraft_tools::{Mods, PointerEvent, PointerKind}; + let mut s = session(); + let v = ViewInfo::at_zoom(1.0); + let r = s.execute("frame.create", &json!({"rect": [100, 100, 300, 200], "content": "text", "text": "Hello"})).unwrap(); + let fid = r["id"].as_u64().unwrap(); + s.execute("selection.set", &json!({"ids": []})).unwrap(); + s.set_tool("type"); + let cmd = Mods { cmd: true, ..Mods::default() }; + // Cmd-press on the frame selects it as an object (no caret) and a Cmd-drag moves it. + s.pointer(&PointerEvent::new(PointerKind::Down, 150.0, 150.0).with_mods(cmd), v).unwrap(); + assert_eq!(s.doc().unwrap().selection.items, vec![designcraft_doc::ItemId(fid)]); + assert!(s.doc().unwrap().selection.text.is_none(), "no caret: the Selection tool was borrowed"); + s.pointer(&PointerEvent::new(PointerKind::Drag, 170.0, 150.0).with_mods(cmd), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 150.0).with_mods(cmd), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0).with_mods(cmd), v).unwrap(); + assert_eq!(s.doc().unwrap().doc.item(designcraft_doc::ItemId(fid)).unwrap().bounds().x0, 150.0, "moved by the drag"); + assert_eq!(s.tool_id(), "type", "the chosen tool stays"); + assert!(s.temp_tool.is_none(), "the borrowed tool is gone on release"); + // The next plain press is the Type tool's again: a caret in the frame. + s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 150.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0), v).unwrap(); + assert!(s.doc().unwrap().selection.text.is_some(), "a plain press places the caret"); + // Cmd with the Selection tool itself borrows nothing (Cmd-click selects through, as before). + s.set_tool("selection"); + s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 150.0).with_mods(cmd), v).unwrap(); + assert!(s.temp_tool.is_none()); + s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0).with_mods(cmd), v).unwrap(); + // The Pen borrows the Direct Selection tool. + s.set_tool("pen"); + s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 150.0).with_mods(cmd), v).unwrap(); + assert_eq!(s.temp_tool.as_ref().map(|t| t.id()), Some("directSelection")); + s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0).with_mods(cmd), v).unwrap(); + assert_eq!(s.tool_id(), "pen"); +} + #[test] fn tool_gesture_creates_one_undo_step() { let mut s = session(); diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index 820c270f..964fd9c1 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use designcraft_doc::{CellAddr, StoryId}; use designcraft_geom::Unit; pub use designcraft_tools::SnapView; -use designcraft_tools::{Action, CanvasLayout, Cursor, Mods, Overlay, PointerEvent, ToolContext, ToolKey}; +use designcraft_tools::{Action, CanvasLayout, Cursor, Mods, Overlay, PointerEvent, PointerKind, ToolContext, ToolKey}; use serde::Serialize; use serde_json::Value; @@ -63,6 +63,7 @@ impl Session { } self.end_composition(); self.tool = designcraft_tools::create(id); + self.temp_tool = None; // Any other tool unloads the place cursor. if id != "placeGun" { self.loaded = None; @@ -103,12 +104,40 @@ impl Session { unit: view.unit, stroke_in_front: view.stroke_in_front, }; - Some(f(self.tool.as_mut(), &cx)) + // A gesture a Cmd-held press started runs on the borrowed selection tool. + let tool = self.temp_tool.as_deref_mut().unwrap_or(self.tool.as_mut()); + Some(f(tool, &cx)) + } + + /// Which selection tool a Cmd-held press borrows while `tool` is chosen, as InDesign's + /// Cmd/Ctrl does: the Direct Selection tool for the Pen family and Pencil, the Selection + /// tool for the drawing, type and other tools; none for the selection tools themselves, + /// Hand, Zoom and a loaded place cursor, where Cmd means something else or nothing. + fn borrowed_selection_tool(tool: &str) -> Option<&'static str> { + match tool { + "selection" | "directSelection" | "hand" | "zoom" | "placeGun" => None, + "pen" | "addAnchor" | "deleteAnchor" | "convertDirection" | "pencil" | "smooth" | "erase" | "scissors" => Some("directSelection"), + _ => Some("selection"), + } } pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<()> { + // Cmd held at a press: the gesture (press, drags, release) goes to the selection tool the + // chosen tool borrows, which selects, moves or resizes without a tool change (#919). + if matches!(ev.kind, PointerKind::Down | PointerKind::DoubleClick) + && ev.mods.cmd + && self.temp_tool.is_none() + && !self.tool.busy() + && let Some(id) = Self::borrowed_selection_tool(self.tool.id()) + { + self.temp_tool = Some(designcraft_tools::create(id)); + } let actions = self.with_ctx(view, |t, cx| t.pointer(cx, ev)).unwrap_or_default(); - self.run_actions(actions) + let r = self.run_actions(actions); + if ev.kind == PointerKind::Up && self.temp_tool.as_ref().is_some_and(|t| !t.busy()) { + self.temp_tool = None; + } + r } pub fn tool_key(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> Result { @@ -123,6 +152,15 @@ impl Session { } pub fn cursor(&mut self, p: designcraft_geom::Point, mods: Mods, view: ViewInfo) -> Cursor { + // Cmd held over the canvas shows the selection tool a press would borrow. + if mods.cmd + && self.temp_tool.is_none() + && !self.tool.busy() + && let Some(id) = Self::borrowed_selection_tool(self.tool.id()) + { + let borrowed = designcraft_tools::create(id); + return self.with_ctx(view, |_, cx| borrowed.cursor(cx, p, mods)).unwrap_or_default(); + } self.with_ctx(view, |t, cx| t.cursor(cx, p, mods)).unwrap_or_default() } diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 30512bdf..541d432a 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -28,7 +28,7 @@ basics of editing fail for too many users on Windows and Linux. |---|---|---|---|---| | Chrome: app bar, Control panel, Tools panel, dock, doc tabs, status bar | Medium Dark, measured sizes | measured look | ✓ | 11-observed-ui.md | | Properties panel and Contextual Task Bar | per selection | ✓, task bar movable (#143) | ✓ | | -| Selection tool: click, marquee, move, Alt-duplicate, resize handles, rotate from corners | ✓ | ✓ | ✓ | | +| Selection tool: click, marquee, move, Alt-duplicate, resize handles, rotate from corners | ✓ | ✓ | ✓ | Cmd/Ctrl held with another tool borrows it for one gesture (Direct Selection with the Pen family), #919 | | Direct Selection, Pen family, Pencil, Scissors | ✓ | ✓ | ~ | moving reference point "0" fails (#337) | | Live corners (yellow handle) | ✓ | ✓ | ~ | not working for some users (#186) | | Content grabber, frame fitting by double-click | ✓ | ✓ | ✓ | |