Add rotation gesture support for trackpad sources (#7453)
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
This commit is contained in:
parent
802d307e4a
commit
b0c568a78e
|
|
@ -11,9 +11,15 @@ use super::percent_decode;
|
|||
/// Data gathered between frames.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WebInput {
|
||||
/// Required because we don't get a position on touched
|
||||
/// Required because we don't get a position on touchend
|
||||
pub primary_touch: Option<egui::TouchId>,
|
||||
|
||||
/// Helps to track the delta scale from gesture events
|
||||
pub accumulated_scale: f32,
|
||||
|
||||
/// Helps to track the delta rotation from gesture events
|
||||
pub accumulated_rotation: f32,
|
||||
|
||||
/// The raw input to `egui`.
|
||||
pub raw: egui::RawInput,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use super::{
|
|||
push_touches, text_from_keyboard_event, translate_key,
|
||||
};
|
||||
|
||||
use js_sys::Reflect;
|
||||
use web_sys::{Document, EventTarget, ShadowRoot};
|
||||
|
||||
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
|
||||
|
|
@ -101,6 +102,7 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
|
|||
install_touchcancel(runner_ref, &canvas)?;
|
||||
|
||||
install_wheel(runner_ref, &canvas)?;
|
||||
install_gesture(runner_ref, &canvas)?;
|
||||
install_drag_and_drop(runner_ref, &canvas)?;
|
||||
install_window_events(runner_ref, &window)?;
|
||||
install_color_scheme_change_event(runner_ref, &window)?;
|
||||
|
|
@ -816,7 +818,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
|||
|
||||
let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl {
|
||||
// The browser is saying the ctrl key is down, but it isn't _really_.
|
||||
// This happens on pinch-to-zoom on a Mac trackpad.
|
||||
// This happens on pinch-to-zoom on multitouch trackpads
|
||||
// egui will treat ctrl+scroll as zoom, so it all works.
|
||||
// However, we explicitly handle it here in order to better match the pinch-to-zoom
|
||||
// speed of a native app, without being sensitive to egui's `scroll_zoom_speed` setting.
|
||||
|
|
@ -847,6 +849,73 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
|||
})
|
||||
}
|
||||
|
||||
fn install_gesture(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "gesturestart", |event: web_sys::Event, runner| {
|
||||
runner.input.accumulated_scale = 1.0;
|
||||
runner.input.accumulated_rotation = 0.0;
|
||||
handle_gesture(event, runner);
|
||||
})?;
|
||||
runner_ref.add_event_listener(target, "gesturechange", handle_gesture)?;
|
||||
runner_ref.add_event_listener(target, "gestureend", |event: web_sys::Event, runner| {
|
||||
handle_gesture(event, runner);
|
||||
runner.input.accumulated_scale = 1.0;
|
||||
runner.input.accumulated_rotation = 0.0;
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
fn handle_gesture(event: web_sys::Event, runner: &mut AppRunner) {
|
||||
// GestureEvent is a non-standard API, so this attempts to get the relevant fields if they exist.
|
||||
let new_scale = Reflect::get(&event, &JsValue::from_str("scale"))
|
||||
.ok()
|
||||
.and_then(|scale| scale.as_f64())
|
||||
.map_or(1.0, |scale| scale as f32);
|
||||
let new_rotation = Reflect::get(&event, &JsValue::from_str("rotation"))
|
||||
.ok()
|
||||
.and_then(|rotation| rotation.as_f64())
|
||||
.map_or(0.0, |rotation| rotation.to_radians() as f32);
|
||||
|
||||
let scale_delta = new_scale / runner.input.accumulated_scale;
|
||||
let rotation_delta = new_rotation - runner.input.accumulated_rotation;
|
||||
runner.input.accumulated_scale *= scale_delta;
|
||||
runner.input.accumulated_rotation += rotation_delta;
|
||||
|
||||
let mut should_stop_propagation = true;
|
||||
let mut should_prevent_default = true;
|
||||
|
||||
if scale_delta != 1.0 {
|
||||
let zoom_event = egui::Event::Zoom(scale_delta);
|
||||
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&zoom_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&zoom_event);
|
||||
runner.input.raw.events.push(zoom_event);
|
||||
}
|
||||
|
||||
if rotation_delta != 0.0 {
|
||||
let rotate_event = egui::Event::Rotate(rotation_delta);
|
||||
|
||||
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&rotate_event);
|
||||
should_prevent_default &= (runner.web_options.should_prevent_default)(&rotate_event);
|
||||
runner.input.raw.events.push(rotate_event);
|
||||
}
|
||||
|
||||
if scale_delta != 1.0 || rotation_delta != 0.0 {
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
|
||||
if should_stop_propagation {
|
||||
event.stop_propagation();
|
||||
}
|
||||
|
||||
if should_prevent_default {
|
||||
// Prevents a simulated ctrl-scroll event for zoom
|
||||
event.prevent_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "dragover", |event: web_sys::DragEvent, runner| {
|
||||
if let Some(data_transfer) = event.data_transfer() {
|
||||
|
|
|
|||
|
|
@ -491,9 +491,7 @@ impl State {
|
|||
// Things we completely ignore:
|
||||
WindowEvent::ActivationTokenDone { .. }
|
||||
| WindowEvent::AxisMotion { .. }
|
||||
| WindowEvent::DoubleTapGesture { .. }
|
||||
| WindowEvent::RotationGesture { .. }
|
||||
| WindowEvent::PanGesture { .. } => EventResponse {
|
||||
| WindowEvent::DoubleTapGesture { .. } => EventResponse {
|
||||
repaint: false,
|
||||
consumed: false,
|
||||
},
|
||||
|
|
@ -508,6 +506,33 @@ impl State {
|
|||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::RotationGesture { delta, .. } => {
|
||||
// Positive delta values indicate counterclockwise rotation
|
||||
// Negative delta values indicate clockwise rotation
|
||||
// This is opposite of egui's sign convention for angles
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Rotate(-delta.to_radians()));
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PanGesture { delta, .. } => {
|
||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -479,6 +479,9 @@ pub enum Event {
|
|||
/// As a user, check [`crate::InputState::smooth_scroll_delta`] to see if the user did any zooming this frame.
|
||||
Zoom(f32),
|
||||
|
||||
/// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture).
|
||||
Rotate(f32),
|
||||
|
||||
/// IME Event
|
||||
Ime(ImeEvent),
|
||||
|
||||
|
|
|
|||
|
|
@ -267,6 +267,9 @@ pub struct InputState {
|
|||
/// * `zoom > 1`: pinch spread
|
||||
zoom_factor_delta: f32,
|
||||
|
||||
/// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture).
|
||||
rotation_radians: f32,
|
||||
|
||||
// ----------------------------------------------
|
||||
/// Position and size of the egui area.
|
||||
pub screen_rect: Rect,
|
||||
|
|
@ -354,6 +357,7 @@ impl Default for InputState {
|
|||
raw_scroll_delta: Vec2::ZERO,
|
||||
smooth_scroll_delta: Vec2::ZERO,
|
||||
zoom_factor_delta: 1.0,
|
||||
rotation_radians: 0.0,
|
||||
|
||||
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
|
||||
pixels_per_point: 1.0,
|
||||
|
|
@ -402,6 +406,7 @@ impl InputState {
|
|||
|
||||
let mut keys_down = self.keys_down;
|
||||
let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor
|
||||
let mut rotation_radians = 0.0;
|
||||
let mut raw_scroll_delta = Vec2::ZERO;
|
||||
|
||||
let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta;
|
||||
|
|
@ -480,6 +485,9 @@ impl InputState {
|
|||
Event::Zoom(factor) => {
|
||||
zoom_factor_delta *= *factor;
|
||||
}
|
||||
Event::Rotate(radians) => {
|
||||
rotation_radians += *radians;
|
||||
}
|
||||
Event::WindowFocused(false) => {
|
||||
// Example: pressing `Cmd+S` brings up a save-dialog (e.g. using rfd),
|
||||
// but we get no key-up event for the `S` key (in winit).
|
||||
|
|
@ -542,6 +550,7 @@ impl InputState {
|
|||
raw_scroll_delta,
|
||||
smooth_scroll_delta,
|
||||
zoom_factor_delta,
|
||||
rotation_radians,
|
||||
|
||||
screen_rect,
|
||||
pixels_per_point,
|
||||
|
|
@ -631,6 +640,26 @@ impl InputState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture).
|
||||
#[inline(always)]
|
||||
pub fn rotation_delta(&self) -> f32 {
|
||||
self.multi_touch()
|
||||
.map_or(self.rotation_radians, |touch| touch.rotation_delta)
|
||||
}
|
||||
|
||||
/// Panning translation in pixels this frame (e.g. from scrolling or a pan gesture)
|
||||
///
|
||||
/// The delta indicates how the **content** should move.
|
||||
///
|
||||
/// A positive X-value indicates the content is being moved right, as when swiping right on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// A positive Y-value indicates the content is being moved down, as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
#[inline(always)]
|
||||
pub fn translation_delta(&self) -> Vec2 {
|
||||
self.multi_touch()
|
||||
.map_or(self.smooth_scroll_delta, |touch| touch.translation_delta)
|
||||
}
|
||||
|
||||
/// How long has it been (in seconds) since the use last scrolled?
|
||||
#[inline(always)]
|
||||
pub fn time_since_last_scroll(&self) -> f32 {
|
||||
|
|
@ -1526,6 +1555,7 @@ impl InputState {
|
|||
unprocessed_scroll_delta_for_zoom,
|
||||
raw_scroll_delta,
|
||||
smooth_scroll_delta,
|
||||
rotation_radians,
|
||||
|
||||
zoom_factor_delta,
|
||||
screen_rect,
|
||||
|
|
@ -1579,6 +1609,7 @@ impl InputState {
|
|||
"smooth_scroll_delta: {smooth_scroll_delta:?} points"
|
||||
));
|
||||
ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x"));
|
||||
ui.label(format!("rotation_radians: {rotation_radians:.3} radians"));
|
||||
|
||||
ui.label(format!("screen_rect: {screen_rect:?} points"));
|
||||
ui.label(format!(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use egui::{
|
||||
Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
Color32, Event, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
emath::{RectTransform, Rot2},
|
||||
vec2,
|
||||
};
|
||||
|
|
@ -30,7 +30,7 @@ impl crate::Demo for MultiTouch {
|
|||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.default_size(vec2(544.0, 512.0))
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
use crate::View as _;
|
||||
|
|
@ -45,13 +45,31 @@ impl crate::View for MultiTouch {
|
|||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.strong(
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles and tablets).",
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles, tablets, and trackpads).",
|
||||
);
|
||||
ui.separator();
|
||||
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
|
||||
|
||||
let relative_pointer_gesture = ui.input(|i| {
|
||||
i.events.iter().any(|event| {
|
||||
matches!(
|
||||
event,
|
||||
Event::MouseWheel { .. } | Event::Zoom { .. } | Event::Rotate { .. }
|
||||
)
|
||||
})
|
||||
});
|
||||
let num_touches = ui.input(|i| i.multi_touch().map_or(0, |mt| mt.num_touches));
|
||||
ui.label(format!("Current touches: {num_touches}"));
|
||||
let num_touches_str = format!("{num_touches}-finger touch");
|
||||
ui.label(format!(
|
||||
"Input source: {}",
|
||||
if ui.input(|i| i.multi_touch().is_some()) {
|
||||
num_touches_str.as_str()
|
||||
} else if relative_pointer_gesture {
|
||||
"cursor"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
));
|
||||
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::WHITE
|
||||
|
|
@ -83,18 +101,18 @@ impl crate::View for MultiTouch {
|
|||
// check for touch input (or the lack thereof) and update zoom and scale factors, plus
|
||||
// color and width:
|
||||
let mut stroke_width = 1.;
|
||||
if let Some(multi_touch) = ui.ctx().multi_touch() {
|
||||
// This adjusts the current zoom factor and rotation angle according to the dynamic
|
||||
// change (for the current frame) of the touch gesture:
|
||||
self.zoom *= multi_touch.zoom_delta;
|
||||
self.rotation += multi_touch.rotation_delta;
|
||||
// the translation we get from `multi_touch` needs to be scaled down to the
|
||||
// normalized coordinates we use as the basis for painting:
|
||||
self.translation += to_screen.inverse().scale() * multi_touch.translation_delta;
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * multi_touch.force;
|
||||
if ui.input(|i| i.multi_touch().is_some()) || relative_pointer_gesture {
|
||||
ui.input(|input| {
|
||||
// This adjusts the current zoom factor, rotation angle, and translation according
|
||||
// to the dynamic change (for the current frame) of the touch gesture:
|
||||
self.zoom *= input.zoom_delta();
|
||||
self.rotation += input.rotation_delta();
|
||||
self.translation += to_screen.inverse().scale() * input.translation_delta();
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * input.multi_touch().map_or(0.0, |touch| touch.force);
|
||||
|
||||
self.last_touch_time = ui.input(|i| i.time);
|
||||
self.last_touch_time = input.time;
|
||||
});
|
||||
} else {
|
||||
self.slowly_reset(ui);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ad5938299b6fefb688ace5775f7906f5992cc119eedb487999cf963a5dcf813
|
||||
size 36275
|
||||
oid sha256:bf7f0a76424a959ede7afbb0eaf777638038cc6fe208ef710d9d82638d68b4d0
|
||||
size 37848
|
||||
|
|
|
|||
Loading…
Reference in New Issue