From 3cae7c533db2c9bffd0eae7ad7d1b1d974cd5591 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sat, 10 Apr 2021 01:09:30 +0200 Subject: [PATCH 01/28] translate touch events from glium to egui Unfortunately, winit does not seem to create _Touch_ events for the touch pad on my mac. Only _TouchpadPressure_ events are sent. Found some issues (like [this](https://github.com/rust-windowing/winit/issues/54)), but I am not sure what they exactly mean: Sometimes, touch events are mixed with touch-to-pointer translation in the discussions. --- egui/src/data/input.rs | 37 ++++++++++++++++++++++++++++++++++++- egui_glium/src/lib.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index ae6d55da58d..ab1c415cb69 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -86,7 +86,7 @@ impl RawInput { /// An input event generated by the integration. /// /// This only covers events that egui cares about. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Event { /// The integration detected a "copy" event (e.g. Cmd+C). Copy, @@ -123,6 +123,25 @@ pub enum Event { CompositionUpdate(String), /// IME composition ended with this final result. CompositionEnd(String), + + Touch { + /// Hashed device identifier (if available; may be zero). + /// Can be used to separate touches from different devices. + device_id: u64, + /// Unique identifier of a finger/pen. Value is stable from touch down + /// to lift-up + id: u64, + phase: TouchPhase, + /// Position of the touch (or where the touch was last detected) + pos: Pos2, + /// Describes how hard the touch device was pressed. May be `None` if the platform + /// does not support pressure sensitivity. + /// Expected to be a float between 0.0 (no pressure) and 1.0 (maximum pressure). + force: Option, + // ### Note for review: using Option forced me to remove `#[derive(Eq)]` + // from the `Event` struct. Can this cause issues? I did not get errors, + // so I wonder if `Eq` had been derived on purpose. + }, } /// Mouse button (or similar for touch input) @@ -284,3 +303,19 @@ impl RawInput { .on_hover_text("key presses etc"); } } + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TouchPhase { + /// User just placed a touch point on the touch surface + Start, + /// User moves a touch point along the surface. This event is also sent when + /// any attributes (position, force, ...) of the touch point change. + Move, + /// User lifted the finger or pen from the surface, or slid off the edge of + /// the surface + End, + /// Touch operation has been disrupted by something (various reasons are possible, + /// maybe a pop-up alert or any other kind of interruption which may not have + /// been intended by the user) + Cancel, +} diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 2608a9bbe79..4415c61f658 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -24,7 +24,12 @@ pub use painter::Painter; use { copypasta::ClipboardProvider, egui::*, - glium::glutin::{self, event::VirtualKeyCode, event_loop::ControlFlow}, + glium::glutin::{ + self, + event::{Force, VirtualKeyCode}, + event_loop::ControlFlow, + }, + std::hash::{Hash, Hasher}, }; pub use copypasta::ClipboardContext; // TODO: remove @@ -170,7 +175,41 @@ pub fn input_to_egui( } } } + WindowEvent::TouchpadPressure { + // device_id, + // pressure, + // stage, + .. + } => { + let todo = dbg!(event); + } + WindowEvent::Touch(touch) => { + let todo = dbg!(event); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + touch.device_id.hash(&mut hasher); + input_state.raw.events.push(Event::Touch { + device_id: hasher.finish(), + id: touch.id, + phase: match touch.phase { + glutin::event::TouchPhase::Started => egui::TouchPhase::Start, + glutin::event::TouchPhase::Moved => egui::TouchPhase::Move, + glutin::event::TouchPhase::Ended => egui::TouchPhase::End, + glutin::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel, + }, + pos: Pos2::new(touch.location.x as f32, touch.location.y as f32), + force: match touch.force { + Some(Force::Normalized(force)) => Some(force as f32), + Some(Force::Calibrated { + force, + max_possible_force, + .. + }) => Some((force / max_possible_force) as f32), + None => None, + }, + }); + } _ => { + let todo = dbg!(event); // dbg!(event); } } From 9dbda043571b86efc58ac1079548ca1574973e6d Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sat, 10 Apr 2021 23:15:28 +0200 Subject: [PATCH 02/28] translate touch events from web_sys to egui The are a few open topics: - egui_web currently translates touch events into pointer events. I guess this should change, such that egui itself performs this kind of conversion. - `pub fn egui_web::pos_from_touch_event` is a public function, but I would like to change the return type to an `Option`. Shouldn't this function be private, anyway? --- egui/src/data/input.rs | 10 +++--- egui_glium/src/lib.rs | 10 +++--- egui_web/src/lib.rs | 77 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index ab1c415cb69..e8238e68645 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -134,11 +134,11 @@ pub enum Event { phase: TouchPhase, /// Position of the touch (or where the touch was last detected) pos: Pos2, - /// Describes how hard the touch device was pressed. May be `None` if the platform - /// does not support pressure sensitivity. - /// Expected to be a float between 0.0 (no pressure) and 1.0 (maximum pressure). - force: Option, - // ### Note for review: using Option forced me to remove `#[derive(Eq)]` + /// Describes how hard the touch device was pressed. May always be `0` if the platform does + /// not support pressure sensitivity. + /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). + force: f32, + // ### Note for review: using f32 forced me to remove `#[derive(Eq)]` // from the `Event` struct. Can this cause issues? I did not get errors, // so I wonder if `Eq` had been derived on purpose. }, diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 4415c61f658..f92bfa97e6a 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -181,10 +181,9 @@ pub fn input_to_egui( // stage, .. } => { - let todo = dbg!(event); + // TODO } WindowEvent::Touch(touch) => { - let todo = dbg!(event); let mut hasher = std::collections::hash_map::DefaultHasher::new(); touch.device_id.hash(&mut hasher); input_state.raw.events.push(Event::Touch { @@ -198,18 +197,17 @@ pub fn input_to_egui( }, pos: Pos2::new(touch.location.x as f32, touch.location.y as f32), force: match touch.force { - Some(Force::Normalized(force)) => Some(force as f32), + Some(Force::Normalized(force)) => force as f32, Some(Force::Calibrated { force, max_possible_force, .. - }) => Some((force / max_possible_force) as f32), - None => None, + }) => (force / max_possible_force) as f32, + None => 0_f32, }, }); } _ => { - let todo = dbg!(event); // dbg!(event); } } diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 78629c61541..2507f46e901 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -112,12 +112,57 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option egui::Pos2 { - let canvas = canvas_element(canvas_id).unwrap(); - let rect = canvas.get_bounding_client_rect(); - let t = event.touches().get(0).unwrap(); + // TO BE CLARIFIED: For some types of touch events (e.g. `touchcancel`) it may be necessary to + // change the return type to an `Option` – but this would be an incompatible change. Can + // we do this? But then, I am not sure yet if a `touchcancel` event does not even have at + // least one `Touch`. + + // Calculate the average of all touch positions: + let touch_count = event.touches().length(); + if touch_count == 0 { + egui::Pos2::ZERO // work-around for not returning an `Option` + } else { + let canvas_origin = canvas_origin(canvas_id); + let mut sum = pos_from_touch(canvas_origin, &event.touches().get(0).unwrap()); + if touch_count == 1 { + sum + } else { + for touch_idx in 1..touch_count { + let touch = event.touches().get(touch_idx).unwrap(); + sum += pos_from_touch(canvas_origin, &touch).to_vec2(); + } + let touch_count_recip = 1. / touch_count as f32; + egui::Pos2::new(sum.x * touch_count_recip, sum.y * touch_count_recip) + } + } +} + +fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 { egui::Pos2 { - x: t.page_x() as f32 - rect.left() as f32, - y: t.page_y() as f32 - rect.top() as f32, + x: touch.page_x() as f32 - canvas_origin.x as f32, + y: touch.page_y() as f32 - canvas_origin.y as f32, + } +} + +fn canvas_origin(canvas_id: &str) -> egui::Pos2 { + let rect = canvas_element(canvas_id) + .unwrap() + .get_bounding_client_rect(); + egui::Pos2::new(rect.left() as f32, rect.top() as f32) +} + +fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) { + let canvas_origin = canvas_origin(runner.canvas_id()); + for touch_idx in 0..event.changed_touches().length() { + if let Some(touch) = event.changed_touches().item(touch_idx) { + runner.input.raw.events.push(egui::Event::Touch { + device_id: 0, + id: touch.identifier() as u64, + phase, + pos: pos_from_touch(canvas_origin, &touch), + force: touch.force(), + }); + } } } @@ -887,6 +932,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { pressed: true, modifiers, }); + push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event); runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); @@ -903,11 +949,17 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let pos = pos_from_touch_event(runner_lock.canvas_id(), &event); runner_lock.input.latest_touch_pos = Some(pos); runner_lock.input.is_touch = true; + + // TO BE DISCUSSED: todo for all `touch*`-events: + // Now that egui knows about Touch events, the backend does not need to simulate + // Pointer events, any more. This simulation could be moved to `egui`. runner_lock .input .raw .events .push(egui::Event::PointerMoved(pos)); + + push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event); runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); @@ -935,6 +987,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { pressed: false, modifiers, }); + push_touches(&mut *runner_lock, egui::TouchPhase::End, &event); // Then remove hover effect: runner_lock.input.raw.events.push(egui::Event::PointerGone); runner_lock.needs_repaint.set_true(); @@ -949,6 +1002,20 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { closure.forget(); } + { + let event_name = "touchcancel"; + let runner_ref = runner_ref.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { + let mut runner_lock = runner_ref.0.lock(); + runner_lock.input.is_touch = true; + push_touches(&mut *runner_lock, egui::TouchPhase::Cancel, &event); + event.stop_propagation(); + event.prevent_default(); + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + { let event_name = "wheel"; let runner_ref = runner_ref.clone(); From 7c1c01a0a2533acab5b1b6893924598be8cba01a Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sun, 11 Apr 2021 17:52:44 +0200 Subject: [PATCH 03/28] introduce `TouchState` and `Gesture` InputState.touch was introduced with type `TouchState`, just as InputState.pointer is of type `Pointer`. The TouchState internally relies on a collection of `Gesture`s. This commit provides the first rudimentary implementation of a Gesture, but has no functionality, yet. --- egui/src/input_state.rs | 14 +++++- egui/src/input_state/touch_state.rs | 49 +++++++++++++++++++ egui/src/input_state/touch_state/gestures.rs | 34 +++++++++++++ .../input_state/touch_state/gestures/zoom.rs | 16 ++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 egui/src/input_state/touch_state.rs create mode 100644 egui/src/input_state/touch_state/gestures.rs create mode 100644 egui/src/input_state/touch_state/gestures/zoom.rs diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 94020fa79d1..b7a14d66dcd 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -1,8 +1,11 @@ +mod touch_state; + use crate::data::input::*; use crate::{emath::*, util::History}; use std::collections::HashSet; pub use crate::data::input::Key; +pub use touch_state::TouchState; /// If the pointer moves more than this, it is no longer a click (but maybe a drag) const MAX_CLICK_DIST: f32 = 6.0; // TODO: move to settings @@ -15,9 +18,12 @@ pub struct InputState { /// The raw input we got this frame from the backend. pub raw: RawInput, - /// State of the mouse or touch. + /// State of the mouse or simple touch gestures which can be mapped mouse operations. pub pointer: PointerState, + /// State of touches, except those covered by PointerState. + pub touch: TouchState, + /// How many pixels the user scrolled. pub scroll_delta: Vec2, @@ -55,6 +61,7 @@ impl Default for InputState { Self { raw: Default::default(), pointer: Default::default(), + touch: Default::default(), scroll_delta: Default::default(), screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), pixels_per_point: 1.0, @@ -84,6 +91,7 @@ impl InputState { self.screen_rect } }); + let touch = self.touch.begin_frame(time, &new); let pointer = self.pointer.begin_frame(time, &new); let mut keys_down = self.keys_down; for event in &new.events { @@ -97,6 +105,7 @@ impl InputState { } InputState { pointer, + touch, scroll_delta: new.scroll_delta, screen_rect, pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point), @@ -502,6 +511,7 @@ impl InputState { let Self { raw, pointer, + touch, scroll_delta, screen_rect, pixels_per_point, @@ -522,6 +532,8 @@ impl InputState { pointer.ui(ui); }); + ui.collapsing("Touch State", |ui| touch.ui(ui)); + ui.label(format!("scroll_delta: {:?} points", scroll_delta)); ui.label(format!("screen_rect: {:?} points", screen_rect)); ui.label(format!( diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs new file mode 100644 index 00000000000..a358a350d01 --- /dev/null +++ b/egui/src/input_state/touch_state.rs @@ -0,0 +1,49 @@ +mod gestures; + +use crate::RawInput; +use gestures::Gesture; + +/// The current state of touch events and gestures. Uses a collection of `Gesture` implementations +/// which track their own state individually +#[derive(Debug)] +pub struct TouchState { + gestures: Vec>, +} + +impl Clone for TouchState { + fn clone(&self) -> Self { + let gestures = self + .gestures + .iter() + .map(|gesture| gesture.boxed_clone()) + .collect(); + TouchState { gestures } + } +} + +impl Default for TouchState { + fn default() -> Self { + Self { + gestures: vec![Box::new(gestures::Zoom::default())], + } + } +} + +impl TouchState { + #[must_use] + pub fn begin_frame(self, time: f64, new: &RawInput) -> Self { + self + } + + fn gestures(&self) -> &Vec> { + &self.gestures + } +} + +impl TouchState { + pub fn ui(&self, ui: &mut crate::Ui) { + for gesture in self.gestures() { + ui.label(format!("{:?}", gesture)); + } + } +} diff --git a/egui/src/input_state/touch_state/gestures.rs b/egui/src/input_state/touch_state/gestures.rs new file mode 100644 index 00000000000..b3dac4f0ebd --- /dev/null +++ b/egui/src/input_state/touch_state/gestures.rs @@ -0,0 +1,34 @@ +mod zoom; + +pub use zoom::Zoom; + +/// TODO: docu +/// ``` +/// assert!( 1 == 0 ) +/// ``` +pub trait Gesture: std::fmt::Debug { + fn boxed_clone(&self) -> Box; + fn state(&self) -> Status; +} + +/// TODO: docu +/// ``` +/// assert!( 1 == 0 ) +/// ``` +#[derive(Clone, Copy, Debug)] +pub enum Status { + /// The `Gesture` is idle, and waiting for events + Waiting, + /// The `Gesture` has detected events, but the conditions for activating are not met (yet) + Checking, + /// The `Gesture` is active and can be asked for its `State` + Active, + /// The `Gesture` has decided that it does not match the current touch events. + Rejected, +} + +impl Default for Status { + fn default() -> Self { + Status::Waiting + } +} diff --git a/egui/src/input_state/touch_state/gestures/zoom.rs b/egui/src/input_state/touch_state/gestures/zoom.rs new file mode 100644 index 00000000000..b209360a3d7 --- /dev/null +++ b/egui/src/input_state/touch_state/gestures/zoom.rs @@ -0,0 +1,16 @@ +use super::{Gesture, Status}; + +#[derive(Clone, Debug, Default)] +pub struct Zoom { + state: Status, +} + +impl Gesture for Zoom { + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn state(&self) -> Status { + self.state + } +} From 69dafccb858b326a443198ff283918710f5fa04a Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sun, 11 Apr 2021 20:20:18 +0200 Subject: [PATCH 04/28] add method InputState::zoom() So far, the method always returns `None`, but it should work as soon as the `Zoom` gesture is implemented. --- egui/src/input_state.rs | 5 +++ egui/src/input_state/touch_state.rs | 22 +++++++++-- egui/src/input_state/touch_state/gestures.rs | 38 ++++++++++++++++--- .../input_state/touch_state/gestures/zoom.rs | 18 +++++++-- 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index b7a14d66dcd..734fa0c8b78 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -184,6 +184,11 @@ impl InputState { // TODO: multiply by ~3 for touch inputs because fingers are fat self.physical_pixel_size() } + + /// Zoom factor when user is pinching or zooming with two fingers on a touch device + pub fn zoom(&self) -> Option { + self.touch.zoom() + } } // ---------------------------------------------------------------------------- diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index a358a350d01..cb0803b2955 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -15,7 +15,12 @@ impl Clone for TouchState { let gestures = self .gestures .iter() - .map(|gesture| gesture.boxed_clone()) + .map(|gesture| { + // Cloning this way does not feel right. + // Why do we have to implement `Clone` in the first place? – That's because + // CtxRef::begin_frame() clones self.0. + gesture.boxed_clone() + }) .collect(); TouchState { gestures } } @@ -35,14 +40,23 @@ impl TouchState { self } - fn gestures(&self) -> &Vec> { - &self.gestures + pub fn zoom(&self) -> Option { + self.gestures + .iter() + .filter(|gesture| matches!(gesture.kind(), gestures::Kind::Zoom)) + .find_map(|gesture| { + if let Some(gestures::Details::Zoom { factor }) = gesture.details() { + Some(factor) + } else { + None + } + }) } } impl TouchState { pub fn ui(&self, ui: &mut crate::Ui) { - for gesture in self.gestures() { + for gesture in &self.gestures { ui.label(format!("{:?}", gesture)); } } diff --git a/egui/src/input_state/touch_state/gestures.rs b/egui/src/input_state/touch_state/gestures.rs index b3dac4f0ebd..c5339a017e0 100644 --- a/egui/src/input_state/touch_state/gestures.rs +++ b/egui/src/input_state/touch_state/gestures.rs @@ -1,14 +1,26 @@ mod zoom; +use std::fmt::Debug; + pub use zoom::Zoom; /// TODO: docu /// ``` /// assert!( 1 == 0 ) /// ``` -pub trait Gesture: std::fmt::Debug { +pub trait Gesture: Debug { + /// Creates a clone in a `Box`. fn boxed_clone(&self) -> Box; - fn state(&self) -> Status; + /// The `Kind` of the gesture. Used for filtering. + fn kind(&self) -> Kind; + /// The current processing state. + fn state(&self) -> State; + /// Returns gesture specific detailed information. + /// Returns `None` when `state()` is not `Active`. + fn details(&self) -> Option
; + /// Returns the screen position at which the gesture was first detected. + /// Returns `None` when `state()` is not `Active`. + fn start_position(&self) -> Option; } /// TODO: docu @@ -16,19 +28,33 @@ pub trait Gesture: std::fmt::Debug { /// assert!( 1 == 0 ) /// ``` #[derive(Clone, Copy, Debug)] -pub enum Status { +pub enum State { /// The `Gesture` is idle, and waiting for events Waiting, /// The `Gesture` has detected events, but the conditions for activating are not met (yet) Checking, - /// The `Gesture` is active and can be asked for its `State` + /// The `Gesture` is active and can be asked for its `Context` Active, /// The `Gesture` has decided that it does not match the current touch events. Rejected, } -impl Default for Status { +impl Default for State { fn default() -> Self { - Status::Waiting + State::Waiting } } + +pub enum Kind { + Zoom, + // more to come... + // Tap, + // Rotate, + // Swipe, +} + +pub enum Details { + Zoom { factor: f32 }, + // Rotate { angle: f32 }, + // Swipe { direction: Vec2, velocity: Vec2 } +} diff --git a/egui/src/input_state/touch_state/gestures/zoom.rs b/egui/src/input_state/touch_state/gestures/zoom.rs index b209360a3d7..c33f26c5a0d 100644 --- a/egui/src/input_state/touch_state/gestures/zoom.rs +++ b/egui/src/input_state/touch_state/gestures/zoom.rs @@ -1,8 +1,8 @@ -use super::{Gesture, Status}; +use super::{Details, Gesture, Kind, State}; #[derive(Clone, Debug, Default)] pub struct Zoom { - state: Status, + state: State, } impl Gesture for Zoom { @@ -10,7 +10,19 @@ impl Gesture for Zoom { Box::new(self.clone()) } - fn state(&self) -> Status { + fn state(&self) -> State { self.state } + + fn kind(&self) -> Kind { + Kind::Zoom + } + + fn details(&self) -> Option
{ + None + } + + fn start_position(&self) -> Option { + todo!() + } } From 7d30aa1914a24b9c65fe7ad101e951e8eadd7d4f Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sun, 11 Apr 2021 22:05:40 +0200 Subject: [PATCH 05/28] manage one `TouchState` per individual device Although quite unlikely, it is still possible to connect more than one touch device. (I have three touch pads connected to my MacBook in total, but unfortunately `winit` sends touch events for none of them.) We do not want to mix-up the touches from different devices. --- egui/src/data/input.rs | 5 +++- egui/src/input_state.rs | 45 ++++++++++++++++++++++------- egui/src/input_state/touch_state.rs | 39 ++++++++++++------------- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index e8238e68645..abdc68e10d8 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -127,7 +127,7 @@ pub enum Event { Touch { /// Hashed device identifier (if available; may be zero). /// Can be used to separate touches from different devices. - device_id: u64, + device_id: TouchDeviceId, /// Unique identifier of a finger/pen. Value is stable from touch down /// to lift-up id: u64, @@ -304,6 +304,9 @@ impl RawInput { } } +/// this is a `u64` as values of this kind can always be obtained by hashing +pub type TouchDeviceId = u64; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TouchPhase { /// User just placed a touch point on the touch surface diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 734fa0c8b78..80a7248e02b 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -2,7 +2,7 @@ mod touch_state; use crate::data::input::*; use crate::{emath::*, util::History}; -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; pub use crate::data::input::Key; pub use touch_state::TouchState; @@ -22,7 +22,8 @@ pub struct InputState { pub pointer: PointerState, /// State of touches, except those covered by PointerState. - pub touch: TouchState, + /// (We keep a separate `TouchState` for each encountered touch device.) + pub touch_states: BTreeMap, /// How many pixels the user scrolled. pub scroll_delta: Vec2, @@ -61,7 +62,7 @@ impl Default for InputState { Self { raw: Default::default(), pointer: Default::default(), - touch: Default::default(), + touch_states: Default::default(), scroll_delta: Default::default(), screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), pixels_per_point: 1.0, @@ -77,7 +78,7 @@ impl Default for InputState { impl InputState { #[must_use] - pub fn begin_frame(self, new: RawInput) -> InputState { + pub fn begin_frame(mut self, new: RawInput) -> InputState { #![allow(deprecated)] // for screen_size let time = new @@ -91,7 +92,10 @@ impl InputState { self.screen_rect } }); - let touch = self.touch.begin_frame(time, &new); + self.create_touch_states_for_new_devices(&new.events); + for touch_state in self.touch_states.values_mut() { + touch_state.begin_frame(time, &new); + } let pointer = self.pointer.begin_frame(time, &new); let mut keys_down = self.keys_down; for event in &new.events { @@ -105,7 +109,7 @@ impl InputState { } InputState { pointer, - touch, + touch_states: self.touch_states, scroll_delta: new.scroll_delta, screen_rect, pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point), @@ -185,9 +189,26 @@ impl InputState { self.physical_pixel_size() } - /// Zoom factor when user is pinching or zooming with two fingers on a touch device + /// Zoom factor when user is pinching (`zoom() < 1.0`) or zooming (`zoom() > 1.0`) with two + /// fingers on a supported touch device pub fn zoom(&self) -> Option { - self.touch.zoom() + // return the zoom value of the first device with an active `Zoom` gesture + self.touch_states + .iter() + .find_map(|(_device_id, touch_state)| touch_state.zoom()) + } + + /// Scans `event` for device IDs of touch devices we have not seen before, + /// and creates a new `TouchState` for each such device. + fn create_touch_states_for_new_devices(&mut self, events: &[Event]) { + for event in events { + if let Event::Touch { device_id, .. } = event { + if !self.touch_states.contains_key(device_id) { + self.touch_states + .insert(*device_id, TouchState::new(*device_id)); + } + } + } } } @@ -516,7 +537,7 @@ impl InputState { let Self { raw, pointer, - touch, + touch_states, scroll_delta, screen_rect, pixels_per_point, @@ -537,7 +558,11 @@ impl InputState { pointer.ui(ui); }); - ui.collapsing("Touch State", |ui| touch.ui(ui)); + for (device_id, touch_state) in touch_states { + ui.collapsing(format!("Touch State [device {}]", device_id), |ui| { + touch_state.ui(ui) + }); + } ui.label(format!("scroll_delta: {:?} points", scroll_delta)); ui.label(format!("screen_rect: {:?} points", screen_rect)); diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index cb0803b2955..fa687999fa9 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -1,44 +1,43 @@ mod gestures; -use crate::RawInput; +use crate::{data::input::TouchDeviceId, RawInput}; use gestures::Gesture; /// The current state of touch events and gestures. Uses a collection of `Gesture` implementations /// which track their own state individually #[derive(Debug)] pub struct TouchState { + device_id: TouchDeviceId, gestures: Vec>, } impl Clone for TouchState { fn clone(&self) -> Self { - let gestures = self - .gestures - .iter() - .map(|gesture| { - // Cloning this way does not feel right. - // Why do we have to implement `Clone` in the first place? – That's because - // CtxRef::begin_frame() clones self.0. - gesture.boxed_clone() - }) - .collect(); - TouchState { gestures } + TouchState { + device_id: self.device_id, + gestures: self + .gestures + .iter() + .map(|gesture| { + // Cloning this way does not feel right. + // Why do we have to implement `Clone` in the first place? – That's because + // CtxRef::begin_frame() clones self.0. + gesture.boxed_clone() + }) + .collect(), + } } } -impl Default for TouchState { - fn default() -> Self { +impl TouchState { + pub fn new(device_id: TouchDeviceId) -> Self { Self { + device_id, gestures: vec![Box::new(gestures::Zoom::default())], } } -} -impl TouchState { - #[must_use] - pub fn begin_frame(self, time: f64, new: &RawInput) -> Self { - self - } + pub fn begin_frame(&mut self, time: f64, new: &RawInput) {} pub fn zoom(&self) -> Option { self.gestures From 2d8406bb31db0d43c6f2243dfc3ab8f07555a2f3 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Mon, 12 Apr 2021 01:44:52 +0200 Subject: [PATCH 06/28] implement control loop for gesture detection The basic idea is that each gesture can focus on detection logic and does not have to care (too much) about managing touch state in general. --- egui/src/data/input.rs | 7 +- egui/src/input_state/touch_state.rs | 116 +++++++++++++++++- egui/src/input_state/touch_state/gestures.rs | 32 ++++- .../input_state/touch_state/gestures/zoom.rs | 66 +++++++++- 4 files changed, 207 insertions(+), 14 deletions(-) diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index abdc68e10d8..36c061b6b7c 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -130,7 +130,7 @@ pub enum Event { device_id: TouchDeviceId, /// Unique identifier of a finger/pen. Value is stable from touch down /// to lift-up - id: u64, + id: TouchId, phase: TouchPhase, /// Position of the touch (or where the touch was last detected) pos: Pos2, @@ -307,6 +307,11 @@ impl RawInput { /// this is a `u64` as values of this kind can always be obtained by hashing pub type TouchDeviceId = u64; +/// Unique identifiction of a touch occurence (finger or pen or ...). +/// A Touch ID is valid until the finger is lifted. +/// A new ID is used for the next touch. +pub type TouchId = u64; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TouchPhase { /// User just placed a touch point on the touch surface diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index fa687999fa9..96b2c9aeb0d 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -1,14 +1,27 @@ mod gestures; -use crate::{data::input::TouchDeviceId, RawInput}; +use std::collections::BTreeMap; + +use crate::{data::input::TouchDeviceId, Event, RawInput, TouchId, TouchPhase}; +use epaint::emath::Pos2; use gestures::Gesture; +/// struct members are the same as in enum variant `Event::Touch` +#[derive(Clone, Copy, Debug)] +pub struct Touch { + pos: Pos2, + force: f32, +} + +pub type TouchMap = BTreeMap; + /// The current state of touch events and gestures. Uses a collection of `Gesture` implementations /// which track their own state individually #[derive(Debug)] pub struct TouchState { device_id: TouchDeviceId, gestures: Vec>, + active_touches: TouchMap, } impl Clone for TouchState { @@ -25,24 +38,117 @@ impl Clone for TouchState { gesture.boxed_clone() }) .collect(), + active_touches: self.active_touches.clone(), } } } impl TouchState { pub fn new(device_id: TouchDeviceId) -> Self { - Self { + let mut result = Self { device_id, - gestures: vec![Box::new(gestures::Zoom::default())], + gestures: Vec::new(), + active_touches: Default::default(), + }; + result.reset_gestures(); + result + } + + fn reset_gestures(&mut self) { + self.gestures = vec![Box::new(gestures::Zoom::default())]; + } + + pub fn begin_frame(&mut self, time: f64, new: &RawInput) { + let my_device_id = self.device_id; + + if self.active_touches.is_empty() { + // no touches so far -> make sure all gestures are `Waiting`: + self.reset_gestures(); + } + + let mut notified_gestures = false; + new.events + .iter() + // + // filter for Touch events belonging to my device_id: + .filter_map(|event| { + if let Event::Touch { + device_id, + id, + phase, + pos, + force, + } = event + { + Some(( + device_id, + id, + phase, + Touch { + pos: *pos, + force: *force, + }, + )) + } else { + None + } + }) + .filter(|(&device_id, ..)| device_id == my_device_id) + // + // process matching Touch events: + .for_each(|(_, touch_id, phase, touch)| { + notified_gestures = true; + match phase { + TouchPhase::Start => { + self.active_touches.insert(*touch_id, touch); + for gesture in &mut self.gestures { + gesture.touch_started(*touch_id, time, &self.active_touches); + } + } + TouchPhase::Move => { + self.active_touches.insert(*touch_id, touch); + for gesture in &mut self.gestures { + gesture.touch_changed(*touch_id, time, &self.active_touches); + } + } + TouchPhase::End => { + if let Some(removed_touch) = self.active_touches.remove(touch_id) { + for gesture in &mut self.gestures { + gesture.touch_ended(removed_touch, time, &self.active_touches); + } + } + } + TouchPhase::Cancel => { + if let Some(removed_touch) = self.active_touches.remove(touch_id) { + for gesture in &mut self.gestures { + gesture.touch_cancelled(removed_touch, time, &self.active_touches); + } + } + } + } + self.remove_rejected_gestures(); + }); + + if !notified_gestures && !self.active_touches.is_empty() { + for gesture in &mut self.gestures { + gesture.check(time, &self.active_touches); + } + self.remove_rejected_gestures(); } } - pub fn begin_frame(&mut self, time: f64, new: &RawInput) {} + fn remove_rejected_gestures(&mut self) { + for i in (0..self.gestures.len()).rev() { + if self.gestures.get(i).unwrap().state() == gestures::State::Rejected { + self.gestures.remove(i); + } + } + } pub fn zoom(&self) -> Option { self.gestures .iter() - .filter(|gesture| matches!(gesture.kind(), gestures::Kind::Zoom)) + .filter(|gesture| gesture.kind() == gestures::Kind::Zoom) .find_map(|gesture| { if let Some(gestures::Details::Zoom { factor }) = gesture.details() { Some(factor) diff --git a/egui/src/input_state/touch_state/gestures.rs b/egui/src/input_state/touch_state/gestures.rs index c5339a017e0..87d98c7aaec 100644 --- a/egui/src/input_state/touch_state/gestures.rs +++ b/egui/src/input_state/touch_state/gestures.rs @@ -4,6 +4,8 @@ use std::fmt::Debug; pub use zoom::Zoom; +use super::{Touch, TouchId, TouchMap}; + /// TODO: docu /// ``` /// assert!( 1 == 0 ) @@ -11,23 +13,47 @@ pub use zoom::Zoom; pub trait Gesture: Debug { /// Creates a clone in a `Box`. fn boxed_clone(&self) -> Box; + /// The `Kind` of the gesture. Used for filtering. fn kind(&self) -> Kind; - /// The current processing state. + + /// The current processing state. If it is `Rejected`, the gesture will not be considered + /// until all touches end and a new touch sequence starts fn state(&self) -> State; + /// Returns gesture specific detailed information. /// Returns `None` when `state()` is not `Active`. fn details(&self) -> Option
; + /// Returns the screen position at which the gesture was first detected. /// Returns `None` when `state()` is not `Active`. fn start_position(&self) -> Option; + + /// This method is called, even if there is no event to process. Thus, it is possible to + /// activate gestures with a delay (e.g. a Single Tap gesture, after having waited for the + /// Double-Tap timeout) + fn check(&mut self, time: f64, active_touches: &TouchMap); + + /// indicates the start of an individual touch. `state` contains this touch and possibly other + /// touches which have been notified earlier + fn touch_started(&mut self, touch_id: TouchId, time: f64, active_touches: &TouchMap); + + /// indicates that a known touch has changed in position or force + fn touch_changed(&mut self, touch_id: TouchId, time: f64, active_touches: &TouchMap); + + /// indicates that a known touch has ended. The touch is not contained in `state` any more. + fn touch_ended(&mut self, touch: Touch, time: f64, active_touches: &TouchMap); + + /// indicates that a known touch has ended unexpectedly (e.g. by an interrupted error pop up or + /// other circumstances). The touch is not contained in `state` any more. + fn touch_cancelled(&mut self, touch: Touch, time: f64, active_touches: &TouchMap); } /// TODO: docu /// ``` /// assert!( 1 == 0 ) /// ``` -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum State { /// The `Gesture` is idle, and waiting for events Waiting, @@ -45,6 +71,7 @@ impl Default for State { } } +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Kind { Zoom, // more to come... @@ -53,6 +80,7 @@ pub enum Kind { // Swipe, } +#[derive(Clone, Debug, PartialEq)] pub enum Details { Zoom { factor: f32 }, // Rotate { angle: f32 }, diff --git a/egui/src/input_state/touch_state/gestures/zoom.rs b/egui/src/input_state/touch_state/gestures/zoom.rs index c33f26c5a0d..95fa06e1994 100644 --- a/egui/src/input_state/touch_state/gestures/zoom.rs +++ b/egui/src/input_state/touch_state/gestures/zoom.rs @@ -1,8 +1,10 @@ -use super::{Details, Gesture, Kind, State}; +use super::{Details, Gesture, Kind, State, Touch, TouchId, TouchMap}; #[derive(Clone, Debug, Default)] pub struct Zoom { state: State, + previous_distance: Option, + current_distance: Option, } impl Gesture for Zoom { @@ -10,19 +12,71 @@ impl Gesture for Zoom { Box::new(self.clone()) } + fn kind(&self) -> Kind { + Kind::Zoom + } + fn state(&self) -> State { self.state } - fn kind(&self) -> Kind { - Kind::Zoom + fn details(&self) -> Option
{ + if let (Some(previous_distance), Some(current_distance)) = + (self.previous_distance, self.current_distance) + { + Some(Details::Zoom { + factor: current_distance / previous_distance, + }) + } else { + None + } } - fn details(&self) -> Option
{ + fn start_position(&self) -> Option { None } - fn start_position(&self) -> Option { - todo!() + fn check(&mut self, _time: f64, _active_touches: &TouchMap) {} + + fn touch_started(&mut self, _touch_id: TouchId, _time: f64, active_touches: &TouchMap) { + if active_touches.len() >= 2 { + self.state = State::Active; + self.update_details(); + } else { + self.state = State::Checking; + } + } + + fn touch_changed(&mut self, _touch_id: TouchId, _time: f64, active_touches: &TouchMap) { + if active_touches.len() >= 2 { + self.state = State::Active; + self.update_details(); + } else { + self.state = State::Checking; + } + } + + fn touch_ended(&mut self, _touch: Touch, _time: f64, active_touches: &TouchMap) { + if active_touches.len() < 2 { + self.state = State::Rejected; + } else { + self.update_details(); + } + } + + fn touch_cancelled(&mut self, _touch: Touch, _time: f64, _active_touches: &TouchMap) { + self.state = State::Rejected; + } +} + +impl Zoom { + fn update_details(&mut self) { + // TODO + // TODO + // TODO + // TODO + // TODO + self.previous_distance = Some(20.); + self.current_distance = Some(25.); } } From c2908ab75250c6c8205e4eb543554a117fa0776b Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Mon, 12 Apr 2021 20:26:27 +0200 Subject: [PATCH 07/28] streamline `Gesture` trait, simplifying impl's --- egui/src/input_state/touch_state.rs | 127 +++++++++--------- egui/src/input_state/touch_state/gestures.rs | 54 +++++--- .../input_state/touch_state/gestures/zoom.rs | 55 +++----- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 96b2c9aeb0d..d5c61d5a07b 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -1,10 +1,8 @@ mod gestures; -use std::collections::BTreeMap; - use crate::{data::input::TouchDeviceId, Event, RawInput, TouchId, TouchPhase}; use epaint::emath::Pos2; -use gestures::Gesture; +use gestures::{Gesture, Phase}; /// struct members are the same as in enum variant `Event::Touch` #[derive(Clone, Copy, Debug)] @@ -13,49 +11,46 @@ pub struct Touch { force: f32, } -pub type TouchMap = BTreeMap; - -/// The current state of touch events and gestures. Uses a collection of `Gesture` implementations -/// which track their own state individually -#[derive(Debug)] +/// The current state (for a specific touch device) of touch events and gestures. Uses a +/// collection of `Gesture` implementations which track their own state individually +#[derive(Clone, Debug)] pub struct TouchState { device_id: TouchDeviceId, - gestures: Vec>, - active_touches: TouchMap, + registered_gestures: Vec, + active_touches: gestures::TouchMap, } -impl Clone for TouchState { +#[derive(Debug)] +struct RegisteredGesture { + /// The current processing state. If it is `Rejected`, the gesture will not be considered + /// any more until all touches end and a new touch sequence starts + phase: Phase, + gesture: Box, +} + +impl Clone for RegisteredGesture { fn clone(&self) -> Self { - TouchState { - device_id: self.device_id, - gestures: self - .gestures - .iter() - .map(|gesture| { - // Cloning this way does not feel right. - // Why do we have to implement `Clone` in the first place? – That's because - // CtxRef::begin_frame() clones self.0. - gesture.boxed_clone() - }) - .collect(), - active_touches: self.active_touches.clone(), + RegisteredGesture { + phase: self.phase, + gesture: self.gesture.boxed_clone(), } } } impl TouchState { pub fn new(device_id: TouchDeviceId) -> Self { - let mut result = Self { + Self { device_id, - gestures: Vec::new(), + registered_gestures: Default::default(), active_touches: Default::default(), - }; - result.reset_gestures(); - result + } } fn reset_gestures(&mut self) { - self.gestures = vec![Box::new(gestures::Zoom::default())]; + self.registered_gestures = vec![RegisteredGesture { + phase: gestures::Phase::Waiting, + gesture: Box::new(gestures::TwoFingerPinchOrZoom::default()), + }]; } pub fn begin_frame(&mut self, time: f64, new: &RawInput) { @@ -96,61 +91,71 @@ impl TouchState { .filter(|(&device_id, ..)| device_id == my_device_id) // // process matching Touch events: - .for_each(|(_, touch_id, phase, touch)| { + .for_each(|(_, &touch_id, phase, touch)| { notified_gestures = true; match phase { TouchPhase::Start => { - self.active_touches.insert(*touch_id, touch); - for gesture in &mut self.gestures { - gesture.touch_started(*touch_id, time, &self.active_touches); + self.active_touches.insert(touch_id, touch); + let ctx = gestures::Context { + time, + active_touches: &self.active_touches, + touch_id, + }; + for reg in &mut self.registered_gestures { + reg.phase = reg.gesture.touch_started(&ctx); } } TouchPhase::Move => { - self.active_touches.insert(*touch_id, touch); - for gesture in &mut self.gestures { - gesture.touch_changed(*touch_id, time, &self.active_touches); + self.active_touches.insert(touch_id, touch); + let ctx = gestures::Context { + time, + active_touches: &self.active_touches, + touch_id, + }; + for reg in &mut self.registered_gestures { + reg.phase = reg.gesture.touch_changed(&ctx); } } TouchPhase::End => { - if let Some(removed_touch) = self.active_touches.remove(touch_id) { - for gesture in &mut self.gestures { - gesture.touch_ended(removed_touch, time, &self.active_touches); + if let Some(removed_touch) = self.active_touches.remove(&touch_id) { + let ctx = gestures::Context { + time, + active_touches: &self.active_touches, + touch_id, + }; + for reg in &mut self.registered_gestures { + reg.phase = reg.gesture.touch_ended(&ctx, removed_touch); } } } TouchPhase::Cancel => { - if let Some(removed_touch) = self.active_touches.remove(touch_id) { - for gesture in &mut self.gestures { - gesture.touch_cancelled(removed_touch, time, &self.active_touches); - } + self.active_touches.remove(&touch_id); + for reg in &mut self.registered_gestures { + reg.phase = Phase::Rejected; } } } - self.remove_rejected_gestures(); + self.registered_gestures + .retain(|g| g.phase != Phase::Rejected); }); if !notified_gestures && !self.active_touches.is_empty() { - for gesture in &mut self.gestures { - gesture.check(time, &self.active_touches); - } - self.remove_rejected_gestures(); - } - } - - fn remove_rejected_gestures(&mut self) { - for i in (0..self.gestures.len()).rev() { - if self.gestures.get(i).unwrap().state() == gestures::State::Rejected { - self.gestures.remove(i); + for reg in &mut self.registered_gestures { + if reg.phase == Phase::Checking { + reg.phase = reg.gesture.check(time, &self.active_touches); + } } + self.registered_gestures + .retain(|g| g.phase != Phase::Rejected); } } pub fn zoom(&self) -> Option { - self.gestures + self.registered_gestures .iter() - .filter(|gesture| gesture.kind() == gestures::Kind::Zoom) - .find_map(|gesture| { - if let Some(gestures::Details::Zoom { factor }) = gesture.details() { + .filter(|reg| reg.gesture.kind() == gestures::Kind::Zoom) + .find_map(|reg| { + if let Some(gestures::Details::Zoom { factor }) = reg.gesture.details() { Some(factor) } else { None @@ -161,7 +166,7 @@ impl TouchState { impl TouchState { pub fn ui(&self, ui: &mut crate::Ui) { - for gesture in &self.gestures { + for gesture in &self.registered_gestures { ui.label(format!("{:?}", gesture)); } } diff --git a/egui/src/input_state/touch_state/gestures.rs b/egui/src/input_state/touch_state/gestures.rs index 87d98c7aaec..fbb2abccc40 100644 --- a/egui/src/input_state/touch_state/gestures.rs +++ b/egui/src/input_state/touch_state/gestures.rs @@ -2,9 +2,20 @@ mod zoom; use std::fmt::Debug; -pub use zoom::Zoom; +pub use zoom::TwoFingerPinchOrZoom; -use super::{Touch, TouchId, TouchMap}; +use super::{Touch, TouchId}; + +pub type TouchMap = std::collections::BTreeMap; + +pub struct Context<'a> { + /// Current time + pub time: f64, + /// Collection of active `Touch` instances + pub active_touches: &'a TouchMap, + /// Identifier of the added, changed, or removed touch + pub touch_id: TouchId, +} /// TODO: docu /// ``` @@ -17,10 +28,6 @@ pub trait Gesture: Debug { /// The `Kind` of the gesture. Used for filtering. fn kind(&self) -> Kind; - /// The current processing state. If it is `Rejected`, the gesture will not be considered - /// until all touches end and a new touch sequence starts - fn state(&self) -> State; - /// Returns gesture specific detailed information. /// Returns `None` when `state()` is not `Active`. fn details(&self) -> Option
; @@ -29,24 +36,28 @@ pub trait Gesture: Debug { /// Returns `None` when `state()` is not `Active`. fn start_position(&self) -> Option; - /// This method is called, even if there is no event to process. Thus, it is possible to - /// activate gestures with a delay (e.g. a Single Tap gesture, after having waited for the - /// Double-Tap timeout) - fn check(&mut self, time: f64, active_touches: &TouchMap); + /// When the gesture's phase is `Phase::Checking`, this method is called, even if there is no + /// event to process. Thus, it is possible to activate gestures with a delay (e.g. activate a + /// Single-Tap gesture after having waited for the Double-Tap timeout) + #[must_use] + fn check(&mut self, _time: f64, _active_touches: &TouchMap) -> Phase { + Phase::Checking + } /// indicates the start of an individual touch. `state` contains this touch and possibly other /// touches which have been notified earlier - fn touch_started(&mut self, touch_id: TouchId, time: f64, active_touches: &TouchMap); + #[must_use] + fn touch_started(&mut self, ctx: &Context<'_>) -> Phase; /// indicates that a known touch has changed in position or force - fn touch_changed(&mut self, touch_id: TouchId, time: f64, active_touches: &TouchMap); + #[must_use] + fn touch_changed(&mut self, ctx: &Context<'_>) -> Phase; /// indicates that a known touch has ended. The touch is not contained in `state` any more. - fn touch_ended(&mut self, touch: Touch, time: f64, active_touches: &TouchMap); - - /// indicates that a known touch has ended unexpectedly (e.g. by an interrupted error pop up or - /// other circumstances). The touch is not contained in `state` any more. - fn touch_cancelled(&mut self, touch: Touch, time: f64, active_touches: &TouchMap); + #[must_use] + fn touch_ended(&mut self, _ctx: &Context<'_>, _removed_touch: Touch) -> Phase { + Phase::Rejected + } } /// TODO: docu @@ -54,8 +65,9 @@ pub trait Gesture: Debug { /// assert!( 1 == 0 ) /// ``` #[derive(Clone, Copy, Debug, PartialEq)] -pub enum State { - /// The `Gesture` is idle, and waiting for events +pub enum Phase { + /// The `Gesture` is idle, and waiting for events. This is the initial phase and should + /// not be set by gesture implementations. Waiting, /// The `Gesture` has detected events, but the conditions for activating are not met (yet) Checking, @@ -65,9 +77,9 @@ pub enum State { Rejected, } -impl Default for State { +impl Default for Phase { fn default() -> Self { - State::Waiting + Phase::Waiting } } diff --git a/egui/src/input_state/touch_state/gestures/zoom.rs b/egui/src/input_state/touch_state/gestures/zoom.rs index 95fa06e1994..6e45b60f828 100644 --- a/egui/src/input_state/touch_state/gestures/zoom.rs +++ b/egui/src/input_state/touch_state/gestures/zoom.rs @@ -1,13 +1,12 @@ -use super::{Details, Gesture, Kind, State, Touch, TouchId, TouchMap}; +use super::{Context, Details, Gesture, Kind, Phase}; #[derive(Clone, Debug, Default)] -pub struct Zoom { - state: State, +pub struct TwoFingerPinchOrZoom { previous_distance: Option, current_distance: Option, } -impl Gesture for Zoom { +impl Gesture for TwoFingerPinchOrZoom { fn boxed_clone(&self) -> Box { Box::new(self.clone()) } @@ -16,10 +15,6 @@ impl Gesture for Zoom { Kind::Zoom } - fn state(&self) -> State { - self.state - } - fn details(&self) -> Option
{ if let (Some(previous_distance), Some(current_distance)) = (self.previous_distance, self.current_distance) @@ -36,40 +31,30 @@ impl Gesture for Zoom { None } - fn check(&mut self, _time: f64, _active_touches: &TouchMap) {} - - fn touch_started(&mut self, _touch_id: TouchId, _time: f64, active_touches: &TouchMap) { - if active_touches.len() >= 2 { - self.state = State::Active; - self.update_details(); - } else { - self.state = State::Checking; + fn touch_started(&mut self, ctx: &Context<'_>) -> Phase { + match ctx.active_touches.len() { + 1 => Phase::Checking, + 2 => { + self.update_details(); + Phase::Checking + } + _ => Phase::Rejected, } } - fn touch_changed(&mut self, _touch_id: TouchId, _time: f64, active_touches: &TouchMap) { - if active_touches.len() >= 2 { - self.state = State::Active; - self.update_details(); - } else { - self.state = State::Checking; + fn touch_changed(&mut self, ctx: &Context<'_>) -> Phase { + match ctx.active_touches.len() { + 1 => Phase::Checking, + 2 => { + self.update_details(); + Phase::Active + } + _ => Phase::Rejected, } } - - fn touch_ended(&mut self, _touch: Touch, _time: f64, active_touches: &TouchMap) { - if active_touches.len() < 2 { - self.state = State::Rejected; - } else { - self.update_details(); - } - } - - fn touch_cancelled(&mut self, _touch: Touch, _time: f64, _active_touches: &TouchMap) { - self.state = State::Rejected; - } } -impl Zoom { +impl TwoFingerPinchOrZoom { fn update_details(&mut self) { // TODO // TODO From a5da37d5801c04653cc632539a3480adde01db18 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Mon, 12 Apr 2021 22:00:01 +0200 Subject: [PATCH 08/28] implement first version of Zoom gesture --- egui/src/input_state/touch_state.rs | 2 +- egui/src/input_state/touch_state/gestures.rs | 2 +- .../input_state/touch_state/gestures/zoom.rs | 40 ++++++++++++------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index d5c61d5a07b..97c36d65402 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -153,7 +153,7 @@ impl TouchState { pub fn zoom(&self) -> Option { self.registered_gestures .iter() - .filter(|reg| reg.gesture.kind() == gestures::Kind::Zoom) + .filter(|reg| reg.gesture.kind() == gestures::Kind::TwoFingerPinchOrZoom) .find_map(|reg| { if let Some(gestures::Details::Zoom { factor }) = reg.gesture.details() { Some(factor) diff --git a/egui/src/input_state/touch_state/gestures.rs b/egui/src/input_state/touch_state/gestures.rs index fbb2abccc40..2515a21f97d 100644 --- a/egui/src/input_state/touch_state/gestures.rs +++ b/egui/src/input_state/touch_state/gestures.rs @@ -85,7 +85,7 @@ impl Default for Phase { #[derive(Clone, Copy, Debug, PartialEq)] pub enum Kind { - Zoom, + TwoFingerPinchOrZoom, // more to come... // Tap, // Rotate, diff --git a/egui/src/input_state/touch_state/gestures/zoom.rs b/egui/src/input_state/touch_state/gestures/zoom.rs index 6e45b60f828..5ba40a33c15 100644 --- a/egui/src/input_state/touch_state/gestures/zoom.rs +++ b/egui/src/input_state/touch_state/gestures/zoom.rs @@ -1,9 +1,11 @@ use super::{Context, Details, Gesture, Kind, Phase}; +use epaint::emath::Pos2; #[derive(Clone, Debug, Default)] pub struct TwoFingerPinchOrZoom { previous_distance: Option, current_distance: Option, + start_position: Option, } impl Gesture for TwoFingerPinchOrZoom { @@ -12,7 +14,7 @@ impl Gesture for TwoFingerPinchOrZoom { } fn kind(&self) -> Kind { - Kind::Zoom + Kind::TwoFingerPinchOrZoom } fn details(&self) -> Option
{ @@ -27,16 +29,16 @@ impl Gesture for TwoFingerPinchOrZoom { } } - fn start_position(&self) -> Option { - None + fn start_position(&self) -> Option { + self.start_position } fn touch_started(&mut self, ctx: &Context<'_>) -> Phase { match ctx.active_touches.len() { 1 => Phase::Checking, 2 => { - self.update_details(); - Phase::Checking + self.update(ctx); + Phase::Checking // received the second touch, now awaiting first movement } _ => Phase::Rejected, } @@ -46,7 +48,7 @@ impl Gesture for TwoFingerPinchOrZoom { match ctx.active_touches.len() { 1 => Phase::Checking, 2 => { - self.update_details(); + self.update(ctx); Phase::Active } _ => Phase::Rejected, @@ -55,13 +57,23 @@ impl Gesture for TwoFingerPinchOrZoom { } impl TwoFingerPinchOrZoom { - fn update_details(&mut self) { - // TODO - // TODO - // TODO - // TODO - // TODO - self.previous_distance = Some(20.); - self.current_distance = Some(25.); + /// Updates current and previous distance of touch points. + /// + /// # Panics + /// + /// Panics if `ctx.active_touches` does not contain two touches. + fn update(&mut self, ctx: &Context<'_>) { + let first_activation = self.previous_distance.is_none(); + self.previous_distance = self.current_distance; + + let mut touch_points = ctx.active_touches.values(); + let v1 = touch_points.next().unwrap().pos.to_vec2(); + let v2 = touch_points.next().unwrap().pos.to_vec2(); + + self.current_distance = Some((v1 - v2).length()); + if first_activation { + let v_mid = (v1 + v2) * 0.5; + self.start_position = Some(Pos2::new(v_mid.x, v_mid.y)); + } } } From 45e59e71aa0399fc5f9a783e8030da7dfcc11705 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Tue, 13 Apr 2021 21:17:51 +0200 Subject: [PATCH 09/28] fix failing doctest a simple `TODO` should be enough --- egui/src/input_state/touch_state/gestures.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/egui/src/input_state/touch_state/gestures.rs b/egui/src/input_state/touch_state/gestures.rs index 2515a21f97d..4f6aa4237d6 100644 --- a/egui/src/input_state/touch_state/gestures.rs +++ b/egui/src/input_state/touch_state/gestures.rs @@ -18,9 +18,6 @@ pub struct Context<'a> { } /// TODO: docu -/// ``` -/// assert!( 1 == 0 ) -/// ``` pub trait Gesture: Debug { /// Creates a clone in a `Box`. fn boxed_clone(&self) -> Box; @@ -61,9 +58,6 @@ pub trait Gesture: Debug { } /// TODO: docu -/// ``` -/// assert!( 1 == 0 ) -/// ``` #[derive(Clone, Copy, Debug, PartialEq)] pub enum Phase { /// The `Gesture` is idle, and waiting for events. This is the initial phase and should From 9e841d55d2f2ec65d7f0f56e6619aab962cd774f Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sun, 18 Apr 2021 00:50:57 +0200 Subject: [PATCH 10/28] get rid of `Gesture`s --- egui/src/input_state.rs | 9 - egui/src/input_state/touch_state.rs | 230 +++++++----------- egui/src/input_state/touch_state/gestures.rs | 94 ------- .../input_state/touch_state/gestures/zoom.rs | 79 ------ 4 files changed, 93 insertions(+), 319 deletions(-) delete mode 100644 egui/src/input_state/touch_state/gestures.rs delete mode 100644 egui/src/input_state/touch_state/gestures/zoom.rs diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 80a7248e02b..5bde23cb306 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -189,15 +189,6 @@ impl InputState { self.physical_pixel_size() } - /// Zoom factor when user is pinching (`zoom() < 1.0`) or zooming (`zoom() > 1.0`) with two - /// fingers on a supported touch device - pub fn zoom(&self) -> Option { - // return the zoom value of the first device with an active `Zoom` gesture - self.touch_states - .iter() - .find_map(|(_device_id, touch_state)| touch_state.zoom()) - } - /// Scans `event` for device IDs of touch devices we have not seen before, /// and creates a new `TouchState` for each such device. fn create_touch_states_for_new_devices(&mut self, events: &[Event]) { diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 97c36d65402..5e8398a39d3 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -1,173 +1,129 @@ -mod gestures; +use std::{collections::BTreeMap, fmt::Debug}; use crate::{data::input::TouchDeviceId, Event, RawInput, TouchId, TouchPhase}; use epaint::emath::Pos2; -use gestures::{Gesture, Phase}; -/// struct members are the same as in enum variant `Event::Touch` -#[derive(Clone, Copy, Debug)] -pub struct Touch { - pos: Pos2, - force: f32, -} - -/// The current state (for a specific touch device) of touch events and gestures. Uses a -/// collection of `Gesture` implementations which track their own state individually -#[derive(Clone, Debug)] +/// The current state (for a specific touch device) of touch events and gestures. +#[derive(Clone)] pub struct TouchState { + /// Technical identifier of the touch device. This is used to identify relevant touch events + /// for this `TouchState` instance. device_id: TouchDeviceId, - registered_gestures: Vec, - active_touches: gestures::TouchMap, + /// Active touches, if any. + /// + /// TouchId is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The + /// next touch will receive a new unique ID. + /// + /// Refer to [`ActiveTouch`]. + active_touches: BTreeMap, + /// Time when the current gesture started. Currently, a new gesture is considered started + /// whenever a finger starts or stops touching the surface. + gesture_start_time: Option, } -#[derive(Debug)] -struct RegisteredGesture { - /// The current processing state. If it is `Rejected`, the gesture will not be considered - /// any more until all touches end and a new touch sequence starts - phase: Phase, - gesture: Box, -} - -impl Clone for RegisteredGesture { - fn clone(&self) -> Self { - RegisteredGesture { - phase: self.phase, - gesture: self.gesture.boxed_clone(), - } - } +/// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as +/// long as the finger/pen touches the surface. +#[derive(Clone, Copy, Debug)] +pub struct ActiveTouch { + /// Screen position where this touch started + start_pos: Pos2, + /// Current screen position of this touch + pos: Pos2, + /// Current force of the touch. A value in the interval [0.0 .. 1.0] + /// + /// Note that a value of 0.0 either indicates a very light touch, or it means that the device + /// is not capable of measuring the touch force. + force: f32, } impl TouchState { pub fn new(device_id: TouchDeviceId) -> Self { Self { device_id, - registered_gestures: Default::default(), active_touches: Default::default(), + gesture_start_time: None, } } - fn reset_gestures(&mut self) { - self.registered_gestures = vec![RegisteredGesture { - phase: gestures::Phase::Waiting, - gesture: Box::new(gestures::TwoFingerPinchOrZoom::default()), - }]; - } - pub fn begin_frame(&mut self, time: f64, new: &RawInput) { - let my_device_id = self.device_id; - - if self.active_touches.is_empty() { - // no touches so far -> make sure all gestures are `Waiting`: - self.reset_gestures(); - } - - let mut notified_gestures = false; - new.events - .iter() - // - // filter for Touch events belonging to my device_id: - .filter_map(|event| { - if let Event::Touch { + for event in &new.events { + match *event { + Event::Touch { device_id, id, phase, pos, force, - } = event - { - Some(( - device_id, - id, - phase, - Touch { - pos: *pos, - force: *force, - }, - )) - } else { - None - } - }) - .filter(|(&device_id, ..)| device_id == my_device_id) - // - // process matching Touch events: - .for_each(|(_, &touch_id, phase, touch)| { - notified_gestures = true; - match phase { - TouchPhase::Start => { - self.active_touches.insert(touch_id, touch); - let ctx = gestures::Context { - time, - active_touches: &self.active_touches, - touch_id, - }; - for reg in &mut self.registered_gestures { - reg.phase = reg.gesture.touch_started(&ctx); - } - } - TouchPhase::Move => { - self.active_touches.insert(touch_id, touch); - let ctx = gestures::Context { - time, - active_touches: &self.active_touches, - touch_id, - }; - for reg in &mut self.registered_gestures { - reg.phase = reg.gesture.touch_changed(&ctx); - } - } - TouchPhase::End => { - if let Some(removed_touch) = self.active_touches.remove(&touch_id) { - let ctx = gestures::Context { - time, - active_touches: &self.active_touches, - touch_id, - }; - for reg in &mut self.registered_gestures { - reg.phase = reg.gesture.touch_ended(&ctx, removed_touch); - } - } - } - TouchPhase::Cancel => { - self.active_touches.remove(&touch_id); - for reg in &mut self.registered_gestures { - reg.phase = Phase::Rejected; - } - } - } - self.registered_gestures - .retain(|g| g.phase != Phase::Rejected); - }); - - if !notified_gestures && !self.active_touches.is_empty() { - for reg in &mut self.registered_gestures { - if reg.phase == Phase::Checking { - reg.phase = reg.gesture.check(time, &self.active_touches); - } + } if device_id == self.device_id => match phase { + TouchPhase::Start => self.touch_start(id, pos, force, time), + TouchPhase::Move => self.touch_move(id, pos, force), + TouchPhase::End | TouchPhase::Cancel => self.touch_end(id, time), + }, + _ => (), } - self.registered_gestures - .retain(|g| g.phase != Phase::Rejected); } } +} - pub fn zoom(&self) -> Option { - self.registered_gestures - .iter() - .filter(|reg| reg.gesture.kind() == gestures::Kind::TwoFingerPinchOrZoom) - .find_map(|reg| { - if let Some(gestures::Details::Zoom { factor }) = reg.gesture.details() { - Some(factor) - } else { - None - } - }) +// private methods +impl TouchState { + fn touch_start(&mut self, id: TouchId, pos: Pos2, force: f32, time: f64) { + self.active_touches.insert( + id, + ActiveTouch { + start_pos: pos, + pos, + force, + }, + ); + // adding a touch counts as the start of a new gesture: + self.start_gesture(time); + } + + fn touch_move(&mut self, id: TouchId, pos: Pos2, force: f32) { + if let Some(touch) = self.active_touches.get_mut(&id) { + touch.pos = pos; + touch.force = force; + } + } + + fn touch_end(&mut self, id: TouchId, time: f64) { + self.active_touches.remove(&id); + // lifting a touch counts as the end of the gesture: + if self.active_touches.is_empty() { + self.end_gesture(); + } else { + self.start_gesture(time); + } + } + + fn start_gesture(&mut self, time: f64) { + self.end_gesture(); + self.gesture_start_time = Some(time); + } + + fn end_gesture(&mut self) { + self.gesture_start_time = None; } } impl TouchState { pub fn ui(&self, ui: &mut crate::Ui) { - for gesture in &self.registered_gestures { - ui.label(format!("{:?}", gesture)); + ui.label(format!("{:?}", self)); + } +} + +impl Debug for TouchState { + // We could just use `#[derive(Debug)]`, but the implementation below produces a less cluttered + // output: + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "gesture_start_time: {:?}\n", + self.gesture_start_time + ))?; + for (id, touch) in self.active_touches.iter() { + f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?; } + Ok(()) } } diff --git a/egui/src/input_state/touch_state/gestures.rs b/egui/src/input_state/touch_state/gestures.rs deleted file mode 100644 index 4f6aa4237d6..00000000000 --- a/egui/src/input_state/touch_state/gestures.rs +++ /dev/null @@ -1,94 +0,0 @@ -mod zoom; - -use std::fmt::Debug; - -pub use zoom::TwoFingerPinchOrZoom; - -use super::{Touch, TouchId}; - -pub type TouchMap = std::collections::BTreeMap; - -pub struct Context<'a> { - /// Current time - pub time: f64, - /// Collection of active `Touch` instances - pub active_touches: &'a TouchMap, - /// Identifier of the added, changed, or removed touch - pub touch_id: TouchId, -} - -/// TODO: docu -pub trait Gesture: Debug { - /// Creates a clone in a `Box`. - fn boxed_clone(&self) -> Box; - - /// The `Kind` of the gesture. Used for filtering. - fn kind(&self) -> Kind; - - /// Returns gesture specific detailed information. - /// Returns `None` when `state()` is not `Active`. - fn details(&self) -> Option
; - - /// Returns the screen position at which the gesture was first detected. - /// Returns `None` when `state()` is not `Active`. - fn start_position(&self) -> Option; - - /// When the gesture's phase is `Phase::Checking`, this method is called, even if there is no - /// event to process. Thus, it is possible to activate gestures with a delay (e.g. activate a - /// Single-Tap gesture after having waited for the Double-Tap timeout) - #[must_use] - fn check(&mut self, _time: f64, _active_touches: &TouchMap) -> Phase { - Phase::Checking - } - - /// indicates the start of an individual touch. `state` contains this touch and possibly other - /// touches which have been notified earlier - #[must_use] - fn touch_started(&mut self, ctx: &Context<'_>) -> Phase; - - /// indicates that a known touch has changed in position or force - #[must_use] - fn touch_changed(&mut self, ctx: &Context<'_>) -> Phase; - - /// indicates that a known touch has ended. The touch is not contained in `state` any more. - #[must_use] - fn touch_ended(&mut self, _ctx: &Context<'_>, _removed_touch: Touch) -> Phase { - Phase::Rejected - } -} - -/// TODO: docu -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Phase { - /// The `Gesture` is idle, and waiting for events. This is the initial phase and should - /// not be set by gesture implementations. - Waiting, - /// The `Gesture` has detected events, but the conditions for activating are not met (yet) - Checking, - /// The `Gesture` is active and can be asked for its `Context` - Active, - /// The `Gesture` has decided that it does not match the current touch events. - Rejected, -} - -impl Default for Phase { - fn default() -> Self { - Phase::Waiting - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Kind { - TwoFingerPinchOrZoom, - // more to come... - // Tap, - // Rotate, - // Swipe, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Details { - Zoom { factor: f32 }, - // Rotate { angle: f32 }, - // Swipe { direction: Vec2, velocity: Vec2 } -} diff --git a/egui/src/input_state/touch_state/gestures/zoom.rs b/egui/src/input_state/touch_state/gestures/zoom.rs deleted file mode 100644 index 5ba40a33c15..00000000000 --- a/egui/src/input_state/touch_state/gestures/zoom.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::{Context, Details, Gesture, Kind, Phase}; -use epaint::emath::Pos2; - -#[derive(Clone, Debug, Default)] -pub struct TwoFingerPinchOrZoom { - previous_distance: Option, - current_distance: Option, - start_position: Option, -} - -impl Gesture for TwoFingerPinchOrZoom { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn kind(&self) -> Kind { - Kind::TwoFingerPinchOrZoom - } - - fn details(&self) -> Option
{ - if let (Some(previous_distance), Some(current_distance)) = - (self.previous_distance, self.current_distance) - { - Some(Details::Zoom { - factor: current_distance / previous_distance, - }) - } else { - None - } - } - - fn start_position(&self) -> Option { - self.start_position - } - - fn touch_started(&mut self, ctx: &Context<'_>) -> Phase { - match ctx.active_touches.len() { - 1 => Phase::Checking, - 2 => { - self.update(ctx); - Phase::Checking // received the second touch, now awaiting first movement - } - _ => Phase::Rejected, - } - } - - fn touch_changed(&mut self, ctx: &Context<'_>) -> Phase { - match ctx.active_touches.len() { - 1 => Phase::Checking, - 2 => { - self.update(ctx); - Phase::Active - } - _ => Phase::Rejected, - } - } -} - -impl TwoFingerPinchOrZoom { - /// Updates current and previous distance of touch points. - /// - /// # Panics - /// - /// Panics if `ctx.active_touches` does not contain two touches. - fn update(&mut self, ctx: &Context<'_>) { - let first_activation = self.previous_distance.is_none(); - self.previous_distance = self.current_distance; - - let mut touch_points = ctx.active_touches.values(); - let v1 = touch_points.next().unwrap().pos.to_vec2(); - let v2 = touch_points.next().unwrap().pos.to_vec2(); - - self.current_distance = Some((v1 - v2).length()); - if first_activation { - let v_mid = (v1 + v2) * 0.5; - self.start_position = Some(Pos2::new(v_mid.x, v_mid.y)); - } - } -} From dfc4f20aca5db512c3428305ba316c4bdf601d30 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sun, 18 Apr 2021 19:58:50 +0200 Subject: [PATCH 11/28] Provide a Zoom/Rotate window in the demo app For now, it works for two fingers only. The third finger interrupts the gesture. Bugs: - Pinching in the demo window also moves the window -> Pointer events must be ignored when touch is active - Pinching also works when doing it outside the demo window -> it would be nice to return the touch info in the `Response` of the painter allocation --- egui/src/input_state.rs | 15 +- egui/src/input_state/touch_state.rs | 175 +++++++++++++++++--- egui_demo_lib/src/apps/demo/demo_windows.rs | 1 + egui_demo_lib/src/apps/demo/mod.rs | 1 + egui_demo_lib/src/apps/demo/zoom_rotate.rs | 62 +++++++ 5 files changed, 230 insertions(+), 24 deletions(-) create mode 100644 egui_demo_lib/src/apps/demo/zoom_rotate.rs diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 5bde23cb306..3df8ab2c494 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -5,7 +5,8 @@ use crate::{emath::*, util::History}; use std::collections::{BTreeMap, HashSet}; pub use crate::data::input::Key; -pub use touch_state::TouchState; +pub use touch_state::TouchInfo; +use touch_state::TouchState; /// If the pointer moves more than this, it is no longer a click (but maybe a drag) const MAX_CLICK_DIST: f32 = 6.0; // TODO: move to settings @@ -21,9 +22,9 @@ pub struct InputState { /// State of the mouse or simple touch gestures which can be mapped mouse operations. pub pointer: PointerState, - /// State of touches, except those covered by PointerState. + /// State of touches, except those covered by PointerState (like clicks and drags). /// (We keep a separate `TouchState` for each encountered touch device.) - pub touch_states: BTreeMap, + touch_states: BTreeMap, /// How many pixels the user scrolled. pub scroll_delta: Vec2, @@ -189,6 +190,14 @@ impl InputState { self.physical_pixel_size() } + pub fn touches(&self) -> Option { + if let Some(touch_state) = self.touch_states.values().find(|t| t.is_active()) { + touch_state.info() + } else { + None + } + } + /// Scans `event` for device IDs of touch devices we have not seen before, /// and creates a new `TouchState` for each such device. fn create_touch_states_for_new_devices(&mut self, events: &[Event]) { diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 5e8398a39d3..81f364ce4bb 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -1,7 +1,43 @@ use std::{collections::BTreeMap, fmt::Debug}; use crate::{data::input::TouchDeviceId, Event, RawInput, TouchId, TouchPhase}; -use epaint::emath::Pos2; +use epaint::emath::{pos2, Pos2, Vec2}; + +/// All you probably need to know about the current two-finger touch gesture. +pub struct TouchInfo { + /// Point in time when the gesture started + pub start_time: f64, + /// Position where the gesture started (average of all individual touch positions) + pub start_pos: Pos2, + /// Current position of the gesture (average of all individual touch positions) + pub current_pos: Pos2, + /// Dynamic information about the touch gesture, relative to the start of the gesture. + /// Refer to [`GestureInfo`]. + pub total: DynamicTouchInfo, + /// Dynamic information about the touch gesture, relative to the previous frame. + /// Refer to [`GestureInfo`]. + pub incremental: DynamicTouchInfo, +} + +/// Information about the dynamic state of a gesture. Note that there is no internal threshold +/// which needs to be reached before this information is updated. If you want a threshold, you +/// have to manage this on your application code. +pub struct DynamicTouchInfo { + /// Zoom factor (Pinch or Zoom). Moving fingers closer together or further appart will change + /// this value. + pub zoom: f32, + /// Rotation in radians. Rotating the fingers, but also moving just one of them will change + /// this value. + pub rotation: f32, + /// Movement (in points) of the average position of all touch points. + pub translation: Vec2, + /// Force of the touch (average of the forces of the individual fingers). This is a + /// value in the interval `[0.0 .. =1.0]`. + /// + /// (Note that a value of 0.0 either indicates a very light touch, or it means that the device + /// is not capable of measuring the touch force at all.) + pub force: f32, +} /// The current state (for a specific touch device) of touch events and gestures. #[derive(Clone)] @@ -16,17 +52,34 @@ pub struct TouchState { /// /// Refer to [`ActiveTouch`]. active_touches: BTreeMap, - /// Time when the current gesture started. Currently, a new gesture is considered started - /// whenever a finger starts or stops touching the surface. - gesture_start_time: Option, + /// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this + /// holds state information + gesture_state: Option, +} + +#[derive(Clone, Debug)] +struct GestureState { + start_time: f64, + start: DynGestureState, + previous: DynGestureState, + current: DynGestureState, +} + +/// Gesture data which can change over time +#[derive(Clone, Copy, Debug)] +struct DynGestureState { + pos: Pos2, + distance: f32, + direction: f32, + force: f32, } /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as /// long as the finger/pen touches the surface. #[derive(Clone, Copy, Debug)] pub struct ActiveTouch { - /// Screen position where this touch started - start_pos: Pos2, + /// Screen position where this touch was when the gesture startet + gesture_start_pos: Pos2, /// Current screen position of this touch pos: Pos2, /// Current force of the touch. A value in the interval [0.0 .. 1.0] @@ -41,7 +94,7 @@ impl TouchState { Self { device_id, active_touches: Default::default(), - gesture_start_time: None, + gesture_state: None, } } @@ -63,6 +116,34 @@ impl TouchState { } } } + + pub fn is_active(&self) -> bool { + self.gesture_state.is_some() + } + + pub fn info(&self) -> Option { + if let Some(state) = &self.gesture_state { + Some(TouchInfo { + start_time: state.start_time, + start_pos: state.start.pos, + current_pos: state.current.pos, + total: DynamicTouchInfo { + zoom: state.current.distance / state.start.distance, + rotation: state.current.direction - state.start.direction, + translation: state.current.pos - state.start.pos, + force: state.current.force, + }, + incremental: DynamicTouchInfo { + zoom: state.current.distance / state.previous.distance, + rotation: state.current.direction - state.previous.direction, + translation: state.current.pos - state.previous.pos, + force: state.current.force - state.previous.force, + }, + }) + } else { + None + } + } } // private methods @@ -71,13 +152,17 @@ impl TouchState { self.active_touches.insert( id, ActiveTouch { - start_pos: pos, + gesture_start_pos: pos, pos, force, }, ); - // adding a touch counts as the start of a new gesture: - self.start_gesture(time); + // for now we only support exactly two fingers: + if self.active_touches.len() == 2 { + self.start_gesture(time); + } else { + self.end_gesture() + } } fn touch_move(&mut self, id: TouchId, pos: Pos2, force: f32) { @@ -85,25 +170,63 @@ impl TouchState { touch.pos = pos; touch.force = force; } + if let Some((touch1, touch2)) = self.both_touches() { + let state_new = DynGestureState { + pos: self::center_pos(touch1.pos, touch2.pos), + distance: self::distance(touch1.pos, touch2.pos), + direction: self::direction(touch1.pos, touch2.pos), + force: (touch1.force + touch2.force) * 0.5, + }; + if let Some(ref mut state) = &mut self.gesture_state { + state.previous = state.current; + state.current = state_new; + } + } } fn touch_end(&mut self, id: TouchId, time: f64) { self.active_touches.remove(&id); - // lifting a touch counts as the end of the gesture: - if self.active_touches.is_empty() { - self.end_gesture(); - } else { + // for now we only support exactly two fingers: + if self.active_touches.len() == 2 { self.start_gesture(time); + } else { + self.end_gesture() } } fn start_gesture(&mut self, time: f64) { - self.end_gesture(); - self.gesture_start_time = Some(time); + for mut touch in self.active_touches.values_mut() { + touch.gesture_start_pos = touch.pos; + } + if let Some((touch1, touch2)) = self.both_touches() { + let start_dyn_state = DynGestureState { + pos: self::center_pos(touch1.pos, touch2.pos), + distance: self::distance(touch1.pos, touch2.pos), + direction: self::direction(touch1.pos, touch2.pos), + force: (touch1.force + touch2.force) * 0.5, + }; + self.gesture_state = Some(GestureState { + start_time: time, + start: start_dyn_state, + previous: start_dyn_state, + current: start_dyn_state, + }); + } + } + + fn both_touches(&self) -> Option<(&ActiveTouch, &ActiveTouch)> { + if self.active_touches.len() == 2 { + let mut touches = self.active_touches.values(); + let touch1 = touches.next().unwrap(); + let touch2 = touches.next().unwrap(); + Some((touch1, touch2)) + } else { + None + } } fn end_gesture(&mut self) { - self.gesture_start_time = None; + self.gesture_state = None; } } @@ -117,13 +240,23 @@ impl Debug for TouchState { // We could just use `#[derive(Debug)]`, but the implementation below produces a less cluttered // output: fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "gesture_start_time: {:?}\n", - self.gesture_start_time - ))?; + f.write_fmt(format_args!("gesture: {:?}\n", self.gesture_state))?; for (id, touch) in self.active_touches.iter() { f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?; } Ok(()) } } + +fn center_pos(pos_1: Pos2, pos_2: Pos2) -> Pos2 { + pos2((pos_1.x + pos_2.x) * 0.5, (pos_1.y + pos_2.y) * 0.5) +} + +pub(crate) fn distance(pos_1: Pos2, pos_2: Pos2) -> f32 { + (pos_2 - pos_1).length() +} + +pub(crate) fn direction(pos_1: Pos2, pos_2: Pos2) -> f32 { + let v = (pos_2 - pos_1).normalized(); + v.y.atan2(v.x) +} diff --git a/egui_demo_lib/src/apps/demo/demo_windows.rs b/egui_demo_lib/src/apps/demo/demo_windows.rs index dd20b9d3eda..bdfa1f0fe9b 100644 --- a/egui_demo_lib/src/apps/demo/demo_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_windows.rs @@ -16,6 +16,7 @@ impl Default for Demos { let demos: Vec> = vec![ Box::new(super::dancing_strings::DancingStrings::default()), Box::new(super::drag_and_drop::DragAndDropDemo::default()), + Box::new(super::zoom_rotate::ZoomRotate::default()), Box::new(super::font_book::FontBook::default()), Box::new(super::DemoWindow::default()), Box::new(super::painting::Painting::default()), diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index 42461645073..b709d6d0ca2 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -22,6 +22,7 @@ pub mod toggle_switch; pub mod widget_gallery; mod widgets; pub mod window_options; +pub mod zoom_rotate; pub use {app::*, demo_window::DemoWindow, demo_windows::*, widgets::Widgets}; diff --git a/egui_demo_lib/src/apps/demo/zoom_rotate.rs b/egui_demo_lib/src/apps/demo/zoom_rotate.rs new file mode 100644 index 00000000000..ce493fe8f79 --- /dev/null +++ b/egui_demo_lib/src/apps/demo/zoom_rotate.rs @@ -0,0 +1,62 @@ +use egui::{ + emath::{RectTransform, Rot2}, + vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, +}; + +#[derive(Default)] +pub struct ZoomRotate {} + +impl super::Demo for ZoomRotate { + fn name(&self) -> &'static str { + "👌 Zoom/Rotate" + } + + fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .default_size(vec2(512.0, 512.0)) + .resizable(true) + .show(ctx, |ui| { + use super::View; + self.ui(ui); + }); + } +} + +impl super::View for ZoomRotate { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + ui.add(crate::__egui_github_link_file!()); + }); + ui.colored_label(Color32::RED, "This only works on mobile devices or with other touch devices supported by the backend."); + ui.separator(); + ui.label("Pinch, Zoom, or Rotate the arrow with two fingers."); + Frame::dark_canvas(ui.style()).show(ui, |ui| { + let (response, painter) = + ui.allocate_painter(ui.available_size_before_wrap_finite(), Sense::hover()); + let painter_proportions = response.rect.square_proportions(); + // scale painter to ±1 units in each direction with [0,0] in the center: + let to_screen = RectTransform::from_to( + Rect::from_min_size(Pos2::ZERO - painter_proportions, 2. * painter_proportions), + response.rect, + ); + let stroke = Stroke::new(1.0, Color32::YELLOW); + + let (zoom_factor, rotation); + if let Some(touches) = ui.input().touches() { + zoom_factor = touches.total.zoom; + rotation = touches.total.rotation; + } else { + zoom_factor = 1.; + rotation = 0.; + } + let scaled_rotation = zoom_factor * Rot2::from_angle(rotation); + + painter.arrow( + to_screen * (Pos2::ZERO + scaled_rotation * vec2(-0.5, 0.5)), + to_screen.scale() * (scaled_rotation * vec2(1., -1.)), + stroke, + ); + }); + } +} From 286e4d50ac3dfddc97246937304602dbd26e1934 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sun, 18 Apr 2021 21:59:18 +0200 Subject: [PATCH 12/28] fix comments and non-idiomatic code --- egui/src/input_state.rs | 6 ++- egui/src/input_state/touch_state.rs | 57 ++++++++++++++--------------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 3df8ab2c494..bf15f289cea 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -19,7 +19,7 @@ pub struct InputState { /// The raw input we got this frame from the backend. pub raw: RawInput, - /// State of the mouse or simple touch gestures which can be mapped mouse operations. + /// State of the mouse or simple touch gestures which can be mapped to mouse operations. pub pointer: PointerState, /// State of touches, except those covered by PointerState (like clicks and drags). @@ -191,6 +191,8 @@ impl InputState { } pub fn touches(&self) -> Option { + // In case of multiple touch devices simply pick the touch_state for the first touch device + // with an ongoing gesture: if let Some(touch_state) = self.touch_states.values().find(|t| t.is_active()) { touch_state.info() } else { @@ -198,7 +200,7 @@ impl InputState { } } - /// Scans `event` for device IDs of touch devices we have not seen before, + /// Scans `events` for device IDs of touch devices we have not seen before, /// and creates a new `TouchState` for each such device. fn create_touch_states_for_new_devices(&mut self, events: &[Event]) { for event in events { diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 81f364ce4bb..922b4b76a8d 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -21,7 +21,7 @@ pub struct TouchInfo { /// Information about the dynamic state of a gesture. Note that there is no internal threshold /// which needs to be reached before this information is updated. If you want a threshold, you -/// have to manage this on your application code. +/// have to manage this in your application code. pub struct DynamicTouchInfo { /// Zoom factor (Pinch or Zoom). Moving fingers closer together or further appart will change /// this value. @@ -34,14 +34,17 @@ pub struct DynamicTouchInfo { /// Force of the touch (average of the forces of the individual fingers). This is a /// value in the interval `[0.0 .. =1.0]`. /// - /// (Note that a value of 0.0 either indicates a very light touch, or it means that the device - /// is not capable of measuring the touch force at all.) + /// Note 1: A value of 0.0 either indicates a very light touch, or it means that the device + /// is not capable of measuring the touch force at all. + /// + /// Note 2: Just increasing the physical pressure without actually moving the finger may not + /// lead to a change of this value. pub force: f32, } /// The current state (for a specific touch device) of touch events and gestures. #[derive(Clone)] -pub struct TouchState { +pub(crate) struct TouchState { /// Technical identifier of the touch device. This is used to identify relevant touch events /// for this `TouchState` instance. device_id: TouchDeviceId, @@ -77,7 +80,7 @@ struct DynGestureState { /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as /// long as the finger/pen touches the surface. #[derive(Clone, Copy, Debug)] -pub struct ActiveTouch { +struct ActiveTouch { /// Screen position where this touch was when the gesture startet gesture_start_pos: Pos2, /// Current screen position of this touch @@ -122,27 +125,23 @@ impl TouchState { } pub fn info(&self) -> Option { - if let Some(state) = &self.gesture_state { - Some(TouchInfo { - start_time: state.start_time, - start_pos: state.start.pos, - current_pos: state.current.pos, - total: DynamicTouchInfo { - zoom: state.current.distance / state.start.distance, - rotation: state.current.direction - state.start.direction, - translation: state.current.pos - state.start.pos, - force: state.current.force, - }, - incremental: DynamicTouchInfo { - zoom: state.current.distance / state.previous.distance, - rotation: state.current.direction - state.previous.direction, - translation: state.current.pos - state.previous.pos, - force: state.current.force - state.previous.force, - }, - }) - } else { - None - } + self.gesture_state.as_ref().map(|state| TouchInfo { + start_time: state.start_time, + start_pos: state.start.pos, + current_pos: state.current.pos, + total: DynamicTouchInfo { + zoom: state.current.distance / state.start.distance, + rotation: state.current.direction - state.start.direction, + translation: state.current.pos - state.start.pos, + force: state.current.force, + }, + incremental: DynamicTouchInfo { + zoom: state.current.distance / state.previous.distance, + rotation: state.current.direction - state.previous.direction, + translation: state.current.pos - state.previous.pos, + force: state.current.force - state.previous.force, + }, + }) } } @@ -240,10 +239,10 @@ impl Debug for TouchState { // We could just use `#[derive(Debug)]`, but the implementation below produces a less cluttered // output: fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("gesture: {:?}\n", self.gesture_state))?; for (id, touch) in self.active_touches.iter() { f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?; } + f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?; Ok(()) } } @@ -252,11 +251,11 @@ fn center_pos(pos_1: Pos2, pos_2: Pos2) -> Pos2 { pos2((pos_1.x + pos_2.x) * 0.5, (pos_1.y + pos_2.y) * 0.5) } -pub(crate) fn distance(pos_1: Pos2, pos_2: Pos2) -> f32 { +fn distance(pos_1: Pos2, pos_2: Pos2) -> f32 { (pos_2 - pos_1).length() } -pub(crate) fn direction(pos_1: Pos2, pos_2: Pos2) -> f32 { +fn direction(pos_1: Pos2, pos_2: Pos2) -> f32 { let v = (pos_2 - pos_1).normalized(); v.y.atan2(v.x) } From 39d93e1afb0685f36c1b673e6c344369a61e29be Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sun, 18 Apr 2021 22:14:18 +0200 Subject: [PATCH 13/28] update touch state *each frame* --- egui/src/input_state/touch_state.rs | 48 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 922b4b76a8d..87b548db6c1 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -118,6 +118,9 @@ impl TouchState { _ => (), } } + // This needs to be called each frame, even if there are no new touch events. + // Failing to do so may result in wrong information in `TouchInfo.incremental` + self.update_gesture(); } pub fn is_active(&self) -> bool { @@ -165,22 +168,12 @@ impl TouchState { } fn touch_move(&mut self, id: TouchId, pos: Pos2, force: f32) { - if let Some(touch) = self.active_touches.get_mut(&id) { + if let Some(touch) = self.active_touches.get_mut(&id) + // always true + { touch.pos = pos; touch.force = force; } - if let Some((touch1, touch2)) = self.both_touches() { - let state_new = DynGestureState { - pos: self::center_pos(touch1.pos, touch2.pos), - distance: self::distance(touch1.pos, touch2.pos), - direction: self::direction(touch1.pos, touch2.pos), - force: (touch1.force + touch2.force) * 0.5, - }; - if let Some(ref mut state) = &mut self.gesture_state { - state.previous = state.current; - state.current = state_new; - } - } } fn touch_end(&mut self, id: TouchId, time: f64) { @@ -197,7 +190,9 @@ impl TouchState { for mut touch in self.active_touches.values_mut() { touch.gesture_start_pos = touch.pos; } - if let Some((touch1, touch2)) = self.both_touches() { + if let Some((touch1, touch2)) = self.both_touches() + // always true + { let start_dyn_state = DynGestureState { pos: self::center_pos(touch1.pos, touch2.pos), distance: self::distance(touch1.pos, touch2.pos), @@ -213,6 +208,27 @@ impl TouchState { } } + fn update_gesture(&mut self) { + if let Some((touch1, touch2)) = self.both_touches() { + let state_new = DynGestureState { + pos: self::center_pos(touch1.pos, touch2.pos), + distance: self::distance(touch1.pos, touch2.pos), + direction: self::direction(touch1.pos, touch2.pos), + force: (touch1.force + touch2.force) * 0.5, + }; + if let Some(ref mut state) = &mut self.gesture_state + // always true + { + state.previous = state.current; + state.current = state_new; + } + } + } + + fn end_gesture(&mut self) { + self.gesture_state = None; + } + fn both_touches(&self) -> Option<(&ActiveTouch, &ActiveTouch)> { if self.active_touches.len() == 2 { let mut touches = self.active_touches.values(); @@ -223,10 +239,6 @@ impl TouchState { None } } - - fn end_gesture(&mut self) { - self.gesture_state = None; - } } impl TouchState { From 211972e87428847d589c39aab6a98e8a9a69bf55 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Fri, 23 Apr 2021 00:03:38 +0200 Subject: [PATCH 14/28] change egui_demo to use *relative* touch data --- egui/src/input_state/touch_state.rs | 35 +++++++++-- egui_demo_lib/src/apps/demo/zoom_rotate.rs | 70 +++++++++++++++++----- 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 87b548db6c1..4990d6f9c9b 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -1,4 +1,8 @@ -use std::{collections::BTreeMap, fmt::Debug}; +use std::{ + collections::BTreeMap, + f32::consts::{PI, TAU}, + fmt::Debug, +}; use crate::{data::input::TouchDeviceId, Event, RawInput, TouchId, TouchPhase}; use epaint::emath::{pos2, Pos2, Vec2}; @@ -118,8 +122,8 @@ impl TouchState { _ => (), } } - // This needs to be called each frame, even if there are no new touch events. - // Failing to do so may result in wrong information in `TouchInfo.incremental` + // This needs to be called each frame, even if there are no new touch events. Failing to + // do so may result in wrong delta information self.update_gesture(); } @@ -134,13 +138,13 @@ impl TouchState { current_pos: state.current.pos, total: DynamicTouchInfo { zoom: state.current.distance / state.start.distance, - rotation: state.current.direction - state.start.direction, + rotation: normalized_angle(state.current.direction, state.start.direction), translation: state.current.pos - state.start.pos, force: state.current.force, }, incremental: DynamicTouchInfo { zoom: state.current.distance / state.previous.distance, - rotation: state.current.direction - state.previous.direction, + rotation: normalized_angle(state.current.direction, state.previous.direction), translation: state.current.pos - state.previous.pos, force: state.current.force - state.previous.force, }, @@ -271,3 +275,24 @@ fn direction(pos_1: Pos2, pos_2: Pos2) -> f32 { let v = (pos_2 - pos_1).normalized(); v.y.atan2(v.x) } + +/// Calculate difference between two directions, such that the absolute value of the result is +/// minimized. +fn normalized_angle(current_direction: f32, previous_direction: f32) -> f32 { + let mut angle = current_direction - previous_direction; + angle %= TAU; + if angle > PI { + angle -= TAU; + } else if angle < -PI { + angle += TAU; + } + dbg!(angle) +} + +#[test] +fn normalizing_angle_from_350_to_0_yields_10() { + assert!( + (normalized_angle(0_f32.to_radians(), 350_f32.to_radians()) - 10_f32.to_radians()).abs() + <= 5. * f32::EPSILON // many conversions (=divisions) involved => high error rate + ); +} diff --git a/egui_demo_lib/src/apps/demo/zoom_rotate.rs b/egui_demo_lib/src/apps/demo/zoom_rotate.rs index ce493fe8f79..9818a609b7e 100644 --- a/egui_demo_lib/src/apps/demo/zoom_rotate.rs +++ b/egui_demo_lib/src/apps/demo/zoom_rotate.rs @@ -3,8 +3,21 @@ use egui::{ vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, }; -#[derive(Default)] -pub struct ZoomRotate {} +pub struct ZoomRotate { + last_time: Option, + rotation: f32, + zoom: f32, +} + +impl Default for ZoomRotate { + fn default() -> Self { + Self { + last_time: None, + rotation: 0., + zoom: 1., + } + } +} impl super::Demo for ZoomRotate { fn name(&self) -> &'static str { @@ -20,6 +33,7 @@ impl super::Demo for ZoomRotate { use super::View; self.ui(ui); }); + self.last_time = Some(ctx.input().time); } } @@ -28,34 +42,58 @@ impl super::View for ZoomRotate { ui.vertical_centered(|ui| { ui.add(crate::__egui_github_link_file!()); }); - ui.colored_label(Color32::RED, "This only works on mobile devices or with other touch devices supported by the backend."); + ui.colored_label( + Color32::RED, + "This only works on supported touch devices, like mobiles.", + ); ui.separator(); ui.label("Pinch, Zoom, or Rotate the arrow with two fingers."); Frame::dark_canvas(ui.style()).show(ui, |ui| { + // Note that we use `Sense::drag()` although we do not use any pointer events. With + // the current implementation, the fact that a touch event of two or more fingers is + // recognized, does not mean that the pointer events are suppressed, which are always + // generated for the first finger. Therefore, if we do not explicitly consume pointer + // events, the window will move around, not only when dragged with a single finger, but + // also when a two-finger touch is active. I guess this problem can only be cleanly + // solved when the synthetic pointer events are created by egui, and not by the + // backend. let (response, painter) = - ui.allocate_painter(ui.available_size_before_wrap_finite(), Sense::hover()); + ui.allocate_painter(ui.available_size_before_wrap_finite(), Sense::drag()); + // normalize painter coordinates to ±1 units in each direction with [0,0] in the center: let painter_proportions = response.rect.square_proportions(); - // scale painter to ±1 units in each direction with [0,0] in the center: let to_screen = RectTransform::from_to( Rect::from_min_size(Pos2::ZERO - painter_proportions, 2. * painter_proportions), response.rect, ); - let stroke = Stroke::new(1.0, Color32::YELLOW); - let (zoom_factor, rotation); if let Some(touches) = ui.input().touches() { - zoom_factor = touches.total.zoom; - rotation = touches.total.rotation; - } else { - zoom_factor = 1.; - rotation = 0.; + // This adjusts the current zoom factor and rotation angle according to the dynamic + // change (for the current frame) of the touch gesture: + self.zoom *= touches.incremental.zoom; + self.rotation += touches.incremental.rotation; + // for a smooth touch experience (shouldn't this be done by egui automatically?): + ui.ctx().request_repaint(); + } else if let Some(last_time) = self.last_time { + // This has nothing to do with the touch gesture. It just smoothly brings the + // painted arrow back into its original position, for a better visual effect: + let dt = ui.input().time - last_time; + const ZOOM_ROTATE_HALF_LIFE: f64 = 1.; // time[sec] after which half the amount of zoom/rotation will be reverted + let half_life_factor = (-(2_f64.ln()) / ZOOM_ROTATE_HALF_LIFE * dt).exp() as f32; + self.zoom = 1. + ((self.zoom - 1.) * half_life_factor); + self.rotation *= half_life_factor; + // this is an animation, so we want real-time UI updates: + ui.ctx().request_repaint(); } - let scaled_rotation = zoom_factor * Rot2::from_angle(rotation); + let zoom_and_rotate = self.zoom * Rot2::from_angle(self.rotation); + // Paints an arrow pointing from bottom-left (-0.5, 0.5) to top-right (0.5, -0.5), + // but scaled and rotated according to the current translation: + let arrow_start = zoom_and_rotate * vec2(-0.5, 0.5); + let arrow_direction = zoom_and_rotate * vec2(1., -1.); painter.arrow( - to_screen * (Pos2::ZERO + scaled_rotation * vec2(-0.5, 0.5)), - to_screen.scale() * (scaled_rotation * vec2(1., -1.)), - stroke, + to_screen * (Pos2::ZERO + arrow_start), + to_screen.scale() * arrow_direction, + Stroke::new(1.0, Color32::YELLOW), ); }); } From ca846e5e02d0bfc3b30e856e72096941a02bb5b6 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sat, 24 Apr 2021 00:11:46 +0200 Subject: [PATCH 15/28] support more than two fingers This commit includes an improved Demo Window for egui_demo, and a complete re-write of the gesture detection. The PR should be ready for review, soon. --- egui/src/input_state.rs | 11 +- egui/src/input_state/touch_state.rs | 296 ++++++++++----------- egui/src/lib.rs | 2 +- egui_demo_lib/src/apps/demo/zoom_rotate.rs | 34 ++- 4 files changed, 173 insertions(+), 170 deletions(-) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 838b8a651e2..5e9a3ed4f92 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -5,7 +5,7 @@ use crate::{emath::*, util::History}; use std::collections::{BTreeMap, HashSet}; pub use crate::data::input::Key; -pub use touch_state::TouchInfo; +pub use touch_state::MultiTouchInfo; use touch_state::TouchState; /// If the pointer moves more than this, it is no longer a click (but maybe a drag) @@ -95,7 +95,7 @@ impl InputState { }); self.create_touch_states_for_new_devices(&new.events); for touch_state in self.touch_states.values_mut() { - touch_state.begin_frame(time, &new); + touch_state.begin_frame(time, &new, self.pointer.interact_pos); } let pointer = self.pointer.begin_frame(time, &new); let mut keys_down = self.keys_down; @@ -194,9 +194,10 @@ impl InputState { self.physical_pixel_size() } - pub fn touches(&self) -> Option { - // In case of multiple touch devices simply pick the touch_state for the first touch device - // with an ongoing gesture: + /// Details about the currently ongoing multi-touch gesture, if any. See [`MultiTouchInfo`]. + pub fn multi_touch(&self) -> Option { + // In case of multiple touch devices simply pick the touch_state for the first active + // device if let Some(touch_state) = self.touch_states.values().find(|t| t.is_active()) { touch_state.info() } else { diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 4990d6f9c9b..0e7613a97f8 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -5,44 +5,43 @@ use std::{ }; use crate::{data::input::TouchDeviceId, Event, RawInput, TouchId, TouchPhase}; -use epaint::emath::{pos2, Pos2, Vec2}; +use epaint::emath::{Pos2, Vec2}; -/// All you probably need to know about the current two-finger touch gesture. -pub struct TouchInfo { - /// Point in time when the gesture started +/// All you probably need to know about a multi-touch gesture. +pub struct MultiTouchInfo { + /// Point in time when the gesture started. pub start_time: f64, - /// Position where the gesture started (average of all individual touch positions) + /// Position of the pointer at the time the gesture started. pub start_pos: Pos2, - /// Current position of the gesture (average of all individual touch positions) - pub current_pos: Pos2, - /// Dynamic information about the touch gesture, relative to the start of the gesture. - /// Refer to [`GestureInfo`]. - pub total: DynamicTouchInfo, - /// Dynamic information about the touch gesture, relative to the previous frame. - /// Refer to [`GestureInfo`]. - pub incremental: DynamicTouchInfo, -} - -/// Information about the dynamic state of a gesture. Note that there is no internal threshold -/// which needs to be reached before this information is updated. If you want a threshold, you -/// have to manage this in your application code. -pub struct DynamicTouchInfo { + /// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no + /// `MultiTouchInfo` is created. + pub num_touches: usize, /// Zoom factor (Pinch or Zoom). Moving fingers closer together or further appart will change - /// this value. + /// this value. This is a relative value, comparing the average distances of the fingers in + /// the current and previous frame. If the fingers did not move since the previous frame, + /// this value is `1.0`. pub zoom: f32, - /// Rotation in radians. Rotating the fingers, but also moving just one of them will change - /// this value. + /// Rotation in radians. Moving fingers around each other will change this value. This is a + /// relative value, comparing the orientation of fingers in the current frame with the previous + /// frame. If all fingers are resting, this value is `0.0`. pub rotation: f32, - /// Movement (in points) of the average position of all touch points. + /// Relative movement (comparing previous frame and current frame) of the average position of + /// all touch points. Without movement this value is `Vec2::ZERO`. + /// + /// Note that this may not necessarily be measured in screen points (although it _will_ be for + /// most mobile devices). In general (depending on the touch device), touch coordinates cannot + /// be directly mapped to the screen. A touch always is considered to start at the position of + /// the pointer, but touch movement is always measured in the units delivered by the device, + /// and may depend on hardware and system settings. pub translation: Vec2, - /// Force of the touch (average of the forces of the individual fingers). This is a + /// Current force of the touch (average of the forces of the individual fingers). This is a /// value in the interval `[0.0 .. =1.0]`. /// - /// Note 1: A value of 0.0 either indicates a very light touch, or it means that the device + /// Note 1: A value of `0.0` either indicates a very light touch, or it means that the device /// is not capable of measuring the touch force at all. /// /// Note 2: Just increasing the physical pressure without actually moving the finger may not - /// lead to a change of this value. + /// necessarily lead to a change of this value. pub force: f32, } @@ -67,27 +66,25 @@ pub(crate) struct TouchState { #[derive(Clone, Debug)] struct GestureState { start_time: f64, - start: DynGestureState, - previous: DynGestureState, + start_pointer_pos: Pos2, + force: f32, + previous: Option, current: DynGestureState, } /// Gesture data which can change over time #[derive(Clone, Copy, Debug)] struct DynGestureState { - pos: Pos2, - distance: f32, + avg_pos: Pos2, + avg_distance: f32, direction: f32, - force: f32, } /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as /// long as the finger/pen touches the surface. #[derive(Clone, Copy, Debug)] struct ActiveTouch { - /// Screen position where this touch was when the gesture startet - gesture_start_pos: Pos2, - /// Current screen position of this touch + /// Current position of this touch, in device coordinates (not necessarily screen position) pos: Pos2, /// Current force of the touch. A value in the interval [0.0 .. 1.0] /// @@ -105,7 +102,8 @@ impl TouchState { } } - pub fn begin_frame(&mut self, time: f64, new: &RawInput) { + pub fn begin_frame(&mut self, time: f64, new: &RawInput, pointer_pos: Option) { + let mut added_or_removed_touches = false; for event in &new.events { match *event { Event::Touch { @@ -115,135 +113,141 @@ impl TouchState { pos, force, } if device_id == self.device_id => match phase { - TouchPhase::Start => self.touch_start(id, pos, force, time), - TouchPhase::Move => self.touch_move(id, pos, force), - TouchPhase::End | TouchPhase::Cancel => self.touch_end(id, time), + TouchPhase::Start => { + self.active_touches.insert(id, ActiveTouch { pos, force }); + added_or_removed_touches = true; + } + TouchPhase::Move => { + if let Some(touch) = self.active_touches.get_mut(&id) { + touch.pos = pos; + touch.force = force; + } + } + TouchPhase::End | TouchPhase::Cancel => { + self.active_touches.remove(&id); + added_or_removed_touches = true; + } }, _ => (), } } - // This needs to be called each frame, even if there are no new touch events. Failing to - // do so may result in wrong delta information - self.update_gesture(); + // This needs to be called each frame, even if there are no new touch events. + // Otherwise, we would send the same old delta information multiple times: + self.update_gesture(time, pointer_pos); + + if added_or_removed_touches { + // Adding or removing fingers makes the average values "jump". We better forget + // about the previous values, and don't create delta information for this frame: + if let Some(ref mut state) = &mut self.gesture_state { + state.previous = None; + } + } } pub fn is_active(&self) -> bool { self.gesture_state.is_some() } - pub fn info(&self) -> Option { - self.gesture_state.as_ref().map(|state| TouchInfo { - start_time: state.start_time, - start_pos: state.start.pos, - current_pos: state.current.pos, - total: DynamicTouchInfo { - zoom: state.current.distance / state.start.distance, - rotation: normalized_angle(state.current.direction, state.start.direction), - translation: state.current.pos - state.start.pos, - force: state.current.force, - }, - incremental: DynamicTouchInfo { - zoom: state.current.distance / state.previous.distance, - rotation: normalized_angle(state.current.direction, state.previous.direction), - translation: state.current.pos - state.previous.pos, - force: state.current.force - state.previous.force, - }, + pub fn info(&self) -> Option { + self.gesture_state.as_ref().map(|state| { + // state.previous can be `None` when the number of simultaneous touches has just + // changed. In this case, we take `current` as `previous`, pretending that there + // was no change for the current frame. + let state_previous = state.previous.unwrap_or(state.current); + MultiTouchInfo { + start_time: state.start_time, + start_pos: state.start_pointer_pos, + num_touches: self.active_touches.len(), + zoom: state.current.avg_distance / state_previous.avg_distance, + rotation: normalized_angle(state.current.direction, state_previous.direction), + translation: state.current.avg_pos - state_previous.avg_pos, + force: state.force, + } }) } -} -// private methods -impl TouchState { - fn touch_start(&mut self, id: TouchId, pos: Pos2, force: f32, time: f64) { - self.active_touches.insert( - id, - ActiveTouch { - gesture_start_pos: pos, - pos, - force, - }, - ); - // for now we only support exactly two fingers: - if self.active_touches.len() == 2 { - self.start_gesture(time); + fn update_gesture(&mut self, time: f64, pointer_pos: Option) { + if let Some(avg) = self.calc_averages() { + if let Some(ref mut state) = &mut self.gesture_state { + // updating an ongoing gesture + state.force = avg.force; + state.previous = Some(state.current); + state.current.avg_pos = avg.pos; + state.current.direction = avg.direction; + state.current.avg_distance = avg.distance; + } else if let Some(pointer_pos) = pointer_pos { + // starting a new gesture + self.gesture_state = Some(GestureState { + start_time: time, + start_pointer_pos: pointer_pos, + force: avg.force, + previous: None, + current: DynGestureState { + avg_pos: avg.pos, + avg_distance: avg.distance, + direction: avg.direction, + }, + }); + } } else { - self.end_gesture() - } - } - - fn touch_move(&mut self, id: TouchId, pos: Pos2, force: f32) { - if let Some(touch) = self.active_touches.get_mut(&id) - // always true - { - touch.pos = pos; - touch.force = force; + // the end of a gesture (if there is any) + self.gesture_state = None; } } - fn touch_end(&mut self, id: TouchId, time: f64) { - self.active_touches.remove(&id); - // for now we only support exactly two fingers: - if self.active_touches.len() == 2 { - self.start_gesture(time); + fn calc_averages(&self) -> Option { + let num_touches = self.active_touches.len(); + if num_touches < 2 { + None } else { - self.end_gesture() - } - } - - fn start_gesture(&mut self, time: f64) { - for mut touch in self.active_touches.values_mut() { - touch.gesture_start_pos = touch.pos; - } - if let Some((touch1, touch2)) = self.both_touches() - // always true - { - let start_dyn_state = DynGestureState { - pos: self::center_pos(touch1.pos, touch2.pos), - distance: self::distance(touch1.pos, touch2.pos), - direction: self::direction(touch1.pos, touch2.pos), - force: (touch1.force + touch2.force) * 0.5, + let mut avg = TouchAverages { + pos: Pos2::ZERO, + force: 0., + distance: 0., + direction: 0., }; - self.gesture_state = Some(GestureState { - start_time: time, - start: start_dyn_state, - previous: start_dyn_state, - current: start_dyn_state, - }); - } - } + let num_touches_rezip = 1. / num_touches as f32; - fn update_gesture(&mut self) { - if let Some((touch1, touch2)) = self.both_touches() { - let state_new = DynGestureState { - pos: self::center_pos(touch1.pos, touch2.pos), - distance: self::distance(touch1.pos, touch2.pos), - direction: self::direction(touch1.pos, touch2.pos), - force: (touch1.force + touch2.force) * 0.5, - }; - if let Some(ref mut state) = &mut self.gesture_state - // always true - { - state.previous = state.current; - state.current = state_new; + // first pass: calculate force, and center position: + for touch in self.active_touches.values() { + avg.force += touch.force; + avg.pos.x += touch.pos.x; + avg.pos.y += touch.pos.y; } - } - } + avg.force *= num_touches_rezip; + avg.pos.x *= num_touches_rezip; + avg.pos.y *= num_touches_rezip; - fn end_gesture(&mut self) { - self.gesture_state = None; - } + // second pass: calculate distances from center: + for touch in self.active_touches.values() { + avg.distance += avg.pos.distance(touch.pos); + } + avg.distance *= num_touches_rezip; - fn both_touches(&self) -> Option<(&ActiveTouch, &ActiveTouch)> { - if self.active_touches.len() == 2 { - let mut touches = self.active_touches.values(); - let touch1 = touches.next().unwrap(); - let touch2 = touches.next().unwrap(); - Some((touch1, touch2)) - } else { - None + // Calculate the direction from the first touch to the center position. + // This is not the perfect way of calculating the direction if more than two fingers + // are involved, but as long as all fingers rotate more or less at the same angular + // velocity, the shortcomings of this method will not be noticed. One can see the + // issues though, when touching with three or more fingers, and moving only one of them + // (it takes two hands to do this in a controlled manner). A better technique would be + // to store the current and previous directions (with reference to the center) for each + // touch individually, and then calculate the average of all individual changes in + // direction. But this approach cannot be implemented locally in this method, making + // everything a bit more complicated. + let first_touch = self.active_touches.values().next().unwrap(); + let direction_vec = (avg.pos - first_touch.pos).normalized(); + avg.direction = direction_vec.y.atan2(direction_vec.x); + + Some(avg) } } } +struct TouchAverages { + pos: Pos2, + force: f32, + distance: f32, + direction: f32, +} impl TouchState { pub fn ui(&self, ui: &mut crate::Ui) { @@ -252,8 +256,7 @@ impl TouchState { } impl Debug for TouchState { - // We could just use `#[derive(Debug)]`, but the implementation below produces a less cluttered - // output: + // This outputs less clutter than `#[derive(Debug)]`: fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for (id, touch) in self.active_touches.iter() { f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?; @@ -263,19 +266,6 @@ impl Debug for TouchState { } } -fn center_pos(pos_1: Pos2, pos_2: Pos2) -> Pos2 { - pos2((pos_1.x + pos_2.x) * 0.5, (pos_1.y + pos_2.y) * 0.5) -} - -fn distance(pos_1: Pos2, pos_2: Pos2) -> f32 { - (pos_2 - pos_1).length() -} - -fn direction(pos_1: Pos2, pos_2: Pos2) -> f32 { - let v = (pos_2 - pos_1).normalized(); - v.y.atan2(v.x) -} - /// Calculate difference between two directions, such that the absolute value of the result is /// minimized. fn normalized_angle(current_direction: f32, previous_direction: f32) -> f32 { diff --git a/egui/src/lib.rs b/egui/src/lib.rs index dd64be6671c..4d32334a35e 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -340,7 +340,7 @@ pub use { }, grid::Grid, id::Id, - input_state::{InputState, PointerState}, + input_state::{InputState, MultiTouchInfo, PointerState}, layers::{LayerId, Order}, layout::*, memory::Memory, diff --git a/egui_demo_lib/src/apps/demo/zoom_rotate.rs b/egui_demo_lib/src/apps/demo/zoom_rotate.rs index 9818a609b7e..11156661291 100644 --- a/egui_demo_lib/src/apps/demo/zoom_rotate.rs +++ b/egui_demo_lib/src/apps/demo/zoom_rotate.rs @@ -4,7 +4,7 @@ use egui::{ }; pub struct ZoomRotate { - last_time: Option, + time_of_last_update: Option, rotation: f32, zoom: f32, } @@ -12,7 +12,7 @@ pub struct ZoomRotate { impl Default for ZoomRotate { fn default() -> Self { Self { - last_time: None, + time_of_last_update: None, rotation: 0., zoom: 1., } @@ -33,7 +33,6 @@ impl super::Demo for ZoomRotate { use super::View; self.ui(ui); }); - self.last_time = Some(ctx.input().time); } } @@ -44,10 +43,10 @@ impl super::View for ZoomRotate { }); ui.colored_label( Color32::RED, - "This only works on supported touch devices, like mobiles.", + "This only works on supported touch devices (like mobiles).", ); ui.separator(); - ui.label("Pinch, Zoom, or Rotate the arrow with two fingers."); + ui.label("Pinch, Zoom, or Rotate the arrow with two or more fingers."); Frame::dark_canvas(ui.style()).show(ui, |ui| { // Note that we use `Sense::drag()` although we do not use any pointer events. With // the current implementation, the fact that a touch event of two or more fingers is @@ -66,14 +65,25 @@ impl super::View for ZoomRotate { response.rect, ); - if let Some(touches) = ui.input().touches() { + let mut stroke_width = 1.; + let mut color = Color32::GRAY; + if let Some(multi_touch) = ui.input().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 *= touches.incremental.zoom; - self.rotation += touches.incremental.rotation; - // for a smooth touch experience (shouldn't this be done by egui automatically?): + self.zoom *= multi_touch.zoom; + self.rotation += multi_touch.rotation; + // touch pressure shall make the arrow thicker (not all touch devices support this): + stroke_width += 10. * multi_touch.force; + // the drawing color depends on the number of touches: + color = match multi_touch.num_touches { + 2 => Color32::GREEN, + 3 => Color32::BLUE, + 4 => Color32::YELLOW, + _ => Color32::RED, + }; + // for a smooth (non-lagging) touch experience: ui.ctx().request_repaint(); - } else if let Some(last_time) = self.last_time { + } else if let Some(last_time) = self.time_of_last_update { // This has nothing to do with the touch gesture. It just smoothly brings the // painted arrow back into its original position, for a better visual effect: let dt = ui.input().time - last_time; @@ -81,6 +91,7 @@ impl super::View for ZoomRotate { let half_life_factor = (-(2_f64.ln()) / ZOOM_ROTATE_HALF_LIFE * dt).exp() as f32; self.zoom = 1. + ((self.zoom - 1.) * half_life_factor); self.rotation *= half_life_factor; + // this is an animation, so we want real-time UI updates: ui.ctx().request_repaint(); } @@ -93,8 +104,9 @@ impl super::View for ZoomRotate { painter.arrow( to_screen * (Pos2::ZERO + arrow_start), to_screen.scale() * arrow_direction, - Stroke::new(1.0, Color32::YELLOW), + Stroke::new(stroke_width, color), ); + self.time_of_last_update = Some(ui.input().time); }); } } From bd5b909f504708eaee442a4e792a84b530b854e8 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Mon, 26 Apr 2021 22:44:03 +0200 Subject: [PATCH 16/28] cleanup code and comments for review --- egui/src/data/input.rs | 3 -- egui/src/input_state.rs | 39 +++++++++++++++++++--- egui/src/input_state/touch_state.rs | 12 +++---- egui_demo_lib/src/apps/demo/zoom_rotate.rs | 21 +++++++----- egui_web/src/lib.rs | 11 +++--- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index 71b5dcf584a..d80797a0eda 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -148,9 +148,6 @@ pub enum Event { /// not support pressure sensitivity. /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). force: f32, - // ### Note for review: using f32 forced me to remove `#[derive(Eq)]` - // from the `Event` struct. Can this cause issues? I did not get errors, - // so I wonder if `Eq` had been derived on purpose. }, } diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index bc1b5422696..4f16632c50d 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -135,7 +135,15 @@ impl InputState { /// * `zoom > 1`: pinch spread #[inline(always)] pub fn zoom_delta(&self) -> f32 { - self.raw.zoom_delta + // decide whether to use the factor from a synthetic ctrl-scroll event or from native touch + // events + if let Some(touch) = self.multi_touch() { + // If a multi touch gesture is detected, its zoom factor is more accurate because it + // measures the exact and linear proportions of the distances of the finger tips + touch.zoom_delta + } else { + self.raw.zoom_delta + } } pub fn wants_repaint(&self) -> bool { @@ -203,10 +211,33 @@ impl InputState { self.physical_pixel_size() } - /// Details about the currently ongoing multi-touch gesture, if any. See [`MultiTouchInfo`]. + /// Returns details about the currently ongoing multi-touch gesture, if any. Note that this + /// method returns `None` for single-touch gestures (click, drag, …). + /// + /// ``` + /// # use egui::emath::Rot2; + /// # let ui = &mut egui::Ui::__test(); + /// let mut zoom = 1.0; // no zoom + /// let mut rotation = 0.0; // no rotation + /// if let Some(multi_touch) = ui.input().multi_touch() { + /// zoom *= multi_touch.zoom_delta; + /// rotation += multi_touch.rotation_delta; + /// } + /// let transform = zoom * Rot2::from_angle(rotation); + /// ``` + /// + /// By far not all touch devices are supported, and the details depend on the `egui` + /// integration backend you are using. `egui_web` supports multi touch for most mobile + /// devices, but not for a `Trackpad` on `MacOS`, for example. The backend has to be able to + /// capture native touch events, but many browsers seem to pass such events only for touch + /// _screens_, but not touch _pads._ + /// + /// Refer to [`MultiTouchInfo`] for details about the touch information available. + /// + /// Consider using `zoom_delta()` instead of `MultiTouchInfo::zoom_delta` as the former + /// delivers a synthetic zoom factor based on ctrl-scroll events, as a fallback. pub fn multi_touch(&self) -> Option { - // In case of multiple touch devices simply pick the touch_state for the first active - // device + // In case of multiple touch devices simply pick the touch_state of the first active device if let Some(touch_state) = self.touch_states.values().find(|t| t.is_active()) { touch_state.info() } else { diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 0e7613a97f8..166d3aeb015 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -20,11 +20,11 @@ pub struct MultiTouchInfo { /// this value. This is a relative value, comparing the average distances of the fingers in /// the current and previous frame. If the fingers did not move since the previous frame, /// this value is `1.0`. - pub zoom: f32, + pub zoom_delta: f32, /// Rotation in radians. Moving fingers around each other will change this value. This is a /// relative value, comparing the orientation of fingers in the current frame with the previous /// frame. If all fingers are resting, this value is `0.0`. - pub rotation: f32, + pub rotation_delta: f32, /// Relative movement (comparing previous frame and current frame) of the average position of /// all touch points. Without movement this value is `Vec2::ZERO`. /// @@ -33,7 +33,7 @@ pub struct MultiTouchInfo { /// be directly mapped to the screen. A touch always is considered to start at the position of /// the pointer, but touch movement is always measured in the units delivered by the device, /// and may depend on hardware and system settings. - pub translation: Vec2, + pub translation_delta: Vec2, /// Current force of the touch (average of the forces of the individual fingers). This is a /// value in the interval `[0.0 .. =1.0]`. /// @@ -158,9 +158,9 @@ impl TouchState { start_time: state.start_time, start_pos: state.start_pointer_pos, num_touches: self.active_touches.len(), - zoom: state.current.avg_distance / state_previous.avg_distance, - rotation: normalized_angle(state.current.direction, state_previous.direction), - translation: state.current.avg_pos - state_previous.avg_pos, + zoom_delta: state.current.avg_distance / state_previous.avg_distance, + rotation_delta: normalized_angle(state.current.direction, state_previous.direction), + translation_delta: state.current.avg_pos - state_previous.avg_pos, force: state.force, } }) diff --git a/egui_demo_lib/src/apps/demo/zoom_rotate.rs b/egui_demo_lib/src/apps/demo/zoom_rotate.rs index 11156661291..e9f689e7124 100644 --- a/egui_demo_lib/src/apps/demo/zoom_rotate.rs +++ b/egui_demo_lib/src/apps/demo/zoom_rotate.rs @@ -21,7 +21,7 @@ impl Default for ZoomRotate { impl super::Demo for ZoomRotate { fn name(&self) -> &'static str { - "👌 Zoom/Rotate" + "👌 Multi Touch" } fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { @@ -43,10 +43,10 @@ impl super::View for ZoomRotate { }); ui.colored_label( Color32::RED, - "This only works on supported touch devices (like mobiles).", + "This only works on devices which send native touch events (mostly mobiles).", ); ui.separator(); - ui.label("Pinch, Zoom, or Rotate the arrow with two or more fingers."); + ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers."); Frame::dark_canvas(ui.style()).show(ui, |ui| { // Note that we use `Sense::drag()` although we do not use any pointer events. With // the current implementation, the fact that a touch event of two or more fingers is @@ -56,6 +56,8 @@ impl super::View for ZoomRotate { // also when a two-finger touch is active. I guess this problem can only be cleanly // solved when the synthetic pointer events are created by egui, and not by the // backend. + + // set up the drawing canvas with normalized coordinates: let (response, painter) = ui.allocate_painter(ui.available_size_before_wrap_finite(), Sense::drag()); // normalize painter coordinates to ±1 units in each direction with [0,0] in the center: @@ -65,13 +67,15 @@ impl super::View for ZoomRotate { response.rect, ); + // check for touch input (or the lack thereof) and update zoom and scale factors, plus + // color and width: let mut stroke_width = 1.; let mut color = Color32::GRAY; if let Some(multi_touch) = ui.input().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; - self.rotation += multi_touch.rotation; + self.zoom *= multi_touch.zoom_delta; + self.rotation += multi_touch.rotation_delta; // touch pressure shall make the arrow thicker (not all touch devices support this): stroke_width += 10. * multi_touch.force; // the drawing color depends on the number of touches: @@ -81,20 +85,21 @@ impl super::View for ZoomRotate { 4 => Color32::YELLOW, _ => Color32::RED, }; - // for a smooth (non-lagging) touch experience: + // for a smooth touch experience (not strictly required, but I had the impression + // that it helps to reduce some lag, especially for the initial touch): ui.ctx().request_repaint(); } else if let Some(last_time) = self.time_of_last_update { // This has nothing to do with the touch gesture. It just smoothly brings the - // painted arrow back into its original position, for a better visual effect: + // painted arrow back into its original position, for a nice visual effect: let dt = ui.input().time - last_time; const ZOOM_ROTATE_HALF_LIFE: f64 = 1.; // time[sec] after which half the amount of zoom/rotation will be reverted let half_life_factor = (-(2_f64.ln()) / ZOOM_ROTATE_HALF_LIFE * dt).exp() as f32; self.zoom = 1. + ((self.zoom - 1.) * half_life_factor); self.rotation *= half_life_factor; - // this is an animation, so we want real-time UI updates: ui.ctx().request_repaint(); } + let zoom_and_rotate = self.zoom * Rot2::from_angle(self.rotation); // Paints an arrow pointing from bottom-left (-0.5, 0.5) to top-right (0.5, -0.5), diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index a21f9cb37bd..723d28e42f9 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -117,8 +117,7 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option egui::Pos2 { // TO BE CLARIFIED: For some types of touch events (e.g. `touchcancel`) it may be necessary to // change the return type to an `Option` – but this would be an incompatible change. Can - // we do this? But then, I am not sure yet if a `touchcancel` event does not even have at - // least one `Touch`. + // we do this? // Calculate the average of all touch positions: let touch_count = event.touches().length(); @@ -935,6 +934,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { pressed: true, modifiers, }); + push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event); runner_lock.needs_repaint.set_true(); event.stop_propagation(); @@ -952,10 +952,6 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let pos = pos_from_touch_event(runner_lock.canvas_id(), &event); runner_lock.input.latest_touch_pos = Some(pos); runner_lock.input.is_touch = true; - - // TO BE DISCUSSED: todo for all `touch*`-events: - // Now that egui knows about Touch events, the backend does not need to simulate - // Pointer events, any more. This simulation could be moved to `egui`. runner_lock .input .raw @@ -990,9 +986,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { pressed: false, modifiers, }); - push_touches(&mut *runner_lock, egui::TouchPhase::End, &event); // Then remove hover effect: runner_lock.input.raw.events.push(egui::Event::PointerGone); + + push_touches(&mut *runner_lock, egui::TouchPhase::End, &event); runner_lock.needs_repaint.set_true(); event.stop_propagation(); event.prevent_default(); From cc054b8f7e92e8db688e4db74b6a1a4789cc5e5e Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:01:15 +0200 Subject: [PATCH 17/28] minor code simplifications --- egui/src/input_state.rs | 16 +++++++--------- egui_web/src/lib.rs | 20 +++++++------------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 4f16632c50d..6c292e4811e 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -135,15 +135,13 @@ impl InputState { /// * `zoom > 1`: pinch spread #[inline(always)] pub fn zoom_delta(&self) -> f32 { - // decide whether to use the factor from a synthetic ctrl-scroll event or from native touch - // events - if let Some(touch) = self.multi_touch() { - // If a multi touch gesture is detected, its zoom factor is more accurate because it - // measures the exact and linear proportions of the distances of the finger tips - touch.zoom_delta - } else { - self.raw.zoom_delta - } + // If a multi touch gesture is detected, it measures the exact and linear proportions of + // the distances of the finger tips. It is therefore potentially more accurate than + // `raw.zoom_delta` which is based on the `ctrl-scroll` event which, in turn, may be + // synthesized from an original touch gesture. + self.multi_touch() + .map(|touch| touch.zoom_delta) + .unwrap_or(self.raw.zoom_delta) } pub fn wants_repaint(&self) -> bool { diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 723d28e42f9..4443b9f0b95 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -120,23 +120,17 @@ pub fn pos_from_touch_event(canvas_id: &str, event: &web_sys::TouchEvent) -> egu // we do this? // Calculate the average of all touch positions: + let mut accu = egui::Vec2::ZERO; let touch_count = event.touches().length(); - if touch_count == 0 { - egui::Pos2::ZERO // work-around for not returning an `Option` - } else { + if touch_count > 0 { let canvas_origin = canvas_origin(canvas_id); - let mut sum = pos_from_touch(canvas_origin, &event.touches().get(0).unwrap()); - if touch_count == 1 { - sum - } else { - for touch_idx in 1..touch_count { - let touch = event.touches().get(touch_idx).unwrap(); - sum += pos_from_touch(canvas_origin, &touch).to_vec2(); - } - let touch_count_recip = 1. / touch_count as f32; - egui::Pos2::new(sum.x * touch_count_recip, sum.y * touch_count_recip) + for touch_idx in 0..touch_count { + let touch = event.touches().get(touch_idx).unwrap(); + accu += pos_from_touch(canvas_origin, &touch).to_vec2(); } + accu = accu / touch_count as f32; } + egui::Pos2::ZERO + accu } fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 { From fee8ed83dbe715b5b70433faacfe74b59c99e4a4 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:21:56 +0200 Subject: [PATCH 18/28] =?UTF-8?q?oops=20=E2=80=93=20forgot=20the=20changel?= =?UTF-8?q?og?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34f0e38fe88..da89f6138b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ * [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. +* Support for raw [multi touch](https://github.com/emilk/egui/pull/306) events, + enabling zoom, rotate, and more. Works with `egui_web` on mobile devices, + and should work with `egui_glium` for certain touch devices/screens. ### Changed 🔧 * Make `Memory::has_focus` public (again). From ce8f3a8bedde6c67408c78d542e204f57b8e764f Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Thu, 29 Apr 2021 20:50:29 +0200 Subject: [PATCH 19/28] resolve comment https://github.com/emilk/egui/pull/306/files/fee8ed83dbe715b5b70433faacfe74b59c99e4a4#r623226656 --- egui/src/data/input.rs | 6 ++++-- egui/src/input_state.rs | 2 +- egui_glium/src/lib.rs | 4 ++-- egui_web/src/lib.rs | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index d80797a0eda..548ab62b855 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -314,12 +314,14 @@ impl RawInput { } /// this is a `u64` as values of this kind can always be obtained by hashing -pub type TouchDeviceId = u64; +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct TouchDeviceId(pub u64); /// Unique identifiction of a touch occurence (finger or pen or ...). /// A Touch ID is valid until the finger is lifted. /// A new ID is used for the next touch. -pub type TouchId = u64; +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct TouchId(pub u64); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TouchPhase { diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 6c292e4811e..5c0ecf8e9e1 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -606,7 +606,7 @@ impl InputState { }); for (device_id, touch_state) in touch_states { - ui.collapsing(format!("Touch State [device {}]", device_id), |ui| { + ui.collapsing(format!("Touch State [device {}]", device_id.0), |ui| { touch_state.ui(ui) }); } diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 3502deb528e..5298b50f1a8 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -202,8 +202,8 @@ pub fn input_to_egui( let mut hasher = std::collections::hash_map::DefaultHasher::new(); touch.device_id.hash(&mut hasher); input_state.raw.events.push(Event::Touch { - device_id: hasher.finish(), - id: touch.id, + device_id: TouchDeviceId(hasher.finish()), + id: TouchId(touch.id), phase: match touch.phase { glutin::event::TouchPhase::Started => egui::TouchPhase::Start, glutin::event::TouchPhase::Moved => egui::TouchPhase::Move, diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 4443b9f0b95..089c53a3346 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -152,8 +152,8 @@ fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys for touch_idx in 0..event.changed_touches().length() { if let Some(touch) = event.changed_touches().item(touch_idx) { runner.input.raw.events.push(egui::Event::Touch { - device_id: 0, - id: touch.identifier() as u64, + device_id: egui::TouchDeviceId(0), + id: egui::TouchId(touch.identifier() as u64), phase, pos: pos_from_touch(canvas_origin, &touch), force: touch.force(), From 9c6c6b1a3460dad61838a0b2b1c42bad92b3eba2 Mon Sep 17 00:00:00 2001 From: Ivo Vollrath <57874618+quadruple-output@users.noreply.github.com> Date: Thu, 29 Apr 2021 20:58:43 +0200 Subject: [PATCH 20/28] accept suggestion https://github.com/emilk/egui/pull/306#discussion_r623229228 Co-authored-by: Emil Ernerfeldt --- egui/src/input_state.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 5c0ecf8e9e1..2d47785e9d1 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -248,10 +248,7 @@ impl InputState { fn create_touch_states_for_new_devices(&mut self, events: &[Event]) { for event in events { if let Event::Touch { device_id, .. } = event { - if !self.touch_states.contains_key(device_id) { - self.touch_states - .insert(*device_id, TouchState::new(*device_id)); - } + self.touch_states.entry(*device_id).or_insert_with(|| TouchState::new(*device_id))); } } } From 5c27120b48c002ee1535e6b2bd03ed46f3af98a3 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Thu, 29 Apr 2021 21:18:06 +0200 Subject: [PATCH 21/28] fix syntax error (dough!) --- egui/src/input_state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 2d47785e9d1..c4ed98b41a5 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -248,7 +248,9 @@ impl InputState { fn create_touch_states_for_new_devices(&mut self, events: &[Event]) { for event in events { if let Event::Touch { device_id, .. } = event { - self.touch_states.entry(*device_id).or_insert_with(|| TouchState::new(*device_id))); + self.touch_states + .entry(*device_id) + .or_insert_with(|| TouchState::new(*device_id)); } } } From 612069a498369a0f59e3ca4259637ec9df625ccb Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Thu, 29 Apr 2021 22:02:23 +0200 Subject: [PATCH 22/28] remove `dbg!` (why didnt clippy see this?) --- egui/src/input_state/touch_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 166d3aeb015..85be98de7a9 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -276,7 +276,7 @@ fn normalized_angle(current_direction: f32, previous_direction: f32) -> f32 { } else if angle < -PI { angle += TAU; } - dbg!(angle) + angle } #[test] From e3cc56ef6f6f6297312c3ba056d217ef5bea81a9 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Thu, 29 Apr 2021 22:32:54 +0200 Subject: [PATCH 23/28] apply suggested diffs from review --- egui/src/input_state/touch_state.rs | 3 +-- egui_demo_lib/src/apps/demo/zoom_rotate.rs | 11 ++++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index 85be98de7a9..f6c136f15dd 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -235,8 +235,7 @@ impl TouchState { // direction. But this approach cannot be implemented locally in this method, making // everything a bit more complicated. let first_touch = self.active_touches.values().next().unwrap(); - let direction_vec = (avg.pos - first_touch.pos).normalized(); - avg.direction = direction_vec.y.atan2(direction_vec.x); + avg.direction = (avg.pos - first_touch.pos).angle(); Some(avg) } diff --git a/egui_demo_lib/src/apps/demo/zoom_rotate.rs b/egui_demo_lib/src/apps/demo/zoom_rotate.rs index e9f689e7124..7f931a29d25 100644 --- a/egui_demo_lib/src/apps/demo/zoom_rotate.rs +++ b/egui_demo_lib/src/apps/demo/zoom_rotate.rs @@ -4,7 +4,6 @@ use egui::{ }; pub struct ZoomRotate { - time_of_last_update: Option, rotation: f32, zoom: f32, } @@ -12,7 +11,6 @@ pub struct ZoomRotate { impl Default for ZoomRotate { fn default() -> Self { Self { - time_of_last_update: None, rotation: 0., zoom: 1., } @@ -88,12 +86,12 @@ impl super::View for ZoomRotate { // for a smooth touch experience (not strictly required, but I had the impression // that it helps to reduce some lag, especially for the initial touch): ui.ctx().request_repaint(); - } else if let Some(last_time) = self.time_of_last_update { + } else { // This has nothing to do with the touch gesture. It just smoothly brings the // painted arrow back into its original position, for a nice visual effect: - let dt = ui.input().time - last_time; - const ZOOM_ROTATE_HALF_LIFE: f64 = 1.; // time[sec] after which half the amount of zoom/rotation will be reverted - let half_life_factor = (-(2_f64.ln()) / ZOOM_ROTATE_HALF_LIFE * dt).exp() as f32; + let dt = ui.input().unstable_dt; + const ZOOM_ROTATE_HALF_LIFE: f32 = 1.; // time[sec] after which half the amount of zoom/rotation will be reverted + let half_life_factor = (-(2_f32.ln()) / ZOOM_ROTATE_HALF_LIFE * dt).exp(); self.zoom = 1. + ((self.zoom - 1.) * half_life_factor); self.rotation *= half_life_factor; // this is an animation, so we want real-time UI updates: @@ -111,7 +109,6 @@ impl super::View for ZoomRotate { to_screen.scale() * arrow_direction, Stroke::new(stroke_width, color), ); - self.time_of_last_update = Some(ui.input().time); }); } } From e4c9a65d57560385fa35701fe5119a6de210e840 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Thu, 29 Apr 2021 23:03:49 +0200 Subject: [PATCH 24/28] fix conversion of physical location to Pos2 --- egui_glium/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 5298b50f1a8..8ce218438b4 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -199,6 +199,7 @@ pub fn input_to_egui( // TODO } WindowEvent::Touch(touch) => { + let pixels_per_point_recip = 1. / pixels_per_point; let mut hasher = std::collections::hash_map::DefaultHasher::new(); touch.device_id.hash(&mut hasher); input_state.raw.events.push(Event::Touch { @@ -210,7 +211,8 @@ pub fn input_to_egui( glutin::event::TouchPhase::Ended => egui::TouchPhase::End, glutin::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel, }, - pos: Pos2::new(touch.location.x as f32, touch.location.y as f32), + pos: pos2(touch.location.x as f32 * pixels_per_point_recip, + touch.location.y as f32 * pixels_per_point_recip), force: match touch.force { Some(Force::Normalized(force)) => force as f32, Some(Force::Calibrated { From 3918653979261912606a2a6d184c0c964fdcc1ce Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Sat, 1 May 2021 16:38:01 +0200 Subject: [PATCH 25/28] remove redundanct type `TouchAverages` --- egui/src/input_state/touch_state.rs | 68 ++++++++++++----------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/egui/src/input_state/touch_state.rs b/egui/src/input_state/touch_state.rs index f6c136f15dd..bcdb92520d8 100644 --- a/egui/src/input_state/touch_state.rs +++ b/egui/src/input_state/touch_state.rs @@ -67,17 +67,17 @@ pub(crate) struct TouchState { struct GestureState { start_time: f64, start_pointer_pos: Pos2, - force: f32, previous: Option, current: DynGestureState, } -/// Gesture data which can change over time +/// Gesture data that can change over time #[derive(Clone, Copy, Debug)] struct DynGestureState { - avg_pos: Pos2, avg_distance: f32, - direction: f32, + avg_pos: Pos2, + avg_force: f32, + heading: f32, } /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as @@ -159,34 +159,26 @@ impl TouchState { start_pos: state.start_pointer_pos, num_touches: self.active_touches.len(), zoom_delta: state.current.avg_distance / state_previous.avg_distance, - rotation_delta: normalized_angle(state.current.direction, state_previous.direction), + rotation_delta: normalized_angle(state.current.heading, state_previous.heading), translation_delta: state.current.avg_pos - state_previous.avg_pos, - force: state.force, + force: state.current.avg_force, } }) } fn update_gesture(&mut self, time: f64, pointer_pos: Option) { - if let Some(avg) = self.calc_averages() { + if let Some(dyn_state) = self.calc_dynamic_state() { if let Some(ref mut state) = &mut self.gesture_state { // updating an ongoing gesture - state.force = avg.force; state.previous = Some(state.current); - state.current.avg_pos = avg.pos; - state.current.direction = avg.direction; - state.current.avg_distance = avg.distance; + state.current = dyn_state; } else if let Some(pointer_pos) = pointer_pos { // starting a new gesture self.gesture_state = Some(GestureState { start_time: time, start_pointer_pos: pointer_pos, - force: avg.force, previous: None, - current: DynGestureState { - avg_pos: avg.pos, - avg_distance: avg.distance, - direction: avg.direction, - }, + current: dyn_state, }); } } else { @@ -195,34 +187,34 @@ impl TouchState { } } - fn calc_averages(&self) -> Option { + fn calc_dynamic_state(&self) -> Option { let num_touches = self.active_touches.len(); if num_touches < 2 { None } else { - let mut avg = TouchAverages { - pos: Pos2::ZERO, - force: 0., - distance: 0., - direction: 0., + let mut state = DynGestureState { + avg_distance: 0., + avg_pos: Pos2::ZERO, + avg_force: 0., + heading: 0., }; - let num_touches_rezip = 1. / num_touches as f32; + let num_touches_recip = 1. / num_touches as f32; - // first pass: calculate force, and center position: + // first pass: calculate force and center of touch positions: for touch in self.active_touches.values() { - avg.force += touch.force; - avg.pos.x += touch.pos.x; - avg.pos.y += touch.pos.y; + state.avg_force += touch.force; + state.avg_pos.x += touch.pos.x; + state.avg_pos.y += touch.pos.y; } - avg.force *= num_touches_rezip; - avg.pos.x *= num_touches_rezip; - avg.pos.y *= num_touches_rezip; + state.avg_force *= num_touches_recip; + state.avg_pos.x *= num_touches_recip; + state.avg_pos.y *= num_touches_recip; // second pass: calculate distances from center: for touch in self.active_touches.values() { - avg.distance += avg.pos.distance(touch.pos); + state.avg_distance += state.avg_pos.distance(touch.pos); } - avg.distance *= num_touches_rezip; + state.avg_distance *= num_touches_recip; // Calculate the direction from the first touch to the center position. // This is not the perfect way of calculating the direction if more than two fingers @@ -235,18 +227,12 @@ impl TouchState { // direction. But this approach cannot be implemented locally in this method, making // everything a bit more complicated. let first_touch = self.active_touches.values().next().unwrap(); - avg.direction = (avg.pos - first_touch.pos).angle(); + state.heading = (state.avg_pos - first_touch.pos).angle(); - Some(avg) + Some(state) } } } -struct TouchAverages { - pos: Pos2, - force: f32, - distance: f32, - direction: f32, -} impl TouchState { pub fn ui(&self, ui: &mut crate::Ui) { From b38e2255f347a65d42645ab13ac02223b4aaca1b Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Mon, 3 May 2021 13:27:46 +0200 Subject: [PATCH 26/28] remove trailing space --- egui_glium/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 8ce218438b4..0a24f3842bf 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -211,7 +211,7 @@ pub fn input_to_egui( glutin::event::TouchPhase::Ended => egui::TouchPhase::End, glutin::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel, }, - pos: pos2(touch.location.x as f32 * pixels_per_point_recip, + pos: pos2(touch.location.x as f32 * pixels_per_point_recip, touch.location.y as f32 * pixels_per_point_recip), force: match touch.force { Some(Force::Normalized(force)) => force as f32, From acbefdac08b420e61b468a5cf7af5c79971c77a6 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Mon, 3 May 2021 23:40:17 +0200 Subject: [PATCH 27/28] avoid initial translation jump in plot demo --- egui/src/data/input.rs | 18 +++++++++++++ egui_glium/src/lib.rs | 2 +- egui_web/src/backend.rs | 3 +++ egui_web/src/lib.rs | 58 +++++++++++++++++++++++++++-------------- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index 548ab62b855..a2c1b49ef3b 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -338,3 +338,21 @@ pub enum TouchPhase { /// been intended by the user) Cancel, } + +impl From for TouchId { + fn from(id: u64) -> Self { + Self(id) + } +} + +impl From for TouchId { + fn from(id: i32) -> Self { + Self(id as u64) + } +} + +impl From for TouchId { + fn from(id: u32) -> Self { + Self(id as u64) + } +} diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 0a24f3842bf..d8f310e7c4e 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -204,7 +204,7 @@ pub fn input_to_egui( touch.device_id.hash(&mut hasher); input_state.raw.events.push(Event::Touch { device_id: TouchDeviceId(hasher.finish()), - id: TouchId(touch.id), + id: TouchId::from(touch.id), phase: match touch.phase { glutin::event::TouchPhase::Started => egui::TouchPhase::Start, glutin::event::TouchPhase::Moved => egui::TouchPhase::Move, diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 4f5d89ec411..a9dd9493326 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -84,6 +84,9 @@ pub struct WebInput { /// Required because we don't get a position on touched pub latest_touch_pos: Option, + /// Required to maintain a stable touch position for multi-touch gestures. + pub latest_touch_pos_id: Option, + pub raw: egui::RawInput, } diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 089c53a3346..2b327d2762b 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -114,23 +114,37 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option egui::Pos2 { - // TO BE CLARIFIED: For some types of touch events (e.g. `touchcancel`) it may be necessary to - // change the return type to an `Option` – but this would be an incompatible change. Can - // we do this? - - // Calculate the average of all touch positions: - let mut accu = egui::Vec2::ZERO; - let touch_count = event.touches().length(); - if touch_count > 0 { - let canvas_origin = canvas_origin(canvas_id); - for touch_idx in 0..touch_count { - let touch = event.touches().get(touch_idx).unwrap(); - accu += pos_from_touch(canvas_origin, &touch).to_vec2(); - } - accu = accu / touch_count as f32; +/// A single touch is translated to a pointer movement. When a second touch is added, the pointer +/// should not jump to a different position. Therefore, we do not calculate the average position +/// of all touches, but we keep using the same touch as long as it is available. +/// +/// `touch_id_for_pos` is the `TouchId` of the `Touch` we previously used to determine the +/// pointer position. +pub fn pos_from_touch_event( + canvas_id: &str, + event: &web_sys::TouchEvent, + touch_id_for_pos: &mut Option, +) -> egui::Pos2 { + let touch_for_pos; + if let Some(touch_id_for_pos) = touch_id_for_pos { + // search for the touch we previously used for the position + // (unfortunately, `event.touches()` is not a rust collection): + touch_for_pos = (0..event.touches().length()) + .into_iter() + .map(|i| event.touches().get(i).unwrap()) + .find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos); + } else { + touch_for_pos = None; } - egui::Pos2::ZERO + accu + // Use the touch found above or pick the first, or return a default position if there is no + // touch at all. (The latter is not expected as the current method is only called when there is + // at least one touch.) + touch_for_pos + .or_else(|| event.touches().get(0)) + .map_or(Default::default(), |touch| { + *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); + pos_from_touch(canvas_origin(canvas_id), &touch) + }) } fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 { @@ -153,7 +167,7 @@ fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys if let Some(touch) = event.changed_touches().item(touch_idx) { runner.input.raw.events.push(egui::Event::Touch { device_id: egui::TouchDeviceId(0), - id: egui::TouchId(touch.identifier() as u64), + id: egui::TouchId::from(touch.identifier()), phase, pos: pos_from_touch(canvas_origin, &touch), force: touch.force(), @@ -914,7 +928,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let runner_ref = runner_ref.clone(); let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { let mut runner_lock = runner_ref.0.lock(); - let pos = pos_from_touch_event(runner_lock.canvas_id(), &event); + let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id; + let pos = + pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id); + runner_lock.input.latest_touch_pos_id = latest_touch_pos_id; runner_lock.input.latest_touch_pos = Some(pos); runner_lock.input.is_touch = true; let modifiers = runner_lock.input.raw.modifiers; @@ -943,7 +960,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let runner_ref = runner_ref.clone(); let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { let mut runner_lock = runner_ref.0.lock(); - let pos = pos_from_touch_event(runner_lock.canvas_id(), &event); + let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id; + let pos = + pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id); + runner_lock.input.latest_touch_pos_id = latest_touch_pos_id; runner_lock.input.latest_touch_pos = Some(pos); runner_lock.input.is_touch = true; runner_lock From 755ca301a08b3d99fc0cca9dca9c7642c4be5d16 Mon Sep 17 00:00:00 2001 From: quadruple-output <57874618+quadruple-output@users.noreply.github.com> Date: Wed, 5 May 2021 23:56:08 +0200 Subject: [PATCH 28/28] extend the demo so it shows off translation --- egui_demo_lib/src/apps/demo/zoom_rotate.rs | 50 ++++++++++++++++------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/zoom_rotate.rs b/egui_demo_lib/src/apps/demo/zoom_rotate.rs index 7f931a29d25..76e256bc178 100644 --- a/egui_demo_lib/src/apps/demo/zoom_rotate.rs +++ b/egui_demo_lib/src/apps/demo/zoom_rotate.rs @@ -1,17 +1,23 @@ use egui::{ emath::{RectTransform, Rot2}, - vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, + vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2, }; pub struct ZoomRotate { + previous_arrow_start_offset: Vec2, rotation: f32, + smoothed_velocity: Vec2, + translation: Vec2, zoom: f32, } impl Default for ZoomRotate { fn default() -> Self { Self { + previous_arrow_start_offset: Vec2::ZERO, rotation: 0., + smoothed_velocity: Vec2::ZERO, + translation: Vec2::ZERO, zoom: 1., } } @@ -64,6 +70,7 @@ impl super::View for ZoomRotate { Rect::from_min_size(Pos2::ZERO - painter_proportions, 2. * painter_proportions), response.rect, ); + let dt = ui.input().unstable_dt; // check for touch input (or the lack thereof) and update zoom and scale factors, plus // color and width: @@ -74,6 +81,9 @@ impl super::View for ZoomRotate { // 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 shall make the arrow thicker (not all touch devices support this): stroke_width += 10. * multi_touch.force; // the drawing color depends on the number of touches: @@ -83,32 +93,48 @@ impl super::View for ZoomRotate { 4 => Color32::YELLOW, _ => Color32::RED, }; - // for a smooth touch experience (not strictly required, but I had the impression - // that it helps to reduce some lag, especially for the initial touch): - ui.ctx().request_repaint(); } else { // This has nothing to do with the touch gesture. It just smoothly brings the // painted arrow back into its original position, for a nice visual effect: - let dt = ui.input().unstable_dt; const ZOOM_ROTATE_HALF_LIFE: f32 = 1.; // time[sec] after which half the amount of zoom/rotation will be reverted let half_life_factor = (-(2_f32.ln()) / ZOOM_ROTATE_HALF_LIFE * dt).exp(); self.zoom = 1. + ((self.zoom - 1.) * half_life_factor); self.rotation *= half_life_factor; - // this is an animation, so we want real-time UI updates: - ui.ctx().request_repaint(); + self.translation *= half_life_factor; } - let zoom_and_rotate = self.zoom * Rot2::from_angle(self.rotation); + let arrow_start_offset = self.translation + zoom_and_rotate * vec2(-0.5, 0.5); + let current_velocity = (arrow_start_offset - self.previous_arrow_start_offset) / dt; + self.previous_arrow_start_offset = arrow_start_offset; + + // aggregate the average velocity of the arrow's start position from latest samples: + const NUM_SMOOTHING_SAMPLES: f32 = 10.; + self.smoothed_velocity = ((NUM_SMOOTHING_SAMPLES - 1.) * self.smoothed_velocity + + current_velocity) + / NUM_SMOOTHING_SAMPLES; - // Paints an arrow pointing from bottom-left (-0.5, 0.5) to top-right (0.5, -0.5), - // but scaled and rotated according to the current translation: - let arrow_start = zoom_and_rotate * vec2(-0.5, 0.5); + // Paints an arrow pointing from bottom-left (-0.5, 0.5) to top-right (0.5, -0.5), but + // scaled, rotated, and translated according to the current touch gesture: + let arrow_start = Pos2::ZERO + arrow_start_offset; let arrow_direction = zoom_and_rotate * vec2(1., -1.); painter.arrow( - to_screen * (Pos2::ZERO + arrow_start), + to_screen * arrow_start, to_screen.scale() * arrow_direction, Stroke::new(stroke_width, color), ); + // Paints a circle at the origin of the arrow. The size and opacity of the circle + // depend on the current velocity, and the circle is translated in the opposite + // direction of the movement, so it follows the origin's movement. Constant factors + // have been determined by trial and error. + let speed = self.smoothed_velocity.length(); + painter.circle_filled( + to_screen * (arrow_start - 0.2 * self.smoothed_velocity), + 2. + to_screen.scale().length() * 0.1 * speed, + Color32::RED.linear_multiply(1. / (1. + (5. * speed).powi(2))), + ); + + // we want continuous UI updates, so the circle can smoothly follow the arrow's origin: + ui.ctx().request_repaint(); }); } }