From 3c096da84d31a596989f6694966aa2efa7c44ca6 Mon Sep 17 00:00:00 2001 From: theironchef <6278036+theironchef@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:19:09 -0700 Subject: [PATCH] Fix #277: Ctrl+Space zooms (Ctrl+Alt+Space out), Space alone pans Held Space always panned, whatever else was held, so the temporary Zoom tool keys did nothing. Held Space is now the Hand tool and Cmd+Space the Zoom tool (Alt: out), whatever the active tool, for the press and the cursor. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- crates/ui-egui/src/canvas.rs | 51 +++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b7d0eaf8..a16e5a51 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -110,7 +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). + - **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool. - **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 f2c8d512..0add95c1 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -359,12 +359,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let m = ui.input(|i| i.modifiers); let space = ui.input(|i| i.key_down(egui::Key::Space)); let panning = matches!(ui.data(|d| d.get_temp::(drag_id())), Some(Drag::Pan { .. })); + let zooming = if space { m.command } else { app.session.tool_id() == "zoom" }; let cur = if panning { egui::CursorIcon::Grabbing + } else if zooming { + if m.alt { egui::CursorIcon::ZoomOut } else { egui::CursorIcon::ZoomIn } } else if space || app.session.tool_id() == "hand" { egui::CursorIcon::Grab - } else if app.session.tool_id() == "zoom" { - if m.alt { egui::CursorIcon::ZoomOut } else { egui::CursorIcon::ZoomIn } } else if let Some(p) = app.hover_doc { let c = app.session.cursor(p, mods(m, space), view_info); let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("tool-cursor"))); @@ -450,7 +451,10 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: } let tool = app.session.tool_id(); - let pan_mode = space || tool == "hand"; + // Held, Space is the Hand tool for the moment, and Cmd+Space the Zoom tool (with Alt, zooming + // out), whatever the tool. + let zoom_mode = if space { m.command } else { tool == "zoom" }; + let pan_mode = if space { !m.command } else { tool == "hand" }; let middle_pan = matches!(drag, Some(Drag::Pan { middle: true, .. })); // egui counts a press a few pixels outside the canvas as on it (its interaction radius): only a // press on the canvas itself reaches the tools, else it would land at the canvas's centre. @@ -461,10 +465,10 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: { ui.ctx().memory_mut(|mem| mem.stop_text_input()); app.ui.flyout = None; - let d = if pan_mode { - Drag::Pan { start: p, center: v.center, middle: false } - } else if tool == "zoom" { + let d = if zoom_mode { Drag::ZoomBox { start: p } + } else if pan_mode { + Drag::Pan { start: p, center: v.center, middle: false } } else if tool == "rotateView" { let c = rect.center(); Drag::RotateView { start_angle: (p.y - c.y).atan2(p.x - c.x) as f64, start_rot: v.rotation } @@ -1599,6 +1603,41 @@ mod tests { assert_eq!(cmd_drag(&mut app, inside, inside), "selection"); } + #[test] + fn cmd_space_zooms_and_space_pans_whatever_the_tool() { + use egui::{Event, Modifiers}; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.select_tool("rectangle"); + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let at = app.canvas_rect.unwrap().center(); + let space = |pressed, modifiers| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers }; + let button = |pos, pressed, modifiers| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers }; + // A drag from `at` to `to` with Space and `held` down. + let drag = |app: &mut VectorcraftApp, held: Modifiers, to: Pos2| { + frame(app, &ctx, vec![Event::ModifiersChanged(held), space(true, held), Event::PointerMoved(at)]); + frame(app, &ctx, vec![button(at, true, held)]); + frame(app, &ctx, vec![Event::PointerMoved(to)]); + frame(app, &ctx, vec![button(to, false, held)]); + frame(app, &ctx, vec![space(false, held), Event::ModifiersChanged(Modifiers::NONE)]); + }; + let z0 = app.view().unwrap().zoom; + drag(&mut app, Modifiers::COMMAND, at); + let z1 = app.view().unwrap().zoom; + assert_eq!(z1, crate::state::next_zoom(z0, true), "Cmd+Space click zooms in"); + drag(&mut app, Modifiers::COMMAND | Modifiers::ALT, at); + assert_eq!(app.view().unwrap().zoom, crate::state::next_zoom(z1, false), "Cmd+Alt+Space click zooms out"); + assert_eq!(app.session.active().unwrap().doc.art_bounds(), None, "the Rectangle tool drew nothing"); + // Space alone pans, with the Zoom tool too. + app.select_tool("zoom"); + let before = *app.view().unwrap(); + drag(&mut app, Modifiers::NONE, at + vec2(30.0, 0.0)); + let after = *app.view().unwrap(); + assert_eq!(after.zoom, before.zoom); + assert!((after.center.x - (before.center.x - 30.0 / before.zoom)).abs() < 1e-6, "{before:?} → {after:?}"); + } + #[test] fn middle_drag_pans_the_view_with_any_tool() { let mut app = VectorcraftApp::new(Session::new(), Default::default());