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