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