diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index c423bc35a..9f7d6058d 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -3357,6 +3357,28 @@ mod tests { assert_eq!(h.app.ui.pan, (0.5, 0.5)); } + #[test] + fn mouse_wheel_zooms_the_loupe_with_and_without_ctrl() { + use crate::state::Zoom; + let mut h = demo([1000.0, 700.0]); + let t = Duration::from_secs(10); + h.request("ui.set", json!({"view": "detail", "right": "none", "leftPanel": false, "filmstrip": false}), t); + h.settle(SETTLE); + for (unit, y, modifiers) in [ + (egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::NONE), + (egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::CTRL), + (egui::MouseWheelUnit::Point, 50.0, egui::Modifiers::CTRL), + ] { + h.request("ui.set", json!({"zoom": "fit", "pan": [0.5, 0.5]}), t); + h.request("ui.hoverWidget", json!({"id": "canvas:image"}), t); + h.app.synthetic.push(egui::Event::MouseWheel { unit, delta: egui::vec2(0.0, y), phase: egui::TouchPhase::Move, modifiers }); + for _ in 0..10 { + h.step(); + } + assert!(matches!(h.app.ui.zoom, Zoom::Percent(_)), "wheel {unit:?} {modifiers:?} must zoom in, got {:?}", h.app.ui.zoom); + } + } + #[test] fn trackpad_navigation_works_in_tools_compare_reference_and_before_after() { use crate::state::Zoom; diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index e72493dd8..a11b99745 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -153,13 +153,44 @@ pub(crate) fn pan_image(app: &mut LightcraftApp, area: Rect, img: Rect, delta: e let _ = app.run("view.navigate", json!({"pan": pan})); } +/// Net vertical notches (up is positive) of unmodified line/page-unit wheel events: a real mouse wheel, not a trackpad. +fn wheel_notches(events: &[egui::Event]) -> f32 { + events + .iter() + .filter_map(|e| match e { + egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page, delta, modifiers, .. } + if *modifiers == egui::Modifiers::NONE && delta.y.is_finite() => + { + Some(delta.y) + } + _ => None, + }) + .sum() +} + /// Native pinch (also modifier-wheel zoom) and two-finger scroll, scoped to this image view. pub(crate) fn navigate_gesture(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response, area: Rect, img: Rect, native: [usize; 2]) -> bool { if !resp.hovered() { return false; } - let (factor, delta, anchor) = + let (mut factor, mut delta, anchor) = ui.input(|i| (i.zoom_delta(), i.translation_delta(), i.multi_touch().map(|t| t.center_pos).or(i.pointer.hover_pos()))); + // A notched mouse wheel (line/page units, no modifier) zooms around the pointer, through egui's smoothed wheel + // delta like Ctrl+wheel (so it glides instead of jumping); trackpads report points and still pan. + let wheel_id = egui::Id::new("loupe-wheel-zoom"); + if ui.input(|i| wheel_notches(&i.events)) != 0.0 { + ui.data_mut(|d| d.insert_temp(wheel_id, true)); + } + if ui.data(|d| d.get_temp::(wheel_id)).unwrap_or(false) { + let smooth = ui.input(|i| i.smooth_scroll_delta.y); + let speed = ui.ctx().options(|o| o.input_options.scroll_zoom_speed); + if smooth == 0.0 { + ui.data_mut(|d| d.remove_temp::(wheel_id)); + } else { + factor *= (speed * smooth).exp(); + delta = egui::Vec2::ZERO; + } + } if !factor.is_finite() || factor <= 0.0 || !delta.is_finite() || (factor == 1.0 && delta == egui::Vec2::ZERO) { return false; } @@ -2234,3 +2265,21 @@ mod tests { assert_eq!(film_label(""), ""); } } + +#[cfg(test)] +mod wheel_zoom_tests { + use super::*; + + fn wheel(unit: egui::MouseWheelUnit, y: f32, modifiers: egui::Modifiers) -> egui::Event { + egui::Event::MouseWheel { unit, delta: egui::vec2(0.0, y), phase: egui::TouchPhase::Move, modifiers } + } + + #[test] + fn only_notched_unmodified_wheels_count() { + let m = egui::Modifiers::NONE; + assert_eq!(wheel_notches(&[wheel(egui::MouseWheelUnit::Line, 1.0, m), wheel(egui::MouseWheelUnit::Line, 2.0, m)]), 3.0); + assert_eq!(wheel_notches(&[wheel(egui::MouseWheelUnit::Point, 40.0, m)]), 0.0); + assert_eq!(wheel_notches(&[wheel(egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::CTRL)]), 0.0); + assert_eq!(wheel_notches(&[wheel(egui::MouseWheelUnit::Line, f32::NAN, m)]), 0.0); + } +}