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// TODO: All `_SCREEN_PX` units below are currently actually used as `DevicePixel`
44// without multiplying with the `hidpi_factor`. This should be fixed and the
45// constants adjusted accordingly.
46/// Minimum number of `DeviceIndependentPixel` to begin touch scrolling.
47const TOUCH_PAN_MIN_SCREEN_PX: f32 = 20.0;
48/// Factor by which the flinging velocity changes on each tick.
49const FLING_SCALING_FACTOR: f32 = 0.95;
50/// Minimum velocity required for transitioning to fling when panning ends.
51const FLING_MIN_SCREEN_PX: f32 = 3.0;
52/// Maximum velocity when flinging.
53const FLING_MAX_SCREEN_PX: f32 = 4000.0;
54
55pub struct TouchHandler {
56    /// The [`WebViewId`] of the `WebView` this [`TouchHandler`] is associated with.
57    webview_id: WebViewId,
58    pub current_sequence_id: TouchSequenceId,
59    // todo: VecDeque + modulo arithmetic would be more efficient.
60    touch_sequence_map: FxHashMap<TouchSequenceId, TouchSequenceInfo>,
61    /// A set of [`InputEventId`]s for touch events that have been sent to the Constellation
62    /// and have not been handled yet.
63    pub(crate) pending_touch_input_events: RefCell<FxHashMap<InputEventId, PendingTouchInputEvent>>,
64    /// Whether or not the [`FlingRefreshDriverObserver`] is currently observing frames for fling.
65    observing_frames_for_fling: Cell<bool>,
66}
67
68/// Whether the default move action is allowed or not.
69#[derive(Debug, Eq, PartialEq)]
70pub enum TouchMoveAllowed {
71    /// The default move action is prevented by script
72    Prevented,
73    /// The default move action is allowed
74    Allowed,
75    /// The initial move handler result is still pending
76    Pending,
77}
78
79/// A cached [`PaintHitTestResult`] to use during a touch sequence. This
80/// is kept so that the renderer doesn't have to constantly keep making hit tests
81/// while during panning and flinging actions.
82struct HitTestResultCache {
83    value: PaintHitTestResult,
84    device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
85}
86
87pub struct TouchSequenceInfo {
88    /// touch sequence state
89    pub(crate) state: TouchSequenceState,
90    /// touch sequence active touch points
91    active_touch_points: Vec<TouchPoint>,
92    /// The script thread is already processing a touchmove operation.
93    ///
94    /// We use this to skip sending the event to the script thread,
95    /// to prevent overloading script.
96    handling_touch_move: bool,
97    /// Do not perform a click action.
98    ///
99    /// This happens when
100    /// - We had a touch move larger than the minimum distance OR
101    /// - We had multiple active touchpoints OR
102    /// - `preventDefault()` was called in a touch_down or touch_up handler
103    pub prevent_click: bool,
104    /// Whether move is allowed, prevented or the result is still pending.
105    /// Once the first move has been processed by script, we can transition to
106    /// non-cancellable events, and directly perform the pan without waiting for script.
107    pub prevent_move: TouchMoveAllowed,
108    /// Move operation waiting to be processed in the touch sequence.
109    ///
110    /// This is only used while the first touch move is processed in script.
111    /// Todo: It would be nice to merge this into the TouchSequenceState, but
112    /// this requires some additional work to handle the merging of pending
113    /// touch move events. Presumably if we keep a history of previous touch points,
114    /// this would allow a better fling algorithm and easier merging of zoom events.
115    pending_touch_move_actions: Vec<ScrollZoomEvent>,
116    /// Cache for the last touch hit test result.
117    hit_test_result_cache: Option<HitTestResultCache>,
118}
119
120impl TouchSequenceInfo {
121    fn touch_count(&self) -> usize {
122        self.active_touch_points.len()
123    }
124
125    fn pinch_distance_and_center(&self) -> (f32, Point2D<f32, DevicePixel>) {
126        debug_assert_eq!(self.touch_count(), 2);
127        let p0 = self.active_touch_points[0].point;
128        let p1 = self.active_touch_points[1].point;
129        let center = p0.lerp(p1, 0.5);
130        let distance = (p0 - p1).length();
131
132        (distance, center)
133    }
134
135    fn add_pending_touch_move_action(&mut self, action: ScrollZoomEvent) {
136        debug_assert!(self.prevent_move == TouchMoveAllowed::Pending);
137        self.pending_touch_move_actions.push(action);
138    }
139
140    /// Returns true when all touch events of a sequence have been received.
141    /// This does not mean that all event handlers have finished yet.
142    fn is_finished(&self) -> bool {
143        matches!(
144            self.state,
145            Finished | Flinging { .. } | PendingFling { .. } | PendingClick(_)
146        )
147    }
148
149    fn update_hit_test_result_cache_pointer(&mut self, delta: Vector2D<f32, DevicePixel>) {
150        if let Some(ref mut hit_test_result_cache) = self.hit_test_result_cache {
151            let scaled_delta = delta / hit_test_result_cache.device_pixels_per_page;
152            // Update the point of the hit test result to match the current touch point.
153            hit_test_result_cache.value.point_in_viewport += scaled_delta;
154        }
155    }
156}
157
158/// An action that can be immediately performed in response to a touch move event
159/// without waiting for script.
160#[derive(Clone, Copy, Debug, PartialEq)]
161
162pub struct TouchPoint {
163    pub id: TouchId,
164    pub point: Point2D<f32, DevicePixel>,
165}
166
167impl TouchPoint {
168    pub fn new(id: TouchId, point: Point2D<f32, DevicePixel>) -> Self {
169        TouchPoint { id, point }
170    }
171}
172
173/// The states of the touch input state machine.
174#[derive(Clone, Copy, Debug, PartialEq)]
175pub(crate) enum TouchSequenceState {
176    /// touch point is active but does not start moving
177    Touching,
178    /// A single touch point is active and has started panning.
179    Panning {
180        velocity: Vector2D<f32, DevicePixel>,
181    },
182    /// A two-finger pinch zoom gesture is active.
183    Pinching,
184    /// A multi-touch gesture is in progress.
185    MultiTouch,
186    // All states below here are reached after a touch-up, i.e. all events of the sequence
187    // have already been received.
188    /// The initial touch move handler has not finished processing yet, so we need to wait
189    /// for the result in order to transition to fling.
190    PendingFling {
191        velocity: Vector2D<f32, DevicePixel>,
192        point: DevicePoint,
193    },
194    /// No active touch points, but there is still scrolling velocity
195    Flinging {
196        velocity: Vector2D<f32, DevicePixel>,
197        point: DevicePoint,
198    },
199    /// The touch sequence is finished, but a click is still pending, waiting on script.
200    PendingClick(DevicePoint),
201    /// touch sequence finished.
202    Finished,
203}
204
205pub(crate) struct FlingAction {
206    pub delta: DeviceVector2D,
207    pub cursor: DevicePoint,
208}
209
210impl TouchHandler {
211    pub fn new(webview_id: WebViewId) -> Self {
212        let finished_info = TouchSequenceInfo {
213            state: TouchSequenceState::Finished,
214            active_touch_points: vec![],
215            handling_touch_move: false,
216            prevent_click: false,
217            prevent_move: TouchMoveAllowed::Pending,
218            pending_touch_move_actions: vec![],
219            hit_test_result_cache: None,
220        };
221        // We insert a simulated initial touch sequence, which is already finished,
222        // so that we always have one element in the map, which simplifies creating
223        // a new touch sequence on touch_down.
224        let mut touch_sequence_map = FxHashMap::default();
225        touch_sequence_map.insert(TouchSequenceId::new(), finished_info);
226        TouchHandler {
227            webview_id,
228            current_sequence_id: TouchSequenceId::new(),
229            touch_sequence_map,
230            pending_touch_input_events: Default::default(),
231            observing_frames_for_fling: Default::default(),
232        }
233    }
234
235    pub(crate) fn set_handling_touch_move(&mut self, sequence_id: TouchSequenceId, flag: bool) {
236        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
237            sequence.handling_touch_move = flag;
238        }
239    }
240
241    pub(crate) fn is_handling_touch_move(&self, sequence_id: TouchSequenceId) -> bool {
242        if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) {
243            sequence.handling_touch_move
244        } else {
245            false
246        }
247    }
248
249    pub(crate) fn prevent_click(&mut self, sequence_id: TouchSequenceId) {
250        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
251            sequence.prevent_click = true;
252        } else {
253            warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
254        }
255    }
256
257    pub(crate) fn prevent_move(&mut self, sequence_id: TouchSequenceId) {
258        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
259            sequence.prevent_move = TouchMoveAllowed::Prevented;
260        } else {
261            warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
262        }
263    }
264
265    /// Returns true if default move actions are allowed, false if prevented or the result
266    /// is still pending.,
267    pub(crate) fn move_allowed(&mut self, sequence_id: TouchSequenceId) -> bool {
268        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
269            sequence.prevent_move == TouchMoveAllowed::Allowed
270        } else {
271            true
272        }
273    }
274
275    pub(crate) fn take_pending_touch_move_actions(
276        &mut self,
277        sequence_id: TouchSequenceId,
278    ) -> Vec<ScrollZoomEvent> {
279        self.touch_sequence_map
280            .get_mut(&sequence_id)
281            .map(|sequence| std::mem::take(&mut sequence.pending_touch_move_actions))
282            .unwrap_or_default()
283    }
284
285    pub(crate) fn remove_pending_touch_move_actions(&mut self, sequence_id: TouchSequenceId) {
286        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
287            sequence.pending_touch_move_actions = Vec::new();
288        }
289    }
290
291    // try to remove touch sequence, if touch sequence end and not has pending action.
292    pub(crate) fn try_remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
293        if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) {
294            if sequence.pending_touch_move_actions.is_empty() && sequence.state == Finished {
295                self.touch_sequence_map.remove(&sequence_id);
296            }
297        }
298    }
299
300    pub(crate) fn remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
301        let old = self.touch_sequence_map.remove(&sequence_id);
302        debug_assert!(old.is_some(), "Sequence already removed?");
303    }
304
305    pub fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
306        self.touch_sequence_map
307            .get_mut(&self.current_sequence_id)
308            .expect("Current Touch sequence does not exist")
309    }
310
311    fn try_get_current_touch_sequence(&self) -> Option<&TouchSequenceInfo> {
312        self.touch_sequence_map.get(&self.current_sequence_id)
313    }
314
315    fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
316        self.touch_sequence_map.get_mut(&self.current_sequence_id)
317    }
318
319    pub(crate) fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
320        self.touch_sequence_map
321            .get(&sequence_id)
322            .expect("Touch sequence not found.")
323    }
324    pub(crate) fn get_touch_sequence_mut(
325        &mut self,
326        sequence_id: TouchSequenceId,
327    ) -> Option<&mut TouchSequenceInfo> {
328        self.touch_sequence_map.get_mut(&sequence_id)
329    }
330
331    pub fn on_touch_down(&mut self, id: TouchId, point: Point2D<f32, DevicePixel>) {
332        // if the current sequence ID does not exist in the map, then it was already handled
333        if !self
334            .touch_sequence_map
335            .contains_key(&self.current_sequence_id) ||
336            self.get_touch_sequence(self.current_sequence_id)
337                .is_finished()
338        {
339            self.current_sequence_id.next();
340            debug!("Entered new touch sequence: {:?}", self.current_sequence_id);
341            let active_touch_points = vec![TouchPoint::new(id, point)];
342            self.touch_sequence_map.insert(
343                self.current_sequence_id,
344                TouchSequenceInfo {
345                    state: Touching,
346                    active_touch_points,
347                    handling_touch_move: false,
348                    prevent_click: false,
349                    prevent_move: TouchMoveAllowed::Pending,
350                    pending_touch_move_actions: vec![],
351                    hit_test_result_cache: None,
352                },
353            );
354        } else {
355            debug!("Touch down in sequence {:?}.", self.current_sequence_id);
356            let touch_sequence = self.get_current_touch_sequence_mut();
357            touch_sequence
358                .active_touch_points
359                .push(TouchPoint::new(id, point));
360            match touch_sequence.active_touch_points.len() {
361                2.. => {
362                    touch_sequence.state = MultiTouch;
363                },
364                0..2 => {
365                    unreachable!("Secondary touch_down event with less than 2 fingers active?");
366                },
367            }
368            // Multiple fingers prevent a click.
369            touch_sequence.prevent_click = true;
370        }
371    }
372
373    pub fn notify_new_frame_start(&mut self) -> Option<FlingAction> {
374        let touch_sequence = self.touch_sequence_map.get_mut(&self.current_sequence_id)?;
375
376        let Flinging {
377            velocity,
378            point: cursor,
379        } = &mut touch_sequence.state
380        else {
381            self.observing_frames_for_fling.set(false);
382            return None;
383        };
384
385        if velocity.length().abs() < FLING_MIN_SCREEN_PX {
386            self.stop_fling_if_needed();
387            None
388        } else {
389            // TODO: Probably we should multiply with the current refresh rate (and divide on each frame)
390            // or save a timestamp to account for a potentially changing display refresh rate.
391            *velocity *= FLING_SCALING_FACTOR;
392            let _span = profile_traits::info_span!(
393                "TouchHandler::Flinging",
394                velocity = ?velocity,
395            )
396            .entered();
397            debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX);
398            Some(FlingAction {
399                delta: DeviceVector2D::new(velocity.x, velocity.y),
400                cursor: *cursor,
401            })
402        }
403    }
404
405    pub(crate) fn stop_fling_if_needed(&mut self) {
406        let current_sequence_id = self.current_sequence_id;
407        let touch_sequence = self.get_current_touch_sequence_mut();
408        let Flinging { .. } = touch_sequence.state else {
409            return;
410        };
411        let _span = profile_traits::info_span!("TouchHandler::FlingEnd").entered();
412        debug!("Stopping fling in touch sequence {current_sequence_id:?}");
413        touch_sequence.state = Finished;
414        // If we were flinging previously, there could still be a touch_up event result
415        // coming in after we stopped flinging
416        self.try_remove_touch_sequence(current_sequence_id);
417        self.observing_frames_for_fling.set(false);
418    }
419
420    pub fn on_touch_move(
421        &mut self,
422        id: TouchId,
423        point: Point2D<f32, DevicePixel>,
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.id == 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 ||
460                    delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX
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 ||
490                    delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX
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 fn on_touch_up(&mut self, 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.id == 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 fn on_touch_cancel(&mut self, 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.id == 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        event_type: TouchEventType,
660    ) {
661        self.pending_touch_input_events.borrow_mut().insert(
662            id,
663            PendingTouchInputEvent {
664                event_type,
665                sequence_id: self.current_sequence_id,
666            },
667        );
668    }
669
670    pub(crate) fn take_pending_touch_input_event(
671        &self,
672        id: InputEventId,
673    ) -> Option<PendingTouchInputEvent> {
674        self.pending_touch_input_events.borrow_mut().remove(&id)
675    }
676
677    pub(crate) fn add_touch_move_refresh_observer_if_necessary(
678        &self,
679        refresh_driver: Rc<BaseRefreshDriver>,
680        repaint_reason: &Cell<RepaintReason>,
681    ) {
682        if self.observing_frames_for_fling.get() {
683            return;
684        }
685
686        let Some(current_touch_sequence) = self.try_get_current_touch_sequence() else {
687            return;
688        };
689
690        if !matches!(
691            current_touch_sequence.state,
692            TouchSequenceState::Flinging { .. },
693        ) {
694            return;
695        }
696
697        refresh_driver.add_observer(Rc::new(FlingRefreshDriverObserver {
698            webview_id: self.webview_id,
699        }));
700        self.observing_frames_for_fling.set(true);
701        repaint_reason.set(repaint_reason.get().union(RepaintReason::StartedFlinging));
702    }
703}
704
705/// This data structure is used to store information about touch events that are
706/// sent from the Renderer to the Constellation, so that they can finish processing
707/// once their DOM events are fired.
708pub(crate) struct PendingTouchInputEvent {
709    pub event_type: TouchEventType,
710    pub sequence_id: TouchSequenceId,
711}
712
713pub(crate) struct FlingRefreshDriverObserver {
714    pub webview_id: WebViewId,
715}
716
717impl RefreshDriverObserver for FlingRefreshDriverObserver {
718    fn frame_started(&self, painter: &mut Painter) -> bool {
719        painter
720            .webview_renderer_mut(self.webview_id)
721            .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
722    }
723}