compositing/
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::RefCell;
6
7use base::id::WebViewId;
8use embedder_traits::{CompositorHitTestResult, InputEventId, Scroll, TouchEventType, TouchId};
9use euclid::{Point2D, Scale, Vector2D};
10use log::{debug, error, warn};
11use rustc_hash::FxHashMap;
12use style_traits::CSSPixel;
13use webrender_api::units::{DevicePixel, DevicePoint, DeviceVector2D};
14
15use self::TouchSequenceState::*;
16use crate::painter::Painter;
17use crate::refresh_driver::RefreshDriverObserver;
18use crate::webview_renderer::{ScrollEvent, ScrollZoomEvent, WebViewRenderer};
19
20/// An ID for a sequence of touch events between a `Down` and the `Up` or `Cancel` event.
21/// The ID is the same for all events between `Down` and `Up` or `Cancel`
22#[repr(transparent)]
23#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
24pub(crate) struct TouchSequenceId(u32);
25
26impl TouchSequenceId {
27    const fn new() -> Self {
28        Self(0)
29    }
30
31    /// Increments the ID for the next touch sequence.
32    ///
33    /// The increment is wrapping, since we can assume that the touch handler
34    /// script for touch sequence N will have finished processing by the time
35    /// we have wrapped around.
36    fn next(&mut self) {
37        self.0 = self.0.wrapping_add(1);
38    }
39}
40
41// TODO: All `_SCREEN_PX` units below are currently actually used as `DevicePixel`
42// without multiplying with the `hidpi_factor`. This should be fixed and the
43// constants adjusted accordingly.
44/// Minimum number of `DeviceIndependentPixel` to begin touch scrolling.
45const TOUCH_PAN_MIN_SCREEN_PX: f32 = 20.0;
46/// Factor by which the flinging velocity changes on each tick.
47const FLING_SCALING_FACTOR: f32 = 0.95;
48/// Minimum velocity required for transitioning to fling when panning ends.
49const FLING_MIN_SCREEN_PX: f32 = 3.0;
50/// Maximum velocity when flinging.
51const FLING_MAX_SCREEN_PX: f32 = 4000.0;
52
53pub struct TouchHandler {
54    pub current_sequence_id: TouchSequenceId,
55    // todo: VecDeque + modulo arithmetic would be more efficient.
56    touch_sequence_map: FxHashMap<TouchSequenceId, TouchSequenceInfo>,
57    /// A set of [`InputEventId`]s for touch events that have been sent to the Constellation
58    /// and have not been handled yet.
59    pub(crate) pending_touch_input_events: RefCell<FxHashMap<InputEventId, PendingTouchInputEvent>>,
60}
61
62/// Whether the default move action is allowed or not.
63#[derive(Debug, Eq, PartialEq)]
64pub enum TouchMoveAllowed {
65    /// The default move action is prevented by script
66    Prevented,
67    /// The default move action is allowed
68    Allowed,
69    /// The initial move handler result is still pending
70    Pending,
71}
72
73/// A cached [`CompositorHitTestResult`] to use during a touch sequence. This
74/// is kept so that the renderer doesn't have to constantly keep making hit tests
75/// while during panning and flinging actions.
76struct HitTestResultCache {
77    value: CompositorHitTestResult,
78    device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
79}
80
81pub struct TouchSequenceInfo {
82    /// touch sequence state
83    pub(crate) state: TouchSequenceState,
84    /// touch sequence active touch points
85    active_touch_points: Vec<TouchPoint>,
86    /// The script thread is already processing a touchmove operation.
87    ///
88    /// We use this to skip sending the event to the script thread,
89    /// to prevent overloading script.
90    handling_touch_move: bool,
91    /// Do not perform a click action.
92    ///
93    /// This happens when
94    /// - We had a touch move larger than the minimum distance OR
95    /// - We had multiple active touchpoints OR
96    /// - `preventDefault()` was called in a touch_down or touch_up handler
97    pub prevent_click: bool,
98    /// Whether move is allowed, prevented or the result is still pending.
99    /// Once the first move has been processed by script, we can transition to
100    /// non-cancellable events, and directly perform the pan without waiting for script.
101    pub prevent_move: TouchMoveAllowed,
102    /// Move operation waiting to be processed in the touch sequence.
103    ///
104    /// This is only used while the first touch move is processed in script.
105    /// Todo: It would be nice to merge this into the TouchSequenceState, but
106    /// this requires some additional work to handle the merging of pending
107    /// touch move events. Presumably if we keep a history of previous touch points,
108    /// this would allow a better fling algorithm and easier merging of zoom events.
109    pending_touch_move_actions: Vec<ScrollZoomEvent>,
110    /// Cache for the last touch hit test result.
111    hit_test_result_cache: Option<HitTestResultCache>,
112}
113
114impl TouchSequenceInfo {
115    fn touch_count(&self) -> usize {
116        self.active_touch_points.len()
117    }
118
119    fn pinch_distance_and_center(&self) -> (f32, Point2D<f32, DevicePixel>) {
120        debug_assert_eq!(self.touch_count(), 2);
121        let p0 = self.active_touch_points[0].point;
122        let p1 = self.active_touch_points[1].point;
123        let center = p0.lerp(p1, 0.5);
124        let distance = (p0 - p1).length();
125
126        (distance, center)
127    }
128
129    fn add_pending_touch_move_action(&mut self, action: ScrollZoomEvent) {
130        debug_assert!(self.prevent_move == TouchMoveAllowed::Pending);
131        self.pending_touch_move_actions.push(action);
132    }
133
134    /// Returns true when all touch events of a sequence have been received.
135    /// This does not mean that all event handlers have finished yet.
136    fn is_finished(&self) -> bool {
137        matches!(
138            self.state,
139            Finished | Flinging { .. } | PendingFling { .. } | PendingClick(_)
140        )
141    }
142
143    fn update_hit_test_result_cache_pointer(&mut self, delta: Vector2D<f32, DevicePixel>) {
144        if let Some(ref mut hit_test_result_cache) = self.hit_test_result_cache {
145            let scaled_delta = delta / hit_test_result_cache.device_pixels_per_page;
146            // Update the point of the hit test result to match the current touch point.
147            hit_test_result_cache.value.point_in_viewport += scaled_delta;
148        }
149    }
150}
151
152/// An action that can be immediately performed in response to a touch move event
153/// without waiting for script.
154#[derive(Clone, Copy, Debug, PartialEq)]
155
156pub struct TouchPoint {
157    pub id: TouchId,
158    pub point: Point2D<f32, DevicePixel>,
159}
160
161impl TouchPoint {
162    pub fn new(id: TouchId, point: Point2D<f32, DevicePixel>) -> Self {
163        TouchPoint { id, point }
164    }
165}
166
167/// The states of the touch input state machine.
168#[derive(Clone, Copy, Debug, PartialEq)]
169pub(crate) enum TouchSequenceState {
170    /// touch point is active but does not start moving
171    Touching,
172    /// A single touch point is active and has started panning.
173    Panning {
174        velocity: Vector2D<f32, DevicePixel>,
175    },
176    /// A two-finger pinch zoom gesture is active.
177    Pinching,
178    /// A multi-touch gesture is in progress.
179    MultiTouch,
180    // All states below here are reached after a touch-up, i.e. all events of the sequence
181    // have already been received.
182    /// The initial touch move handler has not finished processing yet, so we need to wait
183    /// for the result in order to transition to fling.
184    PendingFling {
185        velocity: Vector2D<f32, DevicePixel>,
186        point: DevicePoint,
187    },
188    /// No active touch points, but there is still scrolling velocity
189    Flinging {
190        velocity: Vector2D<f32, DevicePixel>,
191        point: DevicePoint,
192    },
193    /// The touch sequence is finished, but a click is still pending, waiting on script.
194    PendingClick(DevicePoint),
195    /// touch sequence finished.
196    Finished,
197}
198
199pub(crate) struct FlingAction {
200    pub delta: DeviceVector2D,
201    pub cursor: DevicePoint,
202}
203
204impl TouchHandler {
205    pub fn new() -> Self {
206        let finished_info = TouchSequenceInfo {
207            state: TouchSequenceState::Finished,
208            active_touch_points: vec![],
209            handling_touch_move: false,
210            prevent_click: false,
211            prevent_move: TouchMoveAllowed::Pending,
212            pending_touch_move_actions: vec![],
213            hit_test_result_cache: None,
214        };
215        // We insert a simulated initial touch sequence, which is already finished,
216        // so that we always have one element in the map, which simplifies creating
217        // a new touch sequence on touch_down.
218        let mut touch_sequence_map = FxHashMap::default();
219        touch_sequence_map.insert(TouchSequenceId::new(), finished_info);
220        TouchHandler {
221            current_sequence_id: TouchSequenceId::new(),
222            touch_sequence_map,
223            pending_touch_input_events: Default::default(),
224        }
225    }
226
227    pub(crate) fn currently_in_touch_sequence(&self) -> bool {
228        self.touch_sequence_map
229            .get(&self.current_sequence_id)
230            .is_some_and(|sequence| sequence.state != TouchSequenceState::Finished)
231    }
232
233    pub(crate) fn set_handling_touch_move(&mut self, sequence_id: TouchSequenceId, flag: bool) {
234        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
235            sequence.handling_touch_move = flag;
236        }
237    }
238
239    pub(crate) fn is_handling_touch_move(&self, sequence_id: TouchSequenceId) -> bool {
240        if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) {
241            sequence.handling_touch_move
242        } else {
243            false
244        }
245    }
246
247    pub(crate) fn prevent_click(&mut self, sequence_id: TouchSequenceId) {
248        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
249            sequence.prevent_click = true;
250        } else {
251            warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
252        }
253    }
254
255    pub(crate) fn prevent_move(&mut self, sequence_id: TouchSequenceId) {
256        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
257            sequence.prevent_move = TouchMoveAllowed::Prevented;
258        } else {
259            warn!("TouchSequenceInfo corresponding to the sequence number has been deleted.");
260        }
261    }
262
263    /// Returns true if default move actions are allowed, false if prevented or the result
264    /// is still pending.,
265    pub(crate) fn move_allowed(&mut self, sequence_id: TouchSequenceId) -> bool {
266        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
267            sequence.prevent_move == TouchMoveAllowed::Allowed
268        } else {
269            true
270        }
271    }
272
273    pub(crate) fn take_pending_touch_move_actions(
274        &mut self,
275        sequence_id: TouchSequenceId,
276    ) -> Vec<ScrollZoomEvent> {
277        self.touch_sequence_map
278            .get_mut(&sequence_id)
279            .map(|sequence| std::mem::take(&mut sequence.pending_touch_move_actions))
280            .unwrap_or_default()
281    }
282
283    pub(crate) fn remove_pending_touch_move_actions(&mut self, sequence_id: TouchSequenceId) {
284        if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) {
285            sequence.pending_touch_move_actions = Vec::new();
286        }
287    }
288
289    // try to remove touch sequence, if touch sequence end and not has pending action.
290    pub(crate) fn try_remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
291        if let Some(sequence) = self.touch_sequence_map.get(&sequence_id) {
292            if sequence.pending_touch_move_actions.is_empty() && sequence.state == Finished {
293                self.touch_sequence_map.remove(&sequence_id);
294            }
295        }
296    }
297
298    pub(crate) fn remove_touch_sequence(&mut self, sequence_id: TouchSequenceId) {
299        let old = self.touch_sequence_map.remove(&sequence_id);
300        debug_assert!(old.is_some(), "Sequence already removed?");
301    }
302
303    pub fn get_current_touch_sequence_mut(&mut self) -> &mut TouchSequenceInfo {
304        self.touch_sequence_map
305            .get_mut(&self.current_sequence_id)
306            .expect("Current Touch sequence does not exist")
307    }
308
309    fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
310        self.touch_sequence_map.get_mut(&self.current_sequence_id)
311    }
312
313    pub(crate) fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
314        self.touch_sequence_map
315            .get(&sequence_id)
316            .expect("Touch sequence not found.")
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 fn on_touch_down(&mut self, 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(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(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 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            return None;
376        };
377        if velocity.length().abs() < FLING_MIN_SCREEN_PX {
378            touch_sequence.state = Finished;
379            // If we were flinging previously, there could still be a touch_up event result
380            // coming in after we stopped flinging
381            self.try_remove_touch_sequence(self.current_sequence_id);
382            None
383        } else {
384            // TODO: Probably we should multiply with the current refresh rate (and divide on each frame)
385            // or save a timestamp to account for a potentially changing display refresh rate.
386            *velocity *= FLING_SCALING_FACTOR;
387            debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX);
388            Some(FlingAction {
389                delta: DeviceVector2D::new(velocity.x, velocity.y),
390                cursor: *cursor,
391            })
392        }
393    }
394
395    pub fn on_touch_move(
396        &mut self,
397        id: TouchId,
398        point: Point2D<f32, DevicePixel>,
399    ) -> Option<ScrollZoomEvent> {
400        // As `TouchHandler` is per `WebViewRenderer` which is per `WebView` we might get a Touch Sequence Move that
401        // started with a down on a different webview. As the touch_sequence id is only changed on touch_down this
402        // move event gets a touch id which is already cleaned up.
403        let touch_sequence = self.try_get_current_touch_sequence_mut()?;
404        let idx = match touch_sequence
405            .active_touch_points
406            .iter_mut()
407            .position(|t| t.id == id)
408        {
409            Some(i) => i,
410            None => {
411                error!("Got a touchmove event for a non-active touch point");
412                return None;
413            },
414        };
415        let old_point = touch_sequence.active_touch_points[idx].point;
416        let delta = point - old_point;
417        touch_sequence.update_hit_test_result_cache_pointer(delta);
418
419        let action = match touch_sequence.touch_count() {
420            1 => {
421                if let Panning { ref mut velocity } = touch_sequence.state {
422                    // TODO: Probably we should track 1-3 more points and use a smarter algorithm
423                    *velocity += delta;
424                    *velocity /= 2.0;
425                    // update the touch point every time when panning.
426                    touch_sequence.active_touch_points[idx].point = point;
427
428                    // Scroll offsets are opposite to the direction of finger motion.
429                    Some(ScrollZoomEvent::Scroll(ScrollEvent {
430                        scroll: Scroll::Delta((-delta).into()),
431                        point,
432                        event_count: 1,
433                    }))
434                } else if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX ||
435                    delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX
436                {
437                    touch_sequence.state = Panning {
438                        velocity: Vector2D::new(delta.x, delta.y),
439                    };
440                    // No clicks should be issued after we transitioned to move.
441                    touch_sequence.prevent_click = true;
442                    // update the touch point
443                    touch_sequence.active_touch_points[idx].point = point;
444
445                    // Scroll offsets are opposite to the direction of finger motion.
446                    Some(ScrollZoomEvent::Scroll(ScrollEvent {
447                        scroll: Scroll::Delta((-delta).into()),
448                        point,
449                        event_count: 1,
450                    }))
451                } else {
452                    // We don't update the touchpoint, so multiple small moves can
453                    // accumulate and merge into a larger move.
454                    None
455                }
456            },
457            2 => {
458                if touch_sequence.state == Pinching ||
459                    delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX ||
460                    delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX
461                {
462                    touch_sequence.state = Pinching;
463                    let (d0, _) = touch_sequence.pinch_distance_and_center();
464
465                    // update the touch point with the enough distance or pinching.
466                    touch_sequence.active_touch_points[idx].point = point;
467                    let (d1, c1) = touch_sequence.pinch_distance_and_center();
468
469                    Some(ScrollZoomEvent::PinchZoom(d1 / d0, c1))
470                } else {
471                    // We don't update the touchpoint, so multiple small moves can
472                    // accumulate and merge into a larger move.
473                    None
474                }
475            },
476            _ => {
477                touch_sequence.active_touch_points[idx].point = point;
478                touch_sequence.state = MultiTouch;
479                None
480            },
481        };
482        // If the touch action is not `NoAction` and the first move has not been processed,
483        //  set pending_touch_move_action.
484        if let Some(action) = action {
485            if touch_sequence.prevent_move == TouchMoveAllowed::Pending {
486                touch_sequence.add_pending_touch_move_action(action);
487            }
488        }
489
490        action
491    }
492
493    pub fn on_touch_up(&mut self, id: TouchId, point: Point2D<f32, DevicePixel>) {
494        let touch_sequence = self.get_current_touch_sequence_mut();
495        let old = match touch_sequence
496            .active_touch_points
497            .iter()
498            .position(|t| t.id == id)
499        {
500            Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point),
501            None => {
502                warn!("Got a touch up event for a non-active touch point");
503                None
504            },
505        };
506        match touch_sequence.state {
507            Touching => {
508                if touch_sequence.prevent_click {
509                    touch_sequence.state = Finished;
510                } else {
511                    touch_sequence.state = PendingClick(point);
512                }
513            },
514            Panning { velocity } => {
515                if velocity.length().abs() >= FLING_MIN_SCREEN_PX {
516                    // TODO: point != old. Not sure which one is better to take as cursor for flinging.
517                    debug!(
518                        "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \
519                            Raw velocity is {velocity:?}."
520                    );
521
522                    // Multiplying the initial velocity gives the fling a much more snappy feel
523                    // and serves well as a poor-mans acceleration algorithm.
524                    let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX);
525                    match touch_sequence.prevent_move {
526                        TouchMoveAllowed::Allowed => {
527                            touch_sequence.state = Flinging { velocity, point }
528                            // todo: return Touchaction here, or is it sufficient to just
529                            // wait for the next vsync?
530                        },
531                        TouchMoveAllowed::Pending => {
532                            touch_sequence.state = PendingFling { velocity, point }
533                        },
534                        TouchMoveAllowed::Prevented => touch_sequence.state = Finished,
535                    }
536                } else {
537                    touch_sequence.state = Finished;
538                }
539            },
540            Pinching => {
541                touch_sequence.state = Touching;
542            },
543            MultiTouch => {
544                // We stay in multi-touch mode once we entered it until all fingers are lifted.
545                if touch_sequence.active_touch_points.is_empty() {
546                    touch_sequence.state = Finished;
547                }
548            },
549            PendingFling { .. } | Flinging { .. } | PendingClick(_) | Finished => {
550                error!("Touch-up received, but touch handler already in post-touchup state.")
551            },
552        }
553        #[cfg(debug_assertions)]
554        if touch_sequence.active_touch_points.is_empty() {
555            debug_assert!(
556                touch_sequence.is_finished(),
557                "Did not transition to a finished state: {:?}",
558                touch_sequence.state
559            );
560        }
561        debug!(
562            "Touch up with remaining active touchpoints: {:?}, in sequence {:?}",
563            touch_sequence.active_touch_points.len(),
564            self.current_sequence_id
565        );
566    }
567
568    pub fn on_touch_cancel(&mut self, id: TouchId, _point: Point2D<f32, DevicePixel>) {
569        // A similar thing with touch move can happen here where the event is coming from a different webview.
570        let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
571            return;
572        };
573        match touch_sequence
574            .active_touch_points
575            .iter()
576            .position(|t| t.id == id)
577        {
578            Some(i) => {
579                touch_sequence.active_touch_points.swap_remove(i);
580            },
581            None => {
582                warn!("Got a touchcancel event for a non-active touch point");
583                return;
584            },
585        }
586        if touch_sequence.active_touch_points.is_empty() {
587            touch_sequence.state = Finished;
588        }
589    }
590
591    pub(crate) fn get_hit_test_result_cache_value(&self) -> Option<CompositorHitTestResult> {
592        let sequence = self.touch_sequence_map.get(&self.current_sequence_id)?;
593        if sequence.state == Finished {
594            return None;
595        }
596        sequence
597            .hit_test_result_cache
598            .as_ref()
599            .map(|cache| Some(cache.value.clone()))?
600    }
601
602    pub(crate) fn set_hit_test_result_cache_value(
603        &mut self,
604        value: CompositorHitTestResult,
605        device_pixels_per_page: Scale<f32, CSSPixel, DevicePixel>,
606    ) {
607        if let Some(sequence) = self.touch_sequence_map.get_mut(&self.current_sequence_id) {
608            if sequence.hit_test_result_cache.is_none() {
609                sequence.hit_test_result_cache = Some(HitTestResultCache {
610                    value,
611                    device_pixels_per_page,
612                });
613            }
614        }
615    }
616
617    pub(crate) fn add_pending_touch_input_event(
618        &self,
619        id: InputEventId,
620        event_type: TouchEventType,
621    ) {
622        self.pending_touch_input_events.borrow_mut().insert(
623            id,
624            PendingTouchInputEvent {
625                event_type,
626                sequence_id: self.current_sequence_id,
627            },
628        );
629    }
630
631    pub(crate) fn take_pending_touch_input_event(
632        &self,
633        id: InputEventId,
634    ) -> Option<PendingTouchInputEvent> {
635        self.pending_touch_input_events.borrow_mut().remove(&id)
636    }
637}
638
639/// This data structure is used to store information about touch events that are
640/// sent from the Renderer to the Constellation, so that they can finish processing
641/// once their DOM events are fired.
642pub(crate) struct PendingTouchInputEvent {
643    pub event_type: TouchEventType,
644    pub sequence_id: TouchSequenceId,
645}
646
647pub(crate) struct FlingRefreshDriverObserver {
648    pub webview_id: WebViewId,
649}
650
651impl RefreshDriverObserver for FlingRefreshDriverObserver {
652    fn frame_started(&self, compositor: &mut Painter) -> bool {
653        compositor
654            .webview_renderer_mut(self.webview_id)
655            .is_some_and(WebViewRenderer::update_touch_handling_at_new_frame_start)
656    }
657}