From c2744a14373ceefabca04757c0ad52065997917f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 25 Apr 2021 17:04:34 +0200 Subject: [PATCH] Implement trackpad pinch-to-zoom for plots in egui_web (#333) This adds a new `zoom_delta` to input. This is hooked up to ctrl-scroll on egui_web and egui_glium. Browsers convert trackpad pinch gestures to ctrl-scroll, so this means you can not pinch-to-zoom plots (on trackpad). In the future we can support multitouch pinch-to-zoom via the same `InputState::zoom_factor()` function --- CHANGELOG.md | 1 + egui/src/data/input.rs | 12 +++++++++ egui/src/input_state.rs | 9 +++++++ egui/src/widgets/plot/mod.rs | 12 ++++++--- egui/src/widgets/plot/transform.rs | 31 ++++++++++-------------- egui_demo_lib/src/apps/demo/plot_demo.rs | 2 ++ egui_glium/src/lib.rs | 21 ++++++++++++---- egui_web/src/lib.rs | 13 +++++++--- 8 files changed, 72 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b55e14b3..094e5ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ * Add `Response::request_focus` and `Response::surrender_focus`. * [Pan and zoom plots](https://github.com/emilk/egui/pull/317). * [Users can now store custom state in `egui::Memory`.](https://github.com/emilk/egui/pull/257). +* Zoom input: ctrl-scroll and (on `egui_web`) trackpad-pinch gesture. ### Changed 🔧 * Make `Memory::has_focus` public (again). diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index ae6d55da..44075eea 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -12,6 +12,12 @@ pub struct RawInput { /// How many points (logical pixels) the user scrolled pub scroll_delta: Vec2, + /// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). + /// * `zoom = 1`: no change (default). + /// * `zoom < 1`: pinch together + /// * `zoom > 1`: pinch spread + pub zoom_delta: f32, + #[deprecated = "Use instead: `screen_rect: Some(Rect::from_pos_size(Default::default(), screen_size))`"] pub screen_size: Vec2, @@ -55,6 +61,7 @@ impl Default for RawInput { #![allow(deprecated)] // for screen_size Self { scroll_delta: Vec2::ZERO, + zoom_delta: 1.0, screen_size: Default::default(), screen_rect: None, pixels_per_point: None, @@ -70,8 +77,11 @@ impl RawInput { /// Helper: move volatile (deltas and events), clone the rest pub fn take(&mut self) -> RawInput { #![allow(deprecated)] // for screen_size + let zoom = self.zoom_delta; + self.zoom_delta = 1.0; RawInput { scroll_delta: std::mem::take(&mut self.scroll_delta), + zoom_delta: zoom, screen_size: self.screen_size, screen_rect: self.screen_rect.take(), pixels_per_point: self.pixels_per_point.take(), @@ -258,6 +268,7 @@ impl RawInput { #![allow(deprecated)] // for screen_size let Self { scroll_delta, + zoom_delta, screen_size: _, screen_rect, pixels_per_point, @@ -268,6 +279,7 @@ impl RawInput { } = self; ui.label(format!("scroll_delta: {:?} points", scroll_delta)); + ui.label(format!("zoom_delta: {:.3?} x", zoom_delta)); ui.label(format!("screen_rect: {:?} points", screen_rect)); ui.label(format!("pixels_per_point: {:?}", pixels_per_point)) .on_hover_text( diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 22afa989..29fbc202 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -115,6 +115,15 @@ impl InputState { self.screen_rect } + /// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). + /// * `zoom = 1`: no change (default). + /// * `zoom < 1`: pinch together + /// * `zoom > 1`: pinch spread + #[inline(always)] + pub fn zoom_delta(&self) -> f32 { + self.raw.zoom_delta + } + pub fn wants_repaint(&self) -> bool { self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty() } diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 16a8b5b8..080c347c 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -339,9 +339,15 @@ impl Widget for Plot { // Zooming if allow_zoom { if let Some(hover_pos) = response.hover_pos() { - let scroll_delta = ui.input().scroll_delta[1]; - if scroll_delta != 0. { - transform.zoom(-0.01 * scroll_delta, hover_pos); + let zoom_factor = ui.input().zoom_delta(); + #[allow(clippy::float_cmp)] + if zoom_factor != 1.0 { + transform.zoom(zoom_factor, hover_pos); + auto_bounds = false; + } + let scroll_delta = ui.input().scroll_delta; + if scroll_delta != Vec2::ZERO { + transform.translate_bounds(-scroll_delta); auto_bounds = false; } } diff --git a/egui/src/widgets/plot/transform.rs b/egui/src/widgets/plot/transform.rs index f57f6d81..6f9d1318 100644 --- a/egui/src/widgets/plot/transform.rs +++ b/egui/src/widgets/plot/transform.rs @@ -159,25 +159,20 @@ impl ScreenTransform { self.bounds.translate(delta_pos); } - /// Zoom by a relative amount with the given screen position as center. - pub fn zoom(&mut self, delta: f32, mut center: Pos2) { - if self.x_centered { - center.x = self.frame.center().x as f32; + /// Zoom by a relative factor with the given screen position as center. + pub fn zoom(&mut self, zoom_factor: f32, center: Pos2) { + let zoom_factor = zoom_factor as f64; + let center = self.value_from_position(center); + + let mut new_bounds = self.bounds; + new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / zoom_factor; + new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / zoom_factor; + new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / zoom_factor; + new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / zoom_factor; + + if new_bounds.is_valid() { + self.bounds = new_bounds; } - if self.y_centered { - center.y = self.frame.center().y as f32; - } - let delta = delta.clamp(-1., 1.); - let frame_width = self.frame.width(); - let frame_height = self.frame.height(); - let bounds_width = self.bounds.width() as f32; - let bounds_height = self.bounds.height() as f32; - let t_x = (center.x - self.frame.min[0]) / frame_width; - let t_y = (self.frame.max[1] - center.y) / frame_height; - self.bounds.min[0] -= ((t_x * delta) * bounds_width) as f64; - self.bounds.min[1] -= ((t_y * delta) * bounds_height) as f64; - self.bounds.max[0] += (((1. - t_x) * delta) * bounds_width) as f64; - self.bounds.max[1] += (((1. - t_y) * delta) * bounds_height) as f64; } pub fn position_from_value(&self, value: &Value) -> Pos2 { diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 58c7abd2..3c332f79 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -90,6 +90,8 @@ impl PlotDemo { ui.checkbox(proportional, "proportional data axes"); }); }); + + ui.label("Drag to pan, ctrl + scroll to zoom. Double-click to reset view."); } fn circle(&self) -> Curve { diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 7142c2a7..dd046490 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -163,15 +163,26 @@ pub fn input_to_egui( } } WindowEvent::MouseWheel { delta, .. } => { - match delta { + let mut delta = match delta { glutin::event::MouseScrollDelta::LineDelta(x, y) => { - let line_height = 24.0; // TODO - input_state.raw.scroll_delta = vec2(x, y) * line_height; + let line_height = 8.0; // magic value! + vec2(x, y) * line_height } glutin::event::MouseScrollDelta::PixelDelta(delta) => { - // Actually point delta - input_state.raw.scroll_delta = vec2(delta.x as f32, delta.y as f32); + vec2(delta.x as f32, delta.y as f32) / pixels_per_point } + }; + if cfg!(target_os = "macos") { + // This is still buggy in winit despite + // https://github.com/rust-windowing/winit/issues/1695 being closed + delta.x *= -1.0; + } + + if input_state.raw.modifiers.ctrl { + // Treat as zoom instead: + input_state.raw.zoom_delta *= (delta.y / 200.0).exp(); + } else { + input_state.raw.scroll_delta += delta; } } _ => { diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index ace9488d..de00fc68 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -963,13 +963,20 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { canvas_size_in_points(runner_ref.0.lock().canvas_id()).y } web_sys::WheelEvent::DOM_DELTA_LINE => { - 24.0 // TODO: tweak this + 8.0 // magic value! } _ => 1.0, }; - runner_lock.input.raw.scroll_delta.x -= scroll_multiplier * event.delta_x() as f32; - runner_lock.input.raw.scroll_delta.y -= scroll_multiplier * event.delta_y() as f32; + let delta = -scroll_multiplier + * egui::Vec2::new(event.delta_x() as f32, event.delta_y() as f32); + + if event.ctrl_key() { + runner_lock.input.raw.zoom_delta *= (delta.y / 200.0).exp(); + } else { + runner_lock.input.raw.scroll_delta += delta; + } + runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default();