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}