From 97aad95d4de347bca1aafd09422c8a14b0d54960 Mon Sep 17 00:00:00 2001 From: Si Date: Sat, 10 Oct 2026 21:17:33 +1100 Subject: [PATCH] Zoom the page with Ctrl+scroll and pinch (#139) (#179) The canvas ignored egui's zoom delta, so Ctrl/Cmd+wheel and trackpad pinch did nothing over the document. Apply it through view.zoom, starting from the shown scale so fit modes (Page Width etc.) zoom from where they are. Co-authored-by: Simon Geljic Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 7 ++++++ crates/ui-egui/src/lib.rs | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 6cad1f3a2..ca9ae352d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -168,6 +168,13 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { (None, None, full) }; app.canvas.canvas_rect = Some(area); + // Ctrl/⌘ + wheel (and trackpad pinch) zooms the document, like Word. egui reports both as a + // zoom factor and doesn't scroll for them. Start from the shown scale so fit modes zoom too. + let zoom_delta = ui.input(|i| i.zoom_delta()); + if (zoom_delta - 1.0).abs() > 1e-3 && ui.rect_contains_pointer(area) { + let pct = (app.canvas.scale / PX_PER_PT * zoom_delta * 100.0).clamp(10.0, 500.0); + let _ = app.run("view.zoom", json!({"value": pct})); + } let geo = geometry(app, &layout, area.size() - vec2(14.0, 0.0)); app.canvas.scale = geo.scale; let caret = layout.caret_on(&app.session.sel.focus, app.session.page_hint); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ba81539cd..1165f5ae2 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -612,6 +612,48 @@ mod tests { WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()) } + /// Issue #139: Ctrl+wheel over the page didn't zoom. + #[test] + fn ctrl_wheel_over_canvas_zooms() { + let ctx = egui::Context::default(); + let mut a = app(); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 800.0)); + let mut t = 0.0; + let mut frame = |a: &mut WordApp, events: Vec| { + t += 1.0 / 60.0; + let input = egui::RawInput { events, time: Some(t), screen_rect: Some(screen), ..Default::default() }; + ctx.run_ui(input, |ui| { + a.logic(ui.ctx()); + a.ui(ui); + }) + .drop_without_applying_deltas(); + }; + // The first frames install fonts; the canvas appears after them. + for _ in 0..3 { + frame(&mut a, Vec::new()); + } + let over_page = a.canvas.canvas_rect.unwrap().center(); + frame(&mut a, vec![egui::Event::PointerMoved(over_page)]); + let before = a.canvas.scale; + let wheel = |y: f32| egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Line, + delta: egui::vec2(0.0, y), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::COMMAND, + }; + frame(&mut a, vec![wheel(1.0)]); + for _ in 0..30 { + frame(&mut a, Vec::new()); + } + assert!(a.canvas.scale > before * 1.05, "Ctrl+wheel up zooms in: {before} -> {}", a.canvas.scale); + let zoomed = a.canvas.scale; + frame(&mut a, vec![wheel(-1.0)]); + for _ in 0..30 { + frame(&mut a, Vec::new()); + } + assert!(a.canvas.scale < zoomed, "Ctrl+wheel down zooms out"); + } + #[test] fn user_name_survives_restart() { let mut first = app();