Skip to main content

paint/
touch.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7
8use embedder_traits::{InputEventId, PaintHitTestResult, Scroll, TouchEventType, TouchId};
9use euclid::{Point2D, Scale, Vector2D};
10use log::{debug, error, warn};
11use rustc_hash::{FxHashMap, FxHashSet};
12use servo_base::id::WebViewId;
13use style_traits::CSSPixel;
14use webrender_api::units::{DevicePixel, DevicePoint, DeviceVector2D};
15
16use self::TouchSequenceState::*;
17use crate::paint::RepaintReason;
18use crate::painter::Painter;
19use crate::refresh_driver::{BaseRefreshDriver, RefreshDriverObserver};
20use crate::webview_renderer::{ScrollEvent, ScrollZoomEvent, WebViewRenderer};
21
22/// An ID for a sequence of touch events between a `Down` and the `Up` or `Cancel` event.
23/// The ID is the same for all events between `Down` and `Up` or `Cancel`
24#[repr(transparent)]
25#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
26pub(crate) struct TouchSequenceId(u32);
27
28impl TouchSequenceId {
29    const fn new() -> Self {
30        Self(0)
31    }
32
33    /// Increments the ID for the next touch sequence.
34    ///
35    /// The increment is wrapping, since we can assume that the touch handler
36    /// script for touch sequence N will have finished processing by the time
37    /// we have wrapped around.
38    fn next(&mut self) {
39        self.0 = self.0.wrapping_add(1);
40    }
41}
42
43/// Minimum number of `DeviceIndependentPixel` to begin touch scrolling/Pinching.
44const TOUCH_PAN_MIN_SCREEN_PX: f32 = 10.0;
45/// Factor by which the flinging velocity changes on each tick.
46const FLING_SCALING_FACTOR: f32 = 0.95;
47/// Minimum velocity required for transitioning to fling when panning ends.
48const FLING_MIN_SCREEN_PX: f32 = 3.0;
49/// Maximum velocity when flinging.
50const FLING_MAX_SCREEN_PX: f32 = 4000.0;
51
52pub struct TouchHandler {
53    /// The [`WebViewId`] of the `WebView` this [`TouchHandler`] is associated with.
54    webview_id: WebViewId,
55    pub current_sequence_id: TouchSequenceId,
56    // todo: VecDeque + modulo arithmetic would be more efficient.
57    touch_sequence_map: FxHashMap<TouchSequenceId, TouchSequenceInfo>,
58    /// A set of [`InputEventId`]s for touch events that have been sent to the Constellation
59    /// and have not been handled yet.
60    pub(crate) pending_touch_input_events: RefCell<FxHashMap<InputEventId, PendingTouchInputEvent>>,
61    /// Whether or not the [`FlingRefreshDriverObserver`] is currently observing frames for fling.
62    observing_frames_for_fling: Cell<bool>,
63}
64
65/// Whether the default move action is allowed or not.
66#[derive(Debug, Eq, PartialEq)]
67pub enum TouchMoveAllowed {
68    /// The default move action is prevented by script
69    Prevented,
70    /// The default move action is allowed
71    Allowed,
72    /// The initial move handler result is still pending
73    Pending,
74}
75
76pub(crate) enum TouchIdMoveTracking {
77    Track,
78    Remove,
79}
80
81/// The axis of a pan gesture. Once panning begins, the gesture is locked to the
82/// dominant axis for the rest of the sequence, so that e.g. a vertical pan that
83/// passes over a horizontally scrollable element keeps scrolling the page instead
84/// of switching to horizontal scrolling.
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub(crate) enum PanAxis {
87    Horizontal,
88    Vertical,
89}
90
91/// A cached [`PaintHitTestResult`] to use during a touch sequence. This
92/// is kept so that the renderer doesn't have to constantly keep making hit tests
93/// while during panning and flinging actions.
94struct HitTestResultCache {
95    value: PaintHitTestResult,
96    device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
97}
98
99pub struct TouchSequenceInfo {
100    /// touch sequence state
101    pub(crate) state: TouchSequenceState,
102    /// touch sequence active touch points
103    active_touch_points: Vec<TouchPoint>,
104    /// Whether the script thread is already processing a touchmove operation for the TouchId.
105    ///
106    /// We use this to skip sending the event to the script thread,
107    /// to prevent overloading script.
108    touch_ids_in_move: FxHashSet<TouchId>,
109    /// Do not perform a click action.
110    ///
111    /// This happens when
112    /// - We had a touch move larger than the minimum distance OR
113    /// - We had multiple active touchpoints OR
114    /// - `preventDefault()` was called in a touch_down or touch_up handler
115    pub prevent_click: bool,
116    /// Whether move is allowed, prevented or the result is still pending.
117    /// Once the first move has been processed by script, we can transition to
118    /// non-cancellable events, and directly perform the pan without waiting for script.
119    pub prevent_move: TouchMoveAllowed,
120    /// Move operation waiting to be processed in the touch sequence.
121    ///
122    /// This is only used while the first touch move is processed in script.
123    /// Todo: It would be nice to merge this into the TouchSequenceState, but
124    /// this requires some additional work to handle the merging of pending
125    /// touch move events. Presumably if we keep a history of previous touch points,
126    /// this would allow a better fling algorithm and easier merging of zoom events.
127    pending_touch_move_actions: Vec<ScrollZoomEvent>,
128    /// Cache for the last touch hit test result.
129    hit_test_result_cache: Option<HitTestResultCache>,
130}
131
132impl TouchSequenceInfo {
133    fn touch_count(&self) -> usize {
134        self.active_touch_points.len()
135    }
136
137    fn pinch_distance_and_center(&self) -> (f32, Point2D<f32, DevicePixel>) {
138        debug_assert_eq!(self.touch_count(), 2);
139        let p0 = self.active_touch_points[0].point;
140        let p1 = self.active_touch_points[1].point;
141        let center = p0.lerp(p1, 0.5);
142        let distance = (p0 - p1).length();
143
144        (distance, center)
145    }
146
147    fn add_pending_touch_move_action(&mut self, action: ScrollZoomEvent) {
148        debug_assert!(self.prevent_move == TouchMoveAllowed::Pending);
149        self.pending_touch_move_actions.push(action);
150    }
151
152    /// Returns true when all touch events of a sequence have been received.
153    /// This does not mean that all event handlers have finished yet.
154    fn is_finished(&self) -> bool {
155        matches!(
156            self.state,
157            Finished | Flinging { .. } | PendingFling { .. } | PendingClick(_)
158        )
159    }
160
161    fn update_hit_test_result_cache_pointer(&mut self, delta: Vector2D<f32, DevicePixel>) {
162        if let Some(ref mut hit_test_result_cache) = self.hit_test_result_cache {
163            let scaled_delta = delta / hit_test_result_cache.device_pixels_per_page;
164            // Update the point of the hit test result to match the current touch point.
165            hit_test_result_cache.value.point_in_viewport += scaled_delta;
166        }
167    }
168}
169
170/// An action that can be immediately performed in response to a touch move event
171/// without waiting for script.
172#[derive(Clone, Copy, Debug, PartialEq)]
173
174pub struct TouchPoint {
175    pub touch_id: TouchId,
176    pub point: Point2D<f32, DevicePixel>,
177}
178
179impl TouchPoint {
180    fn new(touch_id: TouchId, point: Point2D<f32, DevicePixel>) -> Self {
181        TouchPoint { touch_id, point }
182    }
183}
184
185/// The states of the touch input state machine.
186#[derive(Clone, Copy, Debug, PartialEq)]
187pub(crate) enum TouchSequenceState {
188    /// touch point is active but does not start moving
189    Touching,
190    /// A single touch point is active and has started panning.
191    Panning {
192        /// The dominant axis of the pan, locked for the rest of the sequence.
193        axis: PanAxis,
194        velocity: Vector2D<f32, DevicePixel>,
195    },
196    /// A two-finger pinch zoom gesture is active.
197    Pinching,
198    /// A multi-touch gesture is in progress.
199    MultiTouch,
200    // All states below here are reached after a touch-up, i.e. all events of the sequence
201    // have already been received.
202    /// The initial touch move handler has not finished processing yet, so we need to wait
203    /// for the result in order to transition to fling.
204    PendingFling {
205        velocity: Vector2D<f32, DevicePixel>,
206        point: DevicePoint,
207    },
208    /// No active touch points, but there is still scrolling velocity
209    Flinging {
210        velocity: Vector2D<f32, DevicePixel>,
211        point: DevicePoint,
212    },
213    /// The touch sequence is finished, but a click is still pending, waiting on script.
214    PendingClick(DevicePoint),
215    /// touch sequence finished.
216    Finished,
217}
218
219pub(crate) struct FlingAction {
220    pub delta: DeviceVector2D,
221    pub cursor: DevicePoint,
222}
223
224impl TouchHandler {
225    pub(crate) fn new(webview_id: WebViewId) -> Self {
226        let finished_info = TouchSequenceInfo {
227            state: TouchSequenceState::Finished,
228            active_touch_points: vec![],
229            touch_ids_in_move: FxHashSet::default(),
230            prevent_click: false,
231            prevent_move: TouchMoveAllowed::Pending,
232            pending_touch_move_actions: vec![],
233            hit_test_result_cache: None,
234        };
235        // We insert a simulated initial touch sequence, which is already finished,
236        // so that we always have one element in the map, which simplifies creating
237        // a new touch sequence on touch_down.
238        let mut touch_sequence_map = FxHashMap::default();
239        touch_sequence_map.insert(TouchSequenceId::new(), finished_info);
240        TouchHandler {
241            webview_id,
242            current_sequence_id: TouchSequenceId::new(),
243            touch_sequence_map,
244            pending_touch_input_events: Default::default(),
245            observing_frames_for_fling: Default::default(),
246        }
247    }
248
249    pub(crate) fn set_handling_touch_move_for_touch_id(
250        &mut self,
251        sequence_id: TouchSequenceId,
252        touch_id: TouchId,
253        flag: TouchIdMoveTracking,
254    ) {
255        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
256            match flag {
257                TouchIdMoveTracking::Track => {
258                    sequence.touch_ids_in_move.insert(touch_id);
259                },
260                TouchIdMoveTracking::Remove => {
261                    sequence.touch_ids_in_move.remove(&touch_id);
262                },
263            }
264        }
265    }
266
267    pub(crate) fn is_handling_touch_move_for_touch_id(
268        &self,
269        sequence_id: TouchSequenceId,
270        touch_id: TouchId,
271    ) -> bool {
272        self.touch_sequence_map
273            .get(&sequence_id)
274            .is_some_and(|seq| seq.touch_ids_in_move.contains(&touch_id))
275    }
276
277    pub(crate) fn prevent_click(&mut self, sequence_id: TouchSequenceId) {
278        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
279            sequence.prevent_click = true;
280        } else {
281            warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
282        }
283    }
284
285    pub(crate) fn prevent_move(&mut self, sequence_id: TouchSequenceId) {
286        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
287            sequence.prevent_move = TouchMoveAllowed::Prevented;
288        } else {
289            warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
290        }
291    }
292
293    /// Returns true if default move actions are allowed, false if prevented or the result
294    /// is still pending.,
295    pub(crate) fn move_allowed(&self, sequence_id: TouchSequenceId) -> bool {
296        self.touch_sequence_map
297            .get(&sequence_id)
298            .is_none_or(|sequence| sequence.prevent_move == TouchMoveAllowed::Allowed)
299    }
300
301    pub(crate) fn take_pending_touch_move_actions(
302        &mut self,
303        sequence_id: TouchSequenceId,
304    ) -> Vec<ScrollZoomEvent> {
305        self.touch_sequence_map
306            .get_mut(&sequence_id)
307            .map(|sequence| std::mem::take(&mut sequence.pending_touch_move_actions))
308            .unwrap_or_default()
309    }
310
311    pub(crate) fn remove_pending_touch_move_actions(&mut self, sequence_id: TouchSequenceId) {
312        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
313            sequence.pending_touch_move_actions.clear();
314        }
315    }
316
317    // try to remove touch sequence, if touch sequence end and not has pending action.
318    pub(crate) fn try_remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
319        if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) &&
320            sequence.pending_touch_move_actions.is_empty() &&
321            sequence.state == Finished
322        {
323            self.touch_sequence_map.remove(&sequence_id);
324        }
325    }
326
327    pub(crate) fn remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
328        let old = self.touch_sequence_map.remove(&sequence_id);
329        debug_assert!(old.is_some(), "Sequence already removed?");
330    }
331
332    fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
333        self.touch_sequence_map
334            .get_mut(&self.current_sequence_id)
335            .expect("Current Touch sequence does not exist")
336    }
337
338    fn try_get_current_touch_sequence(&self) -> Option<&TouchSequenceInfo> {
339        self.touch_sequence_map.get(&self.current_sequence_id)
340    }
341
342    fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
343        self.touch_sequence_map.get_mut(&self.current_sequence_id)
344    }
345
346    fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
347        self.touch_sequence_map
348            .get(&sequence_id)
349            .expect("Touch sequence not found.")
350    }
351
352    pub(crate) fn get_touch_sequence_mut(
353        &mut self,
354        sequence_id: TouchSequenceId,
355    ) -> Option<&mut TouchSequenceInfo> {
356        self.touch_sequence_map.get_mut(&sequence_id)
357    }
358
359    pub(crate) fn on_touch_down(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
360        // if the current sequence ID does not exist in the map, then it was already handled
361        if !self
362            .touch_sequence_map
363            .contains_key(&self.current_sequence_id) ||
364            self.get_touch_sequence(self.current_sequence_id)
365                .is_finished()
366        {
367            self.current_sequence_id.next();
368            debug!("Entered new touch sequence: {:?}", self.current_sequence_id);
369            let active_touch_points = vec![TouchPoint::new(touch_id, point)];
370            self.touch_sequence_map.insert(
371                self.current_sequence_id,
372                TouchSequenceInfo {
373                    state: Touching,
374                    active_touch_points,
375                    touch_ids_in_move: FxHashSet::default(),
376                    prevent_click: false,
377                    prevent_move: TouchMoveAllowed::Pending,
378                    pending_touch_move_actions: vec![],
379                    hit_test_result_cache: None,
380                },
381            );
382        } else {
383            debug!("Touch down in sequence {:?}.", self.current_sequence_id);
384            let touch_sequence = self.get_current_touch_sequence_mut();
385            touch_sequence
386                .active_touch_points
387                .push(TouchPoint::new(touch_id, point));
388            match touch_sequence.active_touch_points.len() {
389                2.. => {
390                    touch_sequence.state = MultiTouch;
391                },
392                0..2 => {
393                    unreachable!("Secondary touch_down event with less than 2 fingers active?");
394                },
395            }
396            // Multiple fingers prevent a click.
397            touch_sequence.prevent_click = true;
398        }
399    }
400
401    pub(crate) fn notify_new_frame_start(&mut self) -> Option<FlingAction> {
402        let touch_sequence = self.touch_sequence_map.get_mut(&self.current_sequence_id)?;
403
404        let Flinging {
405            velocity,
406            point: cursor,
407        } = &mut touch_sequence.state
408        else {
409            self.observing_frames_for_fling.set(false);
410            return None;
411        };
412
413        if velocity.length().abs() < FLING_MIN_SCREEN_PX {
414            self.stop_fling_if_needed();
415            None
416        } else {
417            // TODO: Probably we should multiply with the current refresh rate (and divide on each frame)
418            // or save a timestamp to account for a potentially changing display refresh rate.
419            *velocity *= FLING_SCALING_FACTOR;
420            let _span = profile_traits::info_span!(
421                "TouchHandler::Flinging",
422                velocity = ?velocity,
423            )
424            .entered();
425            debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX);
426            Some(FlingAction {
427                delta: DeviceVector2D::new(velocity.x, velocity.y),
428                cursor: *cursor,
429            })
430        }
431    }
432
433    pub(crate) fn stop_fling_if_needed(&mut self) {
434        let current_sequence_id = self.current_sequence_id;
435        let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
436            debug!(
437                "Touch sequence already removed before stoping potential flinging during Paint update"
438            );
439            return;
440        };
441        let Flinging { .. } = touch_sequence.state else {
442            return;
443        };
444        let _span = profile_traits::info_span!("TouchHandler::FlingEnd").entered();
445        debug!("Stopping flinging in touch sequence {current_sequence_id:?}");
446        touch_sequence.state = Finished;
447        // If we were flinging previously, there could still be a touch_up event result
448        // coming in after we stopped flinging
449        self.try_remove_touch_sequence(current_sequence_id);
450        self.observing_frames_for_fling.set(false);
451    }
452
453    pub(crate) fn on_touch_move(
454        &mut self,
455        touch_id: TouchId,
456        point: Point2D<f32, DevicePixel>,
457        scale: f32,
458    ) -> Option<ScrollZoomEvent> {
459        // As `TouchHandler` is per `WebViewRenderer` which is per `WebView` we might get a Touch Sequence Move that
460        // started with a down on a different webview. As the touch_sequence id is only changed on touch_down this
461        // move event gets a touch id which is already cleaned up.
462        let touch_sequence = self.try_get_current_touch_sequence_mut()?;
463        let idx = match touch_sequence
464            .active_touch_points
465            .iter_mut()
466            .position(|t| t.touch_id == touch_id)
467        {
468            Some(i) => i,
469            None => {
470                error!("Got a touchmove event for a non-active touch point");
471                return None;
472            },
473        };
474        let old_point = touch_sequence.active_touch_points[idx].point;
475        let delta = point - old_point;
476        touch_sequence.update_hit_test_result_cache_pointer(delta);
477
478        let action = match touch_sequence.touch_count() {
479            1 => {
480                if let Panning {
481                    axis,
482                    ref mut velocity,
483                } = touch_sequence.state
484                {
485                    // Only scroll along the axis that was dominant when panning started,
486                    // so the gesture cannot switch between horizontal and vertical.
487                    let pan_delta = match axis {
488                        PanAxis::Horizontal => Vector2D::new(delta.x, 0.0),
489                        PanAxis::Vertical => Vector2D::new(0.0, delta.y),
490                    };
491                    // TODO: Probably we should track 1-3 more points and use a smarter algorithm
492                    *velocity += pan_delta;
493                    *velocity /= 2.0;
494                    // update the touch point every time when panning.
495                    touch_sequence.active_touch_points[idx].point = point;
496
497                    // Scroll offsets are opposite to the direction of finger motion.
498                    Some(ScrollZoomEvent::Scroll(ScrollEvent {
499                        scroll: Scroll::Delta((-pan_delta).into()),
500                        point,
501                    }))
502                } else if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
503                    delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
504                {
505                    let _span = profile_traits::info_span!(
506                        "TouchHandler::ScrollBegin",
507                        delta = ?delta,
508                    )
509                    .entered();
510                    // The pan is locked to its dominant axis for the rest of the sequence,
511                    // so that e.g. a vertical pan over a horizontally scrollable element
512                    // keeps scrolling the page instead of switching to horizontal.
513                    let axis = if delta.y.abs() > delta.x.abs() {
514                        PanAxis::Vertical
515                    } else {
516                        PanAxis::Horizontal
517                    };
518                    let pan_delta = match axis {
519                        PanAxis::Horizontal => Vector2D::new(delta.x, 0.0),
520                        PanAxis::Vertical => Vector2D::new(0.0, delta.y),
521                    };
522                    touch_sequence.state = Panning {
523                        axis,
524                        velocity: pan_delta,
525                    };
526                    // No clicks should be issued after we transitioned to move.
527                    touch_sequence.prevent_click = true;
528                    // update the touch point
529                    touch_sequence.active_touch_points[idx].point = point;
530
531                    // Scroll offsets are opposite to the direction of finger motion.
532                    Some(ScrollZoomEvent::Scroll(ScrollEvent {
533                        scroll: Scroll::Delta((-pan_delta).into()),
534                        point,
535                    }))
536                } else {
537                    // We don't update the touchpoint, so multiple small moves can
538                    // accumulate and merge into a larger move.
539                    None
540                }
541            },
542            2 => {
543                if touch_sequence.state == Pinching ||
544                    delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ||
545                    delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale
546                {
547                    touch_sequence.state = Pinching;
548                    let (d0, _) = touch_sequence.pinch_distance_and_center();
549
550                    // update the touch point with the enough distance or pinching.
551                    touch_sequence.active_touch_points[idx].point = point;
552                    let (d1, c1) = touch_sequence.pinch_distance_and_center();
553
554                    Some(ScrollZoomEvent::PinchZoom(d1 / d0, c1))
555                } else {
556                    // We don't update the touchpoint, so multiple small moves can
557                    // accumulate and merge into a larger move.
558                    None
559                }
560            },
561            _ => {
562                touch_sequence.active_touch_points[idx].point = point;
563                touch_sequence.state = MultiTouch;
564                None
565            },
566        };
567        // If the touch action is not `NoAction` and the first move has not been processed,
568        //  set pending_touch_move_action.
569        if let Some(action) = action &&
570            touch_sequence.prevent_move == TouchMoveAllowed::Pending
571        {
572            touch_sequence.add_pending_touch_move_action(action);
573        }
574
575        action
576    }
577
578    pub(crate) fn on_touch_up(&mut self, touch_id: TouchId, point: Point2D<f32, DevicePixel>) {
579        let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
580            warn!("Current touch sequence not found");
581            return;
582        };
583        let old = match touch_sequence
584            .active_touch_points
585            .iter()
586            .position(|t| t.touch_id == touch_id)
587        {
588            Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point),
589            None => {
590                warn!("Got a touchup event for a non-active touch point");
591                None
592            },
593        };
594        match touch_sequence.state {
595            Touching => {
596                if touch_sequence.prevent_click {
597                    touch_sequence.state = Finished;
598                } else {
599                    touch_sequence.state = PendingClick(point);
600                }
601            },
602            Panning { velocity, .. } => {
603                if velocity.length().abs() >= FLING_MIN_SCREEN_PX {
604                    let _span = profile_traits::info_span!(
605                        "TouchHandler::FlingStart",
606                        velocity = ?velocity,
607                    )
608                    .entered();
609                    // TODO: point != old. Not sure which one is better to take as cursor for flinging.
610                    debug!(
611                        "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \
612                            Raw velocity is {velocity:?}."
613                    );
614
615                    // Multiplying the initial velocity gives the fling a much more snappy feel
616                    // and serves well as a poor-mans acceleration algorithm.
617                    let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX);
618                    match touch_sequence.prevent_move {
619                        TouchMoveAllowed::Allowed => {
620                            touch_sequence.state = Flinging { velocity, point }
621                            // todo: return Touchaction here, or is it sufficient to just
622                            // wait for the next vsync?
623                        },
624                        TouchMoveAllowed::Pending => {
625                            touch_sequence.state = PendingFling { velocity, point }
626                        },
627                        TouchMoveAllowed::Prevented => touch_sequence.state = Finished,
628                    }
629                } else {
630                    let _span = profile_traits::info_span!("TouchHandler::ScrollEnd").entered();
631                    touch_sequence.state = Finished;
632                }
633            },
634            Pinching => {
635                touch_sequence.state = Touching;
636            },
637            MultiTouch => {
638                // We stay in multi-touch mode once we entered it until all fingers are lifted.
639                if touch_sequence.active_touch_points.is_empty() {
640                    touch_sequence.state = Finished;
641                }
642            },
643            PendingFling { .. } | Flinging { .. } | PendingClick(_) | Finished => {
644                error!("Touch-up received, but touch handler already in post-touchup state.")
645            },
646        }
647        #[cfg(debug_assertions)]
648        if touch_sequence.active_touch_points.is_empty() {
649            debug_assert!(
650                touch_sequence.is_finished(),
651                "Did not transition to a finished state: {:?}",
652                touch_sequence.state
653            );
654        }
655        debug!(
656            "Touch up with remaining active touchpoints: {:?}, in sequence {:?}",
657            touch_sequence.active_touch_points.len(),
658            self.current_sequence_id
659        );
660    }
661
662    pub(crate) fn on_touch_cancel(&mut self, touch_id: TouchId, _point: Point2D<f32, DevicePixel>) {
663        // A similar thing with touch move can happen here where the event is coming from a different webview.
664        let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
665            return;
666        };
667        match touch_sequence
668            .active_touch_points
669            .iter()
670            .position(|t| t.touch_id == touch_id)
671        {
672            Some(i) => {
673                touch_sequence.active_touch_points.swap_remove(i);
674            },
675            None => {
676                warn!("Got a touchcancel event for a non-active touch point");
677                return;
678            },
679        }
680        if touch_sequence.active_touch_points.is_empty() {
681            touch_sequence.state = Finished;
682        }
683    }
684
685    pub(crate) fn get_hit_test_result_cache_value(&self) -> Option<PaintHitTestResult> {
686        let sequence = self.touch_sequence_map.get(&self.current_sequence_id)?;
687        if sequence.state == Finished {
688            return None;
689        }
690        sequence
691            .hit_test_result_cache
692            .as_ref()
693            .map(|cache| Some(cache.value.clone()))?
694    }
695
696    pub(crate) fn set_hit_test_result_cache_value(
697        &mut self,
698        value: PaintHitTestResult,
699        device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
700    ) {
701        if let Some(sequence) = self.touch_sequence_map.get_mut(&self.current_sequence_id) &&
702            sequence.hit_test_result_cache.is_none()
703        {
704            sequence.hit_test_result_cache = Some(HitTestResultCache {
705                value,
706                device_pixels_per_page,
707            });
708        }
709    }
710
711    pub(crate) fn add_pending_touch_input_event(
712        &self,
713        id: InputEventId,
714        touch_id: TouchId,
715        event_type: TouchEventType,
716    ) {
717        self.pending_touch_input_events.borrow_mut().insert(
718            id,
719            PendingTouchInputEvent {
720                event_type,
721                sequence_id: self.current_sequence_id,
722                touch_id,
723            },
724        );
725    }
726
727    pub(crate) fn take_pending_touch_input_event(
728        &self,
729        id: InputEventId,
730    ) -> Option<PendingTouchInputEvent> {
731        self.pending_touch_input_events.borrow_mut().remove(&id)
732    }
733
734    pub(crate) fn add_touch_move_refresh_observer_if_necessary(
735        &self,
736        refresh_driver: Rc<BaseRefreshDriver>,
737        repaint_reason: &Cell<RepaintReason>,
738    ) {
739        if self.observing_frames_for_fling.get() {
740            return;
741        }
742
743        let Some(current_touch_sequence) = self.try_get_current_touch_sequence() else {
744            return;
745        };
746
747        if !matches!(
748            current_touch_sequence.state,
749            TouchSequenceState::Flinging { .. },
750        ) {
751            return;
752        }
753
754        refresh_driver.add_observer(Rc::new(FlingRefreshDriverObserver {
755            webview_id: self.webview_id,
756        }));
757        self.observing_frames_for_fling.set(true);
758        repaint_reason.set(repaint_reason.get().union(RepaintReason::StartedFlinging));
759    }
760}
761
762/// This data structure is used to store information about touch events that are
763/// sent from the Renderer to the Constellation, so that they can finish processing
764/// once their DOM events are fired.
765pub(crate) struct PendingTouchInputEvent {
766    pub event_type: TouchEventType,
767    pub sequence_id: TouchSequenceId,
768    pub touch_id: TouchId,
769}
770
771pub(crate) struct FlingRefreshDriverObserver {
772    pub webview_id: WebViewId,
773}
774
775impl RefreshDriverObserver for FlingRefreshDriverObserver {
776    fn frame_started(&self, painter: &mut Painter) -> bool {
777        painter
778            .webview_renderer_mut(self.webview_id)
779            .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
780    }
781}