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