From 47104f5f4c0dc5a5da186f144b3aa992cddde5a2 Mon Sep 17 00:00:00 2001 From: theironchef <6278036+theironchef@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:08:53 -0700 Subject: [PATCH] Fix #270: Ctrl with another tool drags with the selection tool used last The canvas always switched to the Selection tool for a Cmd press with a non-selection tool. The UI state now remembers which of Selection, Direct Selection and Group Selection was used last, and the temporary switch uses it, so Cmd-dragging an anchor while drawing with the Pen after using Direct Selection moves that anchor, not the whole path. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/canvas.rs | 55 ++++++++++++++++++++++++++++++++++-- crates/ui-egui/src/lib.rs | 3 ++ crates/ui-egui/src/state.rs | 4 +++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5f621006..b7d0eaf8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -110,6 +110,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout. - **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. - **Other:** Eyedropper, Gradient annotator, Artboard, Measure, Type, Hand, Zoom, Rotate View. + - **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). - **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, Gradient, Eyedropper, Blend and the Liquify tools their panels or options. - **Drawing aids:** Smart Guides and snapping, guides dragged out of the rulers, and Draw Normal / Behind / Inside modes. - **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 3b1e31e9..f2c8d512 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -92,6 +92,11 @@ fn temp_tool_id() -> egui::Id { egui::Id::new("canvas-temp-tool") } +/// Selection, Direct Selection and Group Selection: the tools Cmd switches to for a drag. +pub(crate) fn is_selection_tool(id: &str) -> bool { + matches!(id, "selection" | "directSelection" | "groupSelection") +} + /// The pen pressure (0..1) of the press in progress: the force of this frame's pen or touch /// input, else the last one seen since the press (`pressed`: a new press, whose mouse has none /// until a pen reports it). A mouse presses fully (1). @@ -464,11 +469,12 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: let c = rect.center(); Drag::RotateView { start_angle: (p.y - c.y).atan2(p.x - c.x) as f64, start_rot: v.rotation } } else { - let selection_family = matches!(tool, "selection" | "directSelection" | "groupSelection"); let mut kind = Drag::Tool; - if m.command && !selection_family { + // Cmd with another tool: drag with the selection tool used last. + if m.command && !is_selection_tool(tool) { ui.data_mut(|d| d.insert_temp(temp_tool_id(), tool.to_string())); - app.select_tool("selection"); + let last = app.ui.last_selection_tool.clone(); + app.select_tool(if is_selection_tool(&last) { &last } else { "selection" }); kind = Drag::TempSelect; } let ev = PointerEvent { kind: PointerKind::Down, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, true) }; @@ -1550,6 +1556,49 @@ mod tests { assert_eq!(app.session.active().unwrap().doc.art_bounds(), None, "the tool drew nothing"); } + #[test] + fn cmd_with_another_tool_drags_with_the_selection_tool_used_last() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let id = app.session.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 50})).unwrap()["id"].as_u64().unwrap(); + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let cmd_frame = |app: &mut VectorcraftApp, events: Vec| { + let screen = egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0)); + let events = std::iter::once(egui::Event::ModifiersChanged(egui::Modifiers::COMMAND)).chain(events).collect(); + let raw = egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| show(app, ui)); + out.textures_delta.clear(); + }; + let button = + |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::COMMAND }; + // A Cmd drag from `a` to `b` with the Pen tool; → the tool that did it. + let cmd_drag = |app: &mut VectorcraftApp, a: Pos2, b: Pos2| { + app.select_tool("pen"); + cmd_frame(app, vec![egui::Event::PointerMoved(a), button(a, true)]); + let used = app.session.tool_id().to_string(); + cmd_frame(app, vec![egui::Event::PointerMoved(b)]); + cmd_frame(app, vec![button(b, false)]); + assert_eq!(app.session.tool_id(), "pen", "back to the Pen tool"); + used + }; + let bounds = |app: &VectorcraftApp| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap(); + // Direct Selection used last: the drag moves the rectangle's bottom-right corner only. + app.select_tool("directSelection"); + let corner = xf.to_screen(Point::new(200.0, 150.0)); + assert_eq!(cmd_drag(&mut app, corner, corner + vec2(20.0, 20.0)), "directSelection"); + let b = bounds(&app); + assert_eq!((b.x0, b.y0), (100.0, 100.0)); + assert!((b.x1 - xf.to_doc(corner + vec2(20.0, 20.0)).x).abs() < 1e-6, "{b:?}"); + // Group Selection, then Selection: each is the one used. + app.select_tool("groupSelection"); + let inside = xf.to_screen(Point::new(130.0, 120.0)); + assert_eq!(cmd_drag(&mut app, inside, inside), "groupSelection"); + app.select_tool("selection"); + assert_eq!(cmd_drag(&mut app, inside, inside), "selection"); + } + #[test] fn middle_drag_pans_the_view_with_any_tool() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3cf4855b..242d2492 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -513,6 +513,9 @@ impl VectorcraftApp { { *slot = id.to_string(); } + if canvas::is_selection_tool(id) { + self.ui.last_selection_tool = id.to_string(); + } toolbar::remember(self, id); self.ui.flyout = None; } diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 91e106be..380ee327 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -240,6 +240,9 @@ pub struct UiState { pub flyout: Option, /// Last tool shown for each toolbar group (flyout selection sticks). pub group_tool: Vec, + /// The selection tool used last (Selection, Direct Selection or Group Selection): a Cmd press + /// with any other tool drags with it. + pub last_selection_tool: String, pub status: String, pub palette_open: bool, pub palette_query: String, @@ -368,6 +371,7 @@ impl Default for UiState { dialog: None, flyout: None, group_tool: vectorcraft_tools::TOOL_GROUPS.iter().map(|g| g[0].id.to_string()).collect(), + last_selection_tool: "selection".into(), status: String::new(), palette_open: false, palette_query: String::new(),