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
This commit is contained in:
parent
7f0689e566
commit
c2744a1437
8 changed files with 72 additions and 29 deletions
|
@ -13,6 +13,7 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
|
||||||
* Add `Response::request_focus` and `Response::surrender_focus`.
|
* Add `Response::request_focus` and `Response::surrender_focus`.
|
||||||
* [Pan and zoom plots](https://github.com/emilk/egui/pull/317).
|
* [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).
|
* [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 🔧
|
### Changed 🔧
|
||||||
* Make `Memory::has_focus` public (again).
|
* Make `Memory::has_focus` public (again).
|
||||||
|
|
|
@ -12,6 +12,12 @@ pub struct RawInput {
|
||||||
/// How many points (logical pixels) the user scrolled
|
/// How many points (logical pixels) the user scrolled
|
||||||
pub scroll_delta: Vec2,
|
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))`"]
|
#[deprecated = "Use instead: `screen_rect: Some(Rect::from_pos_size(Default::default(), screen_size))`"]
|
||||||
pub screen_size: Vec2,
|
pub screen_size: Vec2,
|
||||||
|
|
||||||
|
@ -55,6 +61,7 @@ impl Default for RawInput {
|
||||||
#![allow(deprecated)] // for screen_size
|
#![allow(deprecated)] // for screen_size
|
||||||
Self {
|
Self {
|
||||||
scroll_delta: Vec2::ZERO,
|
scroll_delta: Vec2::ZERO,
|
||||||
|
zoom_delta: 1.0,
|
||||||
screen_size: Default::default(),
|
screen_size: Default::default(),
|
||||||
screen_rect: None,
|
screen_rect: None,
|
||||||
pixels_per_point: None,
|
pixels_per_point: None,
|
||||||
|
@ -70,8 +77,11 @@ impl RawInput {
|
||||||
/// Helper: move volatile (deltas and events), clone the rest
|
/// Helper: move volatile (deltas and events), clone the rest
|
||||||
pub fn take(&mut self) -> RawInput {
|
pub fn take(&mut self) -> RawInput {
|
||||||
#![allow(deprecated)] // for screen_size
|
#![allow(deprecated)] // for screen_size
|
||||||
|
let zoom = self.zoom_delta;
|
||||||
|
self.zoom_delta = 1.0;
|
||||||
RawInput {
|
RawInput {
|
||||||
scroll_delta: std::mem::take(&mut self.scroll_delta),
|
scroll_delta: std::mem::take(&mut self.scroll_delta),
|
||||||
|
zoom_delta: zoom,
|
||||||
screen_size: self.screen_size,
|
screen_size: self.screen_size,
|
||||||
screen_rect: self.screen_rect.take(),
|
screen_rect: self.screen_rect.take(),
|
||||||
pixels_per_point: self.pixels_per_point.take(),
|
pixels_per_point: self.pixels_per_point.take(),
|
||||||
|
@ -258,6 +268,7 @@ impl RawInput {
|
||||||
#![allow(deprecated)] // for screen_size
|
#![allow(deprecated)] // for screen_size
|
||||||
let Self {
|
let Self {
|
||||||
scroll_delta,
|
scroll_delta,
|
||||||
|
zoom_delta,
|
||||||
screen_size: _,
|
screen_size: _,
|
||||||
screen_rect,
|
screen_rect,
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
|
@ -268,6 +279,7 @@ impl RawInput {
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
ui.label(format!("scroll_delta: {:?} points", scroll_delta));
|
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!("screen_rect: {:?} points", screen_rect));
|
||||||
ui.label(format!("pixels_per_point: {:?}", pixels_per_point))
|
ui.label(format!("pixels_per_point: {:?}", pixels_per_point))
|
||||||
.on_hover_text(
|
.on_hover_text(
|
||||||
|
|
|
@ -115,6 +115,15 @@ impl InputState {
|
||||||
self.screen_rect
|
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 {
|
pub fn wants_repaint(&self) -> bool {
|
||||||
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
|
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
|
||||||
}
|
}
|
||||||
|
|
|
@ -339,9 +339,15 @@ impl Widget for Plot {
|
||||||
// Zooming
|
// Zooming
|
||||||
if allow_zoom {
|
if allow_zoom {
|
||||||
if let Some(hover_pos) = response.hover_pos() {
|
if let Some(hover_pos) = response.hover_pos() {
|
||||||
let scroll_delta = ui.input().scroll_delta[1];
|
let zoom_factor = ui.input().zoom_delta();
|
||||||
if scroll_delta != 0. {
|
#[allow(clippy::float_cmp)]
|
||||||
transform.zoom(-0.01 * scroll_delta, hover_pos);
|
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;
|
auto_bounds = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,25 +159,20 @@ impl ScreenTransform {
|
||||||
self.bounds.translate(delta_pos);
|
self.bounds.translate(delta_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zoom by a relative amount with the given screen position as center.
|
/// Zoom by a relative factor with the given screen position as center.
|
||||||
pub fn zoom(&mut self, delta: f32, mut center: Pos2) {
|
pub fn zoom(&mut self, zoom_factor: f32, center: Pos2) {
|
||||||
if self.x_centered {
|
let zoom_factor = zoom_factor as f64;
|
||||||
center.x = self.frame.center().x as f32;
|
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 {
|
pub fn position_from_value(&self, value: &Value) -> Pos2 {
|
||||||
|
|
|
@ -90,6 +90,8 @@ impl PlotDemo {
|
||||||
ui.checkbox(proportional, "proportional data axes");
|
ui.checkbox(proportional, "proportional data axes");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.label("Drag to pan, ctrl + scroll to zoom. Double-click to reset view.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn circle(&self) -> Curve {
|
fn circle(&self) -> Curve {
|
||||||
|
|
|
@ -163,15 +163,26 @@ pub fn input_to_egui(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::MouseWheel { delta, .. } => {
|
WindowEvent::MouseWheel { delta, .. } => {
|
||||||
match delta {
|
let mut delta = match delta {
|
||||||
glutin::event::MouseScrollDelta::LineDelta(x, y) => {
|
glutin::event::MouseScrollDelta::LineDelta(x, y) => {
|
||||||
let line_height = 24.0; // TODO
|
let line_height = 8.0; // magic value!
|
||||||
input_state.raw.scroll_delta = vec2(x, y) * line_height;
|
vec2(x, y) * line_height
|
||||||
}
|
}
|
||||||
glutin::event::MouseScrollDelta::PixelDelta(delta) => {
|
glutin::event::MouseScrollDelta::PixelDelta(delta) => {
|
||||||
// Actually point delta
|
vec2(delta.x as f32, delta.y as f32) / pixels_per_point
|
||||||
input_state.raw.scroll_delta = vec2(delta.x as f32, delta.y as f32);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
@ -963,13 +963,20 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
|
||||||
canvas_size_in_points(runner_ref.0.lock().canvas_id()).y
|
canvas_size_in_points(runner_ref.0.lock().canvas_id()).y
|
||||||
}
|
}
|
||||||
web_sys::WheelEvent::DOM_DELTA_LINE => {
|
web_sys::WheelEvent::DOM_DELTA_LINE => {
|
||||||
24.0 // TODO: tweak this
|
8.0 // magic value!
|
||||||
}
|
}
|
||||||
_ => 1.0,
|
_ => 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
runner_lock.input.raw.scroll_delta.x -= scroll_multiplier * event.delta_x() as f32;
|
let delta = -scroll_multiplier
|
||||||
runner_lock.input.raw.scroll_delta.y -= scroll_multiplier * event.delta_y() as f32;
|
* 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();
|
runner_lock.needs_repaint.set_true();
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
|
|
Loading…
Reference in a new issue