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