egui/input_state/touch_state.rs
1use std::{collections::BTreeMap, fmt::Debug};
2
3use crate::{
4 data::input::TouchDeviceId,
5 emath::{normalized_angle, Pos2, Vec2},
6 Event, RawInput, TouchId, TouchPhase,
7};
8
9/// All you probably need to know about a multi-touch gesture.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct MultiTouchInfo {
12 /// Point in time when the gesture started.
13 pub start_time: f64,
14
15 /// Position of the pointer at the time the gesture started.
16 pub start_pos: Pos2,
17
18 /// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no
19 /// [`MultiTouchInfo`] is created.
20 pub num_touches: usize,
21
22 /// Proportional zoom factor (pinch gesture).
23 /// * `zoom = 1`: no change
24 /// * `zoom < 1`: pinch together
25 /// * `zoom > 1`: pinch spread
26 pub zoom_delta: f32,
27
28 /// 2D non-proportional zoom factor (pinch gesture).
29 ///
30 /// For horizontal pinches, this will return `[z, 1]`,
31 /// for vertical pinches this will return `[1, z]`,
32 /// and otherwise this will return `[z, z]`,
33 /// where `z` is the zoom factor:
34 /// * `zoom = 1`: no change
35 /// * `zoom < 1`: pinch together
36 /// * `zoom > 1`: pinch spread
37 pub zoom_delta_2d: Vec2,
38
39 /// Rotation in radians. Moving fingers around each other will change this value. This is a
40 /// relative value, comparing the orientation of fingers in the current frame with the previous
41 /// frame. If all fingers are resting, this value is `0.0`.
42 pub rotation_delta: f32,
43
44 /// Relative movement (comparing previous frame and current frame) of the average position of
45 /// all touch points. Without movement this value is `Vec2::ZERO`.
46 ///
47 /// Note that this may not necessarily be measured in screen points (although it _will_ be for
48 /// most mobile devices). In general (depending on the touch device), touch coordinates cannot
49 /// be directly mapped to the screen. A touch always is considered to start at the position of
50 /// the pointer, but touch movement is always measured in the units delivered by the device,
51 /// and may depend on hardware and system settings.
52 pub translation_delta: Vec2,
53
54 /// Current force of the touch (average of the forces of the individual fingers). This is a
55 /// value in the interval `[0.0 .. =1.0]`.
56 ///
57 /// Note 1: A value of `0.0` either indicates a very light touch, or it means that the device
58 /// is not capable of measuring the touch force at all.
59 ///
60 /// Note 2: Just increasing the physical pressure without actually moving the finger may not
61 /// necessarily lead to a change of this value.
62 pub force: f32,
63}
64
65/// The current state (for a specific touch device) of touch events and gestures.
66#[derive(Clone)]
67#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
68pub(crate) struct TouchState {
69 /// Technical identifier of the touch device. This is used to identify relevant touch events
70 /// for this [`TouchState`] instance.
71 device_id: TouchDeviceId,
72
73 /// Active touches, if any.
74 ///
75 /// `TouchId` is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The
76 /// next touch will receive a new unique ID.
77 ///
78 /// Refer to [`ActiveTouch`].
79 active_touches: BTreeMap<TouchId, ActiveTouch>,
80
81 /// If a gesture has been recognized (i.e. when exactly two fingers touch the surface), this
82 /// holds state information
83 gesture_state: Option<GestureState>,
84}
85
86#[derive(Clone, Debug)]
87#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
88struct GestureState {
89 start_time: f64,
90 start_pointer_pos: Pos2,
91 pinch_type: PinchType,
92 previous: Option<DynGestureState>,
93 current: DynGestureState,
94}
95
96/// Gesture data that can change over time
97#[derive(Clone, Copy, Debug)]
98#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
99struct DynGestureState {
100 /// used for proportional zooming
101 avg_distance: f32,
102
103 /// used for non-proportional zooming
104 avg_abs_distance2: Vec2,
105
106 avg_pos: Pos2,
107
108 avg_force: f32,
109
110 heading: f32,
111}
112
113/// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as
114/// long as the finger/pen touches the surface.
115#[derive(Clone, Copy, Debug)]
116#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
117struct ActiveTouch {
118 /// Current position of this touch, in device coordinates (not necessarily screen position)
119 pos: Pos2,
120
121 /// Current force of the touch. A value in the interval [0.0 .. 1.0]
122 ///
123 /// Note that a value of 0.0 either indicates a very light touch, or it means that the device
124 /// is not capable of measuring the touch force.
125 force: Option<f32>,
126}
127
128impl TouchState {
129 pub fn new(device_id: TouchDeviceId) -> Self {
130 Self {
131 device_id,
132 active_touches: Default::default(),
133 gesture_state: None,
134 }
135 }
136
137 pub fn begin_frame(&mut self, time: f64, new: &RawInput, pointer_pos: Option<Pos2>) {
138 let mut added_or_removed_touches = false;
139 for event in &new.events {
140 match *event {
141 Event::Touch {
142 device_id,
143 id,
144 phase,
145 pos,
146 force,
147 } if device_id == self.device_id => match phase {
148 TouchPhase::Start => {
149 self.active_touches.insert(id, ActiveTouch { pos, force });
150 added_or_removed_touches = true;
151 }
152 TouchPhase::Move => {
153 if let Some(touch) = self.active_touches.get_mut(&id) {
154 touch.pos = pos;
155 touch.force = force;
156 }
157 }
158 TouchPhase::End | TouchPhase::Cancel => {
159 self.active_touches.remove(&id);
160 added_or_removed_touches = true;
161 }
162 },
163 _ => (),
164 }
165 }
166
167 // This needs to be called each frame, even if there are no new touch events.
168 // Otherwise, we would send the same old delta information multiple times:
169 self.update_gesture(time, pointer_pos);
170
171 if added_or_removed_touches {
172 // Adding or removing fingers makes the average values "jump". We better forget
173 // about the previous values, and don't create delta information for this frame:
174 if let Some(ref mut state) = &mut self.gesture_state {
175 state.previous = None;
176 }
177 }
178 }
179
180 /// Are there currently any fingers touching the surface?
181 pub fn any_touches(&self) -> bool {
182 !self.active_touches.is_empty()
183 }
184
185 pub fn info(&self) -> Option<MultiTouchInfo> {
186 self.gesture_state.as_ref().map(|state| {
187 // state.previous can be `None` when the number of simultaneous touches has just
188 // changed. In this case, we take `current` as `previous`, pretending that there
189 // was no change for the current frame.
190 let state_previous = state.previous.unwrap_or(state.current);
191
192 let zoom_delta = state.current.avg_distance / state_previous.avg_distance;
193
194 let zoom_delta2 = match state.pinch_type {
195 PinchType::Horizontal => Vec2::new(
196 state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x,
197 1.0,
198 ),
199 PinchType::Vertical => Vec2::new(
200 1.0,
201 state.current.avg_abs_distance2.y / state_previous.avg_abs_distance2.y,
202 ),
203 PinchType::Proportional => Vec2::splat(zoom_delta),
204 };
205
206 MultiTouchInfo {
207 start_time: state.start_time,
208 start_pos: state.start_pointer_pos,
209 num_touches: self.active_touches.len(),
210 zoom_delta,
211 zoom_delta_2d: zoom_delta2,
212 rotation_delta: normalized_angle(state.current.heading - state_previous.heading),
213 translation_delta: state.current.avg_pos - state_previous.avg_pos,
214 force: state.current.avg_force,
215 }
216 })
217 }
218
219 fn update_gesture(&mut self, time: f64, pointer_pos: Option<Pos2>) {
220 if let Some(dyn_state) = self.calc_dynamic_state() {
221 if let Some(ref mut state) = &mut self.gesture_state {
222 // updating an ongoing gesture
223 state.previous = Some(state.current);
224 state.current = dyn_state;
225 } else if let Some(pointer_pos) = pointer_pos {
226 // starting a new gesture
227 self.gesture_state = Some(GestureState {
228 start_time: time,
229 start_pointer_pos: pointer_pos,
230 pinch_type: PinchType::classify(&self.active_touches),
231 previous: None,
232 current: dyn_state,
233 });
234 }
235 } else {
236 // the end of a gesture (if there is any)
237 self.gesture_state = None;
238 }
239 }
240
241 /// `None` if less than two fingers
242 fn calc_dynamic_state(&self) -> Option<DynGestureState> {
243 let num_touches = self.active_touches.len();
244 if num_touches < 2 {
245 None
246 } else {
247 let mut state = DynGestureState {
248 avg_distance: 0.0,
249 avg_abs_distance2: Vec2::ZERO,
250 avg_pos: Pos2::ZERO,
251 avg_force: 0.0,
252 heading: 0.0,
253 };
254 let num_touches_recip = 1. / num_touches as f32;
255
256 // first pass: calculate force and center of touch positions:
257 for touch in self.active_touches.values() {
258 state.avg_force += touch.force.unwrap_or(0.0);
259 state.avg_pos.x += touch.pos.x;
260 state.avg_pos.y += touch.pos.y;
261 }
262 state.avg_force *= num_touches_recip;
263 state.avg_pos.x *= num_touches_recip;
264 state.avg_pos.y *= num_touches_recip;
265
266 // second pass: calculate distances from center:
267 for touch in self.active_touches.values() {
268 state.avg_distance += state.avg_pos.distance(touch.pos);
269 state.avg_abs_distance2.x += (state.avg_pos.x - touch.pos.x).abs();
270 state.avg_abs_distance2.y += (state.avg_pos.y - touch.pos.y).abs();
271 }
272 state.avg_distance *= num_touches_recip;
273 state.avg_abs_distance2 *= num_touches_recip;
274
275 // Calculate the direction from the first touch to the center position.
276 // This is not the perfect way of calculating the direction if more than two fingers
277 // are involved, but as long as all fingers rotate more or less at the same angular
278 // velocity, the shortcomings of this method will not be noticed. One can see the
279 // issues though, when touching with three or more fingers, and moving only one of them
280 // (it takes two hands to do this in a controlled manner). A better technique would be
281 // to store the current and previous directions (with reference to the center) for each
282 // touch individually, and then calculate the average of all individual changes in
283 // direction. But this approach cannot be implemented locally in this method, making
284 // everything a bit more complicated.
285 let first_touch = self.active_touches.values().next().unwrap();
286 state.heading = (state.avg_pos - first_touch.pos).angle();
287
288 Some(state)
289 }
290 }
291}
292
293impl TouchState {
294 pub fn ui(&self, ui: &mut crate::Ui) {
295 ui.label(format!("{self:?}"));
296 }
297}
298
299impl Debug for TouchState {
300 // This outputs less clutter than `#[derive(Debug)]`:
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 for (id, touch) in &self.active_touches {
303 f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
304 }
305 f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?;
306 Ok(())
307 }
308}
309
310#[derive(Clone, Debug)]
311#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
312enum PinchType {
313 Horizontal,
314 Vertical,
315 Proportional,
316}
317
318impl PinchType {
319 fn classify(touches: &BTreeMap<TouchId, ActiveTouch>) -> Self {
320 // For non-proportional 2d zooming:
321 // If the user is pinching with two fingers that have roughly the same Y coord,
322 // then the Y zoom is unstable and should be 1.
323 // Similarly, if the fingers are directly above/below each other,
324 // we should only zoom on the Y axis.
325 // If the fingers are roughly on a diagonal, we revert to the proportional zooming.
326
327 if touches.len() == 2 {
328 let mut touches = touches.values();
329 let t0 = touches.next().unwrap().pos;
330 let t1 = touches.next().unwrap().pos;
331
332 let dx = (t0.x - t1.x).abs();
333 let dy = (t0.y - t1.y).abs();
334
335 if dx > 3.0 * dy {
336 Self::Horizontal
337 } else if dy > 3.0 * dx {
338 Self::Vertical
339 } else {
340 Self::Proportional
341 }
342 } else {
343 Self::Proportional
344 }
345 }
346}