From 59b7f43ebfc5d6a0fcbc25c1f8e203e3579c2472 Mon Sep 17 00:00:00 2001 From: webbeef Date: Tue, 21 Jul 2026 14:56:55 -0700 Subject: [PATCH] servo: improve touch flinging Signed-off-by: webbeef --- patches/components/paint/touch.rs.patch | 717 +++++++++++++++++- .../paint/webview_renderer.rs.patch | 102 ++- .../document/document_event_handler.rs.patch | 24 +- .../shared/embedder/input_events.rs.patch | 22 +- 4 files changed, 826 insertions(+), 39 deletions(-) diff --git a/patches/components/paint/touch.rs.patch b/patches/components/paint/touch.rs.patch index 84be0b3..0fdc387 100644 --- a/patches/components/paint/touch.rs.patch +++ b/patches/components/paint/touch.rs.patch @@ -1,6 +1,303 @@ --- original +++ modified -@@ -327,6 +327,17 @@ +@@ -3,11 +3,13 @@ + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + + use std::cell::{Cell, RefCell}; ++use std::collections::VecDeque; + use std::rc::Rc; ++use std::time::Instant; + + use embedder_traits::{InputEventId, PaintHitTestResult, Scroll, TouchEventType, TouchId}; + use euclid::{Point2D, Scale, Vector2D}; +-use log::{debug, error, warn}; ++use log::{debug, error, info, warn}; + use rustc_hash::{FxHashMap, FxHashSet}; + use servo_base::id::WebViewId; + use style_traits::CSSPixel; +@@ -42,13 +44,157 @@ + + /// Minimum number of `DeviceIndependentPixel` to begin touch scrolling/Pinching. + const TOUCH_PAN_MIN_SCREEN_PX: f32 = 10.0; +-/// Factor by which the flinging velocity changes on each tick. +-const FLING_SCALING_FACTOR: f32 = 0.95; +-/// Minimum velocity required for transitioning to fling when panning ends. +-const FLING_MIN_SCREEN_PX: f32 = 3.0; +-/// Maximum velocity when flinging. +-const FLING_MAX_SCREEN_PX: f32 = 4000.0; ++/// Time window over which the release velocity is estimated from recent finger ++/// positions. Short enough to stay responsive to the final motion, long enough ++/// to be robust to input-sampling jitter. The estimate is displacement / elapsed ++/// time, so it is independent of how the input events happen to be chunked. ++const FLING_VELOCITY_WINDOW_MS: u32 = 60; ++/// Exponential friction: fling velocity decays as `v(t) = v0 * e^(-k t)` with ++/// `k = FLING_DECEL_PER_S`. Larger means a shorter glide. Total glide distance is ++/// ~`v0 / k`, independent of the frame/refresh rate. ++const FLING_DECEL_PER_S: f32 = 3.0; ++/// Multiplier applied to the measured release velocity when a fling starts, to ++/// give it "snap" (the honest windowed velocity alone can feel heavy). Capped by ++/// `FLING_MAX_SPEED_PX_S`. ++const FLING_INITIAL_BOOST: f32 = 1.5; ++/// Minimum release speed (device px / s) required to start a fling. ++const FLING_MIN_SPEED_PX_S: f32 = 250.0; ++/// Speed (device px / s) below which an ongoing fling stops. ++const FLING_STOP_SPEED_PX_S: f32 = 40.0; ++/// Cap on the initial fling speed (device px / s), to avoid absurd throws. ++const FLING_MAX_SPEED_PX_S: f32 = 12000.0; ++/// Clamp on a single fling frame's elapsed time, so a stall can't produce one ++/// giant jump when the fling resumes. ++const FLING_MAX_FRAME_DT_S: f32 = 0.05; ++/// Fling acceleration: a fraction of an interrupted fling's residual velocity is ++/// folded into a quick same-direction re-flick so repeated flings compound. ++const FLING_ACCEL_MULT: f32 = 1.0; ++/// Max time between interrupting a fling (touch-down) and the new fling starting ++/// for the carried momentum to still apply. ++const FLING_ACCEL_MAX_PAUSE_MS: u32 = 500; ++/// The interrupted fling must have still been moving at least this fast (device ++/// px / s) for its momentum to be carried into the next fling. ++const FLING_ACCEL_MIN_CARRY_PX_S: f32 = 300.0; ++ ++/// Runtime-overridable copy of the fling tuning parameters above. Defaults match the ++/// consts; if `$SERVO_FLING_PARAMS` points to a `key = value` file it is re-read ++/// at the start of every touch sequence. ++#[derive(Clone, Copy, Debug)] ++struct FlingParams { ++ pan_min_px: f32, ++ velocity_window_ms: u32, ++ decel_per_s: f32, ++ initial_boost: f32, ++ min_speed_px_s: f32, ++ stop_speed_px_s: f32, ++ max_speed_px_s: f32, ++ max_frame_dt_s: f32, ++ accel_mult: f32, ++ accel_max_pause_ms: u32, ++ accel_min_carry_px_s: f32, ++} ++ ++impl Default for FlingParams { ++ fn default() -> Self { ++ Self { ++ pan_min_px: TOUCH_PAN_MIN_SCREEN_PX, ++ velocity_window_ms: FLING_VELOCITY_WINDOW_MS, ++ decel_per_s: FLING_DECEL_PER_S, ++ initial_boost: FLING_INITIAL_BOOST, ++ min_speed_px_s: FLING_MIN_SPEED_PX_S, ++ stop_speed_px_s: FLING_STOP_SPEED_PX_S, ++ max_speed_px_s: FLING_MAX_SPEED_PX_S, ++ max_frame_dt_s: FLING_MAX_FRAME_DT_S, ++ accel_mult: FLING_ACCEL_MULT, ++ accel_max_pause_ms: FLING_ACCEL_MAX_PAUSE_MS, ++ accel_min_carry_px_s: FLING_ACCEL_MIN_CARRY_PX_S, ++ } ++ } ++} ++ ++impl FlingParams { ++ /// Load overrides from the `$SERVO_FLING_PARAMS` file if set, else defaults. ++ fn load() -> Self { ++ let mut params = Self::default(); ++ let Ok(path) = std::env::var("SERVO_FLING_PARAMS") else { ++ return params; ++ }; ++ let Ok(contents) = std::fs::read_to_string(path) else { ++ return params; ++ }; ++ for line in contents.lines() { ++ // Drop an inline or whole-line comment ('#' to end of line), then trim. ++ let line = line.split_once('#').map_or(line, |(before, _)| before).trim(); ++ if line.is_empty() { ++ continue; ++ } ++ let Some((key, value)) = line.split_once('=') else { ++ continue; ++ }; ++ let value = value.trim(); ++ match key.trim() { ++ "pan_min_px" => params.pan_min_px = value.parse().unwrap_or(params.pan_min_px), ++ "velocity_window_ms" => { ++ params.velocity_window_ms = value.parse().unwrap_or(params.velocity_window_ms) ++ }, ++ "decel_per_s" => params.decel_per_s = value.parse().unwrap_or(params.decel_per_s), ++ "initial_boost" => { ++ params.initial_boost = value.parse().unwrap_or(params.initial_boost) ++ }, ++ "min_speed_px_s" => { ++ params.min_speed_px_s = value.parse().unwrap_or(params.min_speed_px_s) ++ }, ++ "stop_speed_px_s" => { ++ params.stop_speed_px_s = value.parse().unwrap_or(params.stop_speed_px_s) ++ }, ++ "max_speed_px_s" => { ++ params.max_speed_px_s = value.parse().unwrap_or(params.max_speed_px_s) ++ }, ++ "max_frame_dt_s" => { ++ params.max_frame_dt_s = value.parse().unwrap_or(params.max_frame_dt_s) ++ }, ++ "accel_mult" => params.accel_mult = value.parse().unwrap_or(params.accel_mult), ++ "accel_max_pause_ms" => { ++ params.accel_max_pause_ms = value.parse().unwrap_or(params.accel_max_pause_ms) ++ }, ++ "accel_min_carry_px_s" => { ++ params.accel_min_carry_px_s = ++ value.parse().unwrap_or(params.accel_min_carry_px_s) ++ }, ++ _ => {}, ++ } ++ } ++ params ++ } ++} ++ ++thread_local! { ++ /// Fling params in effect on this compositor thread; refreshed by ++ /// `reload_fling_params()` at the start of each touch sequence. ++ static FLING_PARAMS: Cell = Cell::new(FlingParams::load()); ++} ++ ++/// The fling params for the current gesture. ++fn fling_params() -> FlingParams { ++ FLING_PARAMS.with(|params| params.get()) ++} ++ ++/// Re-read `$SERVO_FLING_PARAMS` if set, so edits apply to the next gesture. ++fn reload_fling_params() { ++ FLING_PARAMS.with(|params| params.set(FlingParams::load())); ++} + ++/// Fold carried fling momentum into one axis of a new fling, but only when this ++/// axis was flicked in the same direction (an opposite or unflicked axis is left ++/// alone). ++fn accelerate_axis(new: f32, carried: f32, mult: f32) -> f32 { ++ if new != 0.0 && (new > 0.0) == (carried > 0.0) { ++ new + carried * mult ++ } else { ++ new ++ } ++} ++ + pub struct TouchHandler { + /// The [`WebViewId`] of the `WebView` this [`TouchHandler`] is associated with. + webview_id: WebViewId, +@@ -60,6 +206,9 @@ + pub(crate) pending_touch_input_events: RefCell>, + /// Whether or not the [`FlingRefreshDriverObserver`] is currently observing frames for fling. + observing_frames_for_fling: Cell, ++ /// Residual velocity + interruption time of a fling cut short by a new touch, ++ /// so a quick same-direction re-flick can build on it (fling acceleration). ++ carried_fling: Option<(Vector2D, Instant)>, + } + + /// Whether the default move action is allowed or not. +@@ -86,6 +235,14 @@ + device_pixels_per_page: Scale, + } + ++/// A timestamped finger position, used to estimate fling velocity over a time ++/// window (see `TouchSequenceInfo::windowed_velocity`). ++#[derive(Clone, Copy, Debug)] ++struct PanSample { ++ time: Instant, ++ point: Point2D, ++} ++ + pub struct TouchSequenceInfo { + /// touch sequence state + pub(crate) state: TouchSequenceState, +@@ -117,6 +274,18 @@ + pending_touch_move_actions: Vec, + /// Cache for the last touch hit test result. + hit_test_result_cache: Option, ++ /// Recent timestamped finger positions while panning, used to estimate the ++ /// release velocity for a fling over a fixed time window ++ /// (`FLING_VELOCITY_WINDOW_MS`). Time-based, so it is independent of the ++ /// input event rate. ++ pan_samples: VecDeque, ++ /// Timestamp of the previous fling frame, for time-based velocity decay. ++ fling_last_frame: Option, ++ /// This touch was forwarded by the chrome to an embedded `` (see ++ /// `InputEventResult::ForwardedToEmbeddedWebView`). When set, this (chrome) ++ /// handler neither pans/flings nor clicks the sequence; it only relays the ++ /// events onward, and its moves are not throttled. ++ forwarded_to_embedded: bool, + } + + impl TouchSequenceInfo { +@@ -124,6 +293,40 @@ + self.active_touch_points.len() + } + ++ /// Record a timestamped finger position and drop samples that have aged out ++ /// of the velocity window relative to `now`. ++ fn record_pan_sample(&mut self, now: Instant, point: Point2D) { ++ self.pan_samples.push_back(PanSample { time: now, point }); ++ self.trim_pan_samples(now); ++ } ++ ++ /// Drop pan samples older than `FLING_VELOCITY_WINDOW_MS` relative to `now`, ++ /// so a pause before lift-off collapses the window to a near-zero velocity ++ /// (and therefore no fling). ++ fn trim_pan_samples(&mut self, now: Instant) { ++ let window_ms = fling_params().velocity_window_ms; ++ while let Some(front) = self.pan_samples.front() { ++ if now.saturating_duration_since(front.time).as_millis() > window_ms.into() { ++ self.pan_samples.pop_front(); ++ } else { ++ break; ++ } ++ } ++ } ++ ++ /// Estimate the current pan velocity in device px / s from the sample window ++ /// (displacement over elapsed time). Zero if there is not enough history. ++ fn windowed_velocity(&self) -> Vector2D { ++ let (Some(first), Some(last)) = (self.pan_samples.front(), self.pan_samples.back()) else { ++ return Vector2D::zero(); ++ }; ++ let dt = last.time.saturating_duration_since(first.time).as_secs_f32(); ++ if dt <= 0.0 { ++ return Vector2D::zero(); ++ } ++ (last.point - first.point) / dt ++ } ++ + fn pinch_distance_and_center(&self) -> (f32, Point2D) { + debug_assert_eq!(self.touch_count(), 2); + let p0 = self.active_touch_points[0].point; +@@ -177,10 +380,10 @@ + pub(crate) enum TouchSequenceState { + /// touch point is active but does not start moving + Touching, +- /// A single touch point is active and has started panning. +- Panning { +- velocity: Vector2D, +- }, ++ /// A single touch point is active and has started panning. The release ++ /// velocity used for a fling is derived from `pan_samples` at touch-up, so ++ /// no velocity is stored here. ++ Panning, + /// A two-finger pinch zoom gesture is active. + Pinching, + /// A multi-touch gesture is in progress. +@@ -219,6 +422,9 @@ + prevent_move: TouchMoveAllowed::Pending, + pending_touch_move_actions: vec![], + hit_test_result_cache: None, ++ pan_samples: VecDeque::new(), ++ fling_last_frame: None, ++ forwarded_to_embedded: false, + }; + // We insert a simulated initial touch sequence, which is already finished, + // so that we always have one element in the map, which simplifies creating +@@ -231,6 +437,7 @@ + touch_sequence_map, + pending_touch_input_events: Default::default(), + observing_frames_for_fling: Default::default(), ++ carried_fling: None, + } + } + +@@ -327,6 +534,31 @@ self.touch_sequence_map.get(&self.current_sequence_id) } @@ -11,10 +308,426 @@ + matches!( + self.try_get_current_touch_sequence() + .map(|sequence| sequence.state), -+ Some(Panning { .. } | PendingFling { .. } | Flinging { .. }) ++ Some(Panning | PendingFling { .. } | Flinging { .. }) + ) + } ++ ++ /// Mark a touch sequence as forwarded to an embedded webview, so this (chrome) ++ /// handler stops panning/flinging/clicking it and its moves are not throttled. ++ pub(crate) fn mark_sequence_forwarded(&mut self, sequence_id: TouchSequenceId) { ++ if let Some(sequence) = self.touch_sequence_map.get_mut(&sequence_id) { ++ sequence.forwarded_to_embedded = true; ++ } ++ } ++ ++ /// Whether the current touch sequence is being forwarded to an embedded webview. ++ pub(crate) fn is_forwarding_current_touch(&self) -> bool { ++ self.try_get_current_touch_sequence() ++ .is_some_and(|sequence| sequence.forwarded_to_embedded) ++ } + fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> { self.touch_sequence_map.get_mut(&self.current_sequence_id) } +@@ -345,6 +577,32 @@ + } + + pub(crate) fn on_touch_down(&mut self, touch_id: TouchId, point: Point2D) { ++ // Refresh the (dev-only) fling tuning knobs at the start of each gesture, ++ // so editing $SERVO_FLING_PARAMS applies to the very next swipe. ++ reload_fling_params(); ++ ++ // If a fling is still in flight, remember its residual velocity so a quick ++ // same-direction re-flick can build on it (fling acceleration); a touch-down ++ // that isn't interrupting a fling clears any stale carry. ++ self.carried_fling = self ++ .touch_sequence_map ++ .get(&self.current_sequence_id) ++ .and_then(|sequence| match sequence.state { ++ Flinging { velocity, .. } => Some((velocity, Instant::now())), ++ _ => None, ++ }); ++ match self.carried_fling { ++ Some((velocity, _)) => info!( ++ "[touch] down: {touch_id:?} in {:?} -- carrying {:.0} px/s from interrupted fling", ++ self.webview_id, ++ velocity.length(), ++ ), ++ None => info!( ++ "[touch] down: {touch_id:?} in {:?} -- nothing to carry", ++ self.webview_id, ++ ), ++ } ++ + // if the current sequence ID does not exist in the map, then it was already handled + if !self + .touch_sequence_map +@@ -354,6 +612,10 @@ + { + self.current_sequence_id.next(); + debug!("Entered new touch sequence: {:?}", self.current_sequence_id); ++ info!( ++ "[touch] down: {touch_id:?} -> NEW sequence {:?}", ++ self.current_sequence_id ++ ); + let active_touch_points = vec![TouchPoint::new(touch_id, point)]; + self.touch_sequence_map.insert( + self.current_sequence_id, +@@ -365,6 +627,9 @@ + prevent_move: TouchMoveAllowed::Pending, + pending_touch_move_actions: vec![], + hit_test_result_cache: None, ++ pan_samples: VecDeque::new(), ++ fling_last_frame: None, ++ forwarded_to_embedded: false, + }, + ); + } else { +@@ -383,6 +648,12 @@ + } + // Multiple fingers prevent a click. + touch_sequence.prevent_click = true; ++ let point_count = touch_sequence.active_touch_points.len(); ++ let state = touch_sequence.state; ++ info!( ++ "[touch] down: {touch_id:?} -> added to existing {:?}, now {point_count} points ({state:?})", ++ self.current_sequence_id, ++ ); + } + } + +@@ -389,33 +660,51 @@ + pub(crate) fn notify_new_frame_start(&mut self) -> Option { + let touch_sequence = self.touch_sequence_map.get_mut(&self.current_sequence_id)?; + ++ // `TouchSequenceState` is `Copy`, so this reads the velocity/cursor out ++ // without holding a borrow of `state` (we mutate `fling_last_frame` and ++ // write `state` back below). + let Flinging { + velocity, + point: cursor, +- } = &mut touch_sequence.state ++ } = touch_sequence.state + else { + self.observing_frames_for_fling.set(false); + return None; + }; + +- if velocity.length().abs() < FLING_MIN_SCREEN_PX { ++ let params = fling_params(); ++ if velocity.length() < params.stop_speed_px_s { + self.stop_fling_if_needed(); +- None +- } else { +- // TODO: Probably we should multiply with the current refresh rate (and divide on each frame) +- // or save a timestamp to account for a potentially changing display refresh rate. +- *velocity *= FLING_SCALING_FACTOR; +- let _span = profile_traits::info_span!( +- "TouchHandler::Flinging", +- velocity = ?velocity, +- ) +- .entered(); +- debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX); +- Some(FlingAction { +- delta: DeviceVector2D::new(velocity.x, velocity.y), +- cursor: *cursor, +- }) ++ return None; + } ++ ++ // Advance by real elapsed time so the glide distance is independent of the ++ // frame/refresh rate. `dt` is clamped so a stall can't produce a huge jump. ++ let now = Instant::now(); ++ let dt = touch_sequence ++ .fling_last_frame ++ .map(|last| now.saturating_duration_since(last).as_secs_f32()) ++ .unwrap_or(0.0) ++ .min(params.max_frame_dt_s); ++ touch_sequence.fling_last_frame = Some(now); ++ ++ // Exponential friction: v(t) = v0 * e^(-k t); this frame moves v * dt. ++ let decayed = velocity * (-params.decel_per_s * dt).exp(); ++ let delta = decayed * dt; ++ touch_sequence.state = Flinging { ++ velocity: decayed, ++ point: cursor, ++ }; ++ ++ let _span = profile_traits::info_span!( ++ "TouchHandler::Flinging", ++ velocity = ?decayed, ++ ) ++ .entered(); ++ Some(FlingAction { ++ delta: DeviceVector2D::new(delta.x, delta.y), ++ cursor, ++ }) + } + + pub(crate) fn stop_fling_if_needed(&mut self) { +@@ -426,11 +715,14 @@ + ); + return; + }; +- let Flinging { .. } = touch_sequence.state else { ++ let Flinging { velocity, .. } = touch_sequence.state else { + return; + }; ++ info!( ++ "[fling] end: stopped at {:.0} px/s in {current_sequence_id:?}", ++ velocity.length() ++ ); + let _span = profile_traits::info_span!("TouchHandler::FlingEnd").entered(); +- debug!("Stopping flinging in touch sequence {current_sequence_id:?}"); + touch_sequence.state = Finished; + // If we were flinging previously, there could still be a touch_up event result + // coming in after we stopped flinging +@@ -447,7 +739,12 @@ + // As `TouchHandler` is per `WebViewRenderer` which is per `WebView` we might get a Touch Sequence Move that + // started with a down on a different webview. As the touch_sequence id is only changed on touch_down this + // move event gets a touch id which is already cleaned up. +- let touch_sequence = self.try_get_current_touch_sequence_mut()?; ++ let seq_id = self.current_sequence_id; ++ let webview_id = self.webview_id; ++ let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else { ++ info!("[touch] move DROPPED: no current sequence {seq_id:?} in {webview_id:?}"); ++ return None; ++ }; + let idx = match touch_sequence + .active_touch_points + .iter_mut() +@@ -455,20 +752,50 @@ + { + Some(i) => i, + None => { +- error!("Got a touchmove event for a non-active touch point"); ++ info!( ++ "[touch] move DROPPED: {touch_id:?} not active in {webview_id:?} sequence \ ++ {seq_id:?} (state {:?}) -- routed to the wrong sequence/renderer", ++ touch_sequence.state, ++ ); + return None; + }, + }; ++ ++ // Forwarded to an embedded webview: the chrome must not pan/scroll this ++ // sequence locally. The event is still relayed to the chrome's script ++ // (unthrottled, see WebViewRenderer::on_touch_move) which forwards it onward. ++ if touch_sequence.forwarded_to_embedded { ++ info!("[touch] move relayed to embedded, chrome not panning ({touch_id:?})"); ++ return None; ++ } ++ + let old_point = touch_sequence.active_touch_points[idx].point; + let delta = point - old_point; + touch_sequence.update_hit_test_result_cache_pointer(delta); + ++ // Minimum finger travel (device px) before a pan/pinch begins; tunable. ++ let pan_min = fling_params().pan_min_px * scale; + let action = match touch_sequence.touch_count() { + 1 => { +- if let Panning { ref mut velocity } = touch_sequence.state { +- // TODO: Probably we should track 1-3 more points and use a smarter algorithm +- *velocity += delta; +- *velocity /= 2.0; ++ // Record the finger position for the time-based release-velocity ++ // estimate consumed when this pan ends (see on_touch_up). ++ touch_sequence.record_pan_sample(Instant::now(), point); ++ let is_panning = matches!(touch_sequence.state, Panning); ++ if is_panning || delta.x.abs() > pan_min || delta.y.abs() > pan_min { ++ if !is_panning { ++ info!( ++ "[touch] pan start: delta ({:.1},{:.1}) exceeded pan_min {:.1}", ++ delta.x, delta.y, pan_min, ++ ); ++ let _span = profile_traits::info_span!( ++ "TouchHandler::ScrollBegin", ++ delta = ?delta, ++ ) ++ .entered(); ++ touch_sequence.state = Panning; ++ // No clicks should be issued after we transitioned to move. ++ touch_sequence.prevent_click = true; ++ } + // update the touch point every time when panning. + touch_sequence.active_touch_points[idx].point = point; + +@@ -477,37 +804,20 @@ + scroll: Scroll::Delta((-delta).into()), + point, + })) +- } else if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale || +- delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale +- { +- let _span = profile_traits::info_span!( +- "TouchHandler::ScrollBegin", +- delta = ?delta, +- ) +- .entered(); +- touch_sequence.state = Panning { +- velocity: Vector2D::new(delta.x, delta.y), +- }; +- // No clicks should be issued after we transitioned to move. +- touch_sequence.prevent_click = true; +- // update the touch point +- touch_sequence.active_touch_points[idx].point = point; +- +- // Scroll offsets are opposite to the direction of finger motion. +- Some(ScrollZoomEvent::Scroll(ScrollEvent { +- scroll: Scroll::Delta((-delta).into()), +- point, +- })) + } else { + // We don't update the touchpoint, so multiple small moves can + // accumulate and merge into a larger move. ++ info!( ++ "[touch] sub-threshold move: accumulated delta ({:.1},{:.1}) <= pan_min {:.1}", ++ delta.x, delta.y, pan_min, ++ ); + None + } + }, + 2 => { + if touch_sequence.state == Pinching || +- delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale || +- delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX * scale ++ delta.x.abs() > pan_min || ++ delta.y.abs() > pan_min + { + touch_sequence.state = Pinching; + let (d0, _) = touch_sequence.pinch_distance_and_center(); +@@ -541,45 +851,105 @@ + } + + pub(crate) fn on_touch_up(&mut self, touch_id: TouchId, point: Point2D) { ++ // Take any carried fling momentum now, before borrowing the sequence; it's ++ // folded into a same-direction re-flick below (fling acceleration). ++ let webview_id = self.webview_id; ++ let carried_fling = self.carried_fling.take(); + let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else { + warn!("Current touch sequence not found"); + return; + }; +- let old = match touch_sequence ++ info!( ++ "[touch] up: {touch_id:?} in {webview_id:?}, state {:?}", ++ touch_sequence.state ++ ); ++ if let Some(i) = touch_sequence + .active_touch_points + .iter() + .position(|t| t.touch_id == touch_id) + { +- Some(i) => Some(touch_sequence.active_touch_points.swap_remove(i).point), +- None => { +- warn!("Got a touchup event for a non-active touch point"); +- None +- }, +- }; ++ touch_sequence.active_touch_points.swap_remove(i); ++ } else { ++ warn!("Got a touchup event for a non-active touch point"); ++ } ++ // Forwarded to an embedded webview: the chrome must not synthesize a click ++ // or start a fling for a touch it doesn't own. The embedded webview handles ++ // its own click/fling from the relayed sequence. ++ if touch_sequence.forwarded_to_embedded { ++ touch_sequence.state = Finished; ++ return; ++ } + match touch_sequence.state { + Touching => { + if touch_sequence.prevent_click { + touch_sequence.state = Finished; + } else { ++ info!( ++ "[touch] up in Touching -> CLICK (never crossed pan threshold this gesture)" ++ ); + touch_sequence.state = PendingClick(point); + } + }, +- Panning { velocity } => { +- if velocity.length().abs() >= FLING_MIN_SCREEN_PX { ++ Panning => { ++ // Estimate the release velocity (device px / s) from the recent ++ // sample window, dropping stale samples so a pause before lifting ++ // yields a near-zero velocity and no fling. ++ let params = fling_params(); ++ let now = Instant::now(); ++ touch_sequence.trim_pan_samples(now); ++ let release_velocity = touch_sequence.windowed_velocity(); ++ let release_speed = release_velocity.length(); ++ if release_speed >= params.min_speed_px_s { ++ // Snap boost, then fold in any carried momentum from a fling this ++ // gesture just interrupted, so rapid same-direction re-flicks ++ // compound (fling acceleration). Capped by max_speed_px_s. ++ let mut velocity = release_velocity * params.initial_boost; ++ match carried_fling { ++ Some((carried, cancel_time)) => { ++ let pause_ms = now.saturating_duration_since(cancel_time).as_millis(); ++ let carried_speed = carried.length(); ++ if pause_ms <= params.accel_max_pause_ms.into() && ++ carried_speed >= params.accel_min_carry_px_s ++ { ++ velocity.x = ++ accelerate_axis(velocity.x, carried.x, params.accel_mult); ++ velocity.y = ++ accelerate_axis(velocity.y, carried.y, params.accel_mult); ++ info!( ++ "[fling] accelerate: boosted {:.0} + carried {:.0} (pause {}ms) = {:.0} px/s", ++ release_speed * params.initial_boost, ++ carried_speed, ++ pause_ms, ++ velocity.length(), ++ ); ++ } else { ++ info!( ++ "[fling] no accel: carried {:.0} px/s pause {}ms (need >= {:.0} px/s, <= {}ms)", ++ carried_speed, ++ pause_ms, ++ params.accel_min_carry_px_s, ++ params.accel_max_pause_ms, ++ ); ++ } ++ }, ++ None => info!("[fling] no carry available"), ++ } ++ let velocity = velocity.with_max_length(params.max_speed_px_s); ++ info!( ++ "[fling] launch: release {:.0} -> {:.0} px/s (cap {:.0}, decel {:.1}/s)", ++ release_speed, ++ velocity.length(), ++ params.max_speed_px_s, ++ params.decel_per_s, ++ ); + let _span = profile_traits::info_span!( + "TouchHandler::FlingStart", + velocity = ?velocity, + ) + .entered(); +- // TODO: point != old. Not sure which one is better to take as cursor for flinging. +- debug!( +- "Transitioning to Fling. Cursor is {point:?}. Old cursor was {old:?}. \ +- Raw velocity is {velocity:?}." +- ); +- +- // Multiplying the initial velocity gives the fling a much more snappy feel +- // and serves well as a poor-mans acceleration algorithm. +- let velocity = (velocity * 2.0).with_max_length(FLING_MAX_SCREEN_PX); ++ // Seed the decay clock now, so the first fling frame advances by ++ // the real time since lift-off (clamped in notify_new_frame_start). ++ touch_sequence.fling_last_frame = Some(now); + match touch_sequence.prevent_move { + TouchMoveAllowed::Allowed => { + touch_sequence.state = Flinging { velocity, point } +@@ -592,6 +962,10 @@ + TouchMoveAllowed::Prevented => touch_sequence.state = Finished, + } + } else { ++ info!( ++ "[fling] no fling: release {:.0} px/s < min {:.0}", ++ release_speed, params.min_speed_px_s, ++ ); + let _span = profile_traits::info_span!("TouchHandler::ScrollEnd").entered(); + touch_sequence.state = Finished; + } diff --git a/patches/components/paint/webview_renderer.rs.patch b/patches/components/paint/webview_renderer.rs.patch index 9c52bcb..17b299f 100644 --- a/patches/components/paint/webview_renderer.rs.patch +++ b/patches/components/paint/webview_renderer.rs.patch @@ -177,7 +177,65 @@ result } -@@ -722,7 +772,7 @@ +@@ -523,12 +573,20 @@ + // When the event is touchmove, if the script thread is processing the touch + // move event, we skip sending the event to the script thread. + // This prevents the script thread from stacking up for a large amount of time. +- if !self.touch_handler.is_handling_touch_move_for_touch_id( +- self.touch_handler.current_sequence_id, +- event.touch_id, +- ) { ++ // ++ // Exception: a touch forwarded to an embedded webview must have every move ++ // reach script so it can be relayed onward; dropping moves here would leave ++ // the embedded webview stuck (down + up, no moves -> spurious link click). So ++ // for forwarded touches we always send and never mark it as "handling". ++ let forwarding = self.touch_handler.is_forwarding_current_touch(); ++ if forwarding || ++ !self.touch_handler.is_handling_touch_move_for_touch_id( ++ self.touch_handler.current_sequence_id, ++ event.touch_id, ++ ) ++ { + reached_constellation = self.send_touch_event(render_api, event, id); +- if reached_constellation && event.is_cancelable() { ++ if !forwarding && reached_constellation && event.is_cancelable() { + self.touch_handler.set_handling_touch_move_for_touch_id( + self.touch_handler.current_sequence_id, + event.touch_id, +@@ -572,6 +630,13 @@ + touch_id, + } = pending_touch_input_event; + ++ // If the chrome forwarded this touch to an embedded webview, mark the sequence ++ // so this handler stops panning/clicking it and stops throttling its moves ++ // (every move must reach script to be relayed onward). ++ if result.contains(InputEventResult::ForwardedToEmbeddedWebView) { ++ self.touch_handler.mark_sequence_forwarded(sequence_id); ++ } ++ + if result.contains(InputEventResult::DefaultPrevented) { + debug!( + "Touch event {:?} in sequence {:?} prevented!", +@@ -625,7 +690,7 @@ + self.touch_handler.remove_touch_sequence(sequence_id); + }, + TouchSequenceState::Touching | +- TouchSequenceState::Panning { .. } | ++ TouchSequenceState::Panning | + TouchSequenceState::Pinching | + TouchSequenceState::MultiTouch | + TouchSequenceState::PendingFling { .. } => { +@@ -691,7 +756,7 @@ + TouchSequenceState::Finished => { + self.touch_handler.remove_touch_sequence(sequence_id); + }, +- TouchSequenceState::Panning { .. } | ++ TouchSequenceState::Panning | + TouchSequenceState::Pinching | + TouchSequenceState::PendingFling { .. } => { + // It's possible to transition from Pinch to pan, Which means that +@@ -722,7 +787,7 @@ ); self.dispatch_input_event_with_hit_testing( render_api, @@ -186,7 +244,7 @@ MouseButtonAction::Down, button, point.into(), -@@ -731,7 +781,7 @@ +@@ -731,7 +796,7 @@ ); self.dispatch_input_event_with_hit_testing( render_api, @@ -195,7 +253,7 @@ MouseButtonAction::Up, button, point.into(), -@@ -753,18 +803,23 @@ +@@ -753,18 +818,23 @@ })); } @@ -225,7 +283,7 @@ } // Batch up all scroll events and changes to pinch zoom into a single change, or -@@ -813,101 +868,136 @@ +@@ -813,101 +883,136 @@ } } @@ -273,11 +331,6 @@ + pinch_zoom_result, + pending_scroll: combined_scroll_event, + } -+ } -+ -+ /// Whether this renderer has a scroll tree for `pipeline_id`. -+ pub(crate) fn owns_pipeline(&self, pipeline_id: PipelineId) -> bool { -+ self.pipelines.contains_key(&pipeline_id) } - /// Perform a hit test at the given [`DevicePoint`] and apply the [`Scroll`] @@ -291,6 +344,11 @@ - scroll: Scroll, - ) -> Option { - let scroll_location = match scroll { ++ /// Whether this renderer has a scroll tree for `pipeline_id`. ++ pub(crate) fn owns_pipeline(&self, pipeline_id: PipelineId) -> bool { ++ self.pipelines.contains_key(&pipeline_id) ++ } ++ + /// Convert a [`Scroll`] into a [`ScrollLocation`] using this renderer's own scale. + fn scroll_location_for(&self, scroll: Scroll) -> ScrollLocation { + match scroll { @@ -307,7 +365,12 @@ - }; + } + } -+ + +- let hit_test_results: Vec<_> = self +- .touch_handler +- .get_hit_test_result_cache_value() +- .map(|result| vec![result]) +- .unwrap_or_else(|| self.hit_test(render_api, cursor)); + /// Scroll the node named by `hit_test_result` (or its nearest scrollable ancestor) in + /// this renderer, using this renderer's own scale, and notify layout. Returns the + /// result if a node actually scrolled. Used by the Painter's `scroll_at_point` for the @@ -361,12 +424,7 @@ + offset, + }) + } - -- let hit_test_results: Vec<_> = self -- .touch_handler -- .get_hit_test_result_cache_value() -- .map(|result| vec![result]) -- .unwrap_or_else(|| self.hit_test(render_api, cursor)); ++ + /// Scroll any scrollable node in this renderer's root pipeline (depth-first). Used to + /// chain an embedded webview's unconsumed scroll to its parent, whose scrollable + /// container (e.g. a horizontal panel strip) is not necessarily the root scroll node and @@ -436,7 +494,7 @@ } /// Scroll the viewport (root pipeline, root scroll node) of this WebView, but first -@@ -949,14 +1039,6 @@ +@@ -949,14 +1054,6 @@ return (pinch_zoom_result, vec![]); }; @@ -451,7 +509,7 @@ self.send_scroll_positions_to_layout_for_pipeline(root_pipeline_id, external_scroll_id); if pinch_zoom_result == PinchZoomResult::DidPinchZoom { -@@ -964,7 +1046,6 @@ +@@ -964,7 +1061,6 @@ } let scroll_result = ScrollResult { @@ -459,7 +517,7 @@ external_scroll_id, offset, }; -@@ -1035,6 +1116,12 @@ +@@ -1035,6 +1131,12 @@ self.page_zoom * self.hidpi_scale_factor } @@ -472,7 +530,7 @@ /// Adjust the pinch zoom of the [`WebView`] by the given zoom delta. pub(crate) fn adjust_pinch_zoom(&mut self, magnification: f32, center: DevicePoint) { if magnification == 1.0 { -@@ -1046,8 +1133,17 @@ +@@ -1046,8 +1148,17 @@ } fn send_window_size_message(&self) { @@ -492,7 +550,7 @@ let device_pixel_ratio = self.device_pixels_per_page_pixel_not_including_pinch_zoom(); // From : // This is the viewport you get after processing the viewport tag. -@@ -1060,6 +1156,7 @@ +@@ -1060,6 +1171,7 @@ hidpi_scale_factor: device_pixel_ratio, size: layout_viewport, device_size: self.screen_size, @@ -500,7 +558,7 @@ }, WindowSizeType::Resize, ), -@@ -1143,8 +1240,21 @@ +@@ -1143,8 +1255,21 @@ !result.contains(InputEventResult::DefaultPrevented) { // A scroll delta for a wheel event is the inverse of the wheel delta. diff --git a/patches/components/script/dom/document/document_event_handler.rs.patch b/patches/components/script/dom/document/document_event_handler.rs.patch index 5cf4818..b110014 100644 --- a/patches/components/script/dom/document/document_event_handler.rs.patch +++ b/patches/components/script/dom/document/document_event_handler.rs.patch @@ -616,7 +616,7 @@ fn handle_touch_event( &self, cx: &mut JSContext, -@@ -1215,6 +1667,29 @@ +@@ -1215,6 +1667,30 @@ event: EmbedderTouchEvent, input_event: &ConstellationInputEvent, ) -> InputEventResult { @@ -639,14 +639,15 @@ + forwarded.swap_remove(pos); + } + -+ return InputEventResult::DefaultPrevented; ++ return InputEventResult::DefaultPrevented | ++ InputEventResult::ForwardedToEmbeddedWebView; + } + } + // Ignore all incoming events without a hit test. let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else { self.update_active_touch_points_when_early_return(event); -@@ -1221,6 +1696,16 @@ +@@ -1221,6 +1697,19 @@ return Default::default(); }; @@ -656,14 +657,17 @@ + self.update_active_touch_points_when_early_return(event); + // Return DefaultPrevented so the parent's compositor doesn't synthesize + // a click for this touch sequence. The embedded webview's compositor will -+ // handle click synthesis for the forwarded touch events. -+ return InputEventResult::DefaultPrevented; ++ // handle click synthesis for the forwarded touch events. ForwardedToEmbeddedWebView ++ // tells the parent compositor to keep the whole sequence flowing to the embedded ++ // webview (no move throttling) and not to pan/fling the chrome itself. ++ return InputEventResult::DefaultPrevented | ++ InputEventResult::ForwardedToEmbeddedWebView; + } + let TouchId(identifier) = event.touch_id; let Some(element) = hit_test_result -@@ -1392,6 +1877,10 @@ +@@ -1392,6 +1881,10 @@ .safe_borrow_mut(cx.no_gc()) .push(Dom::from_ref(&*pointer_touch)); self.set_active_element(&element); @@ -674,7 +678,7 @@ (current_target, pointer_touch) }, _ => { -@@ -1426,15 +1915,47 @@ +@@ -1426,15 +1919,47 @@ ); let mut active_touch_points = self.active_touch_points.safe_borrow_mut(cx.no_gc()); @@ -723,7 +727,7 @@ }, TouchEventType::Down => unreachable!("Should have been handled above"), } -@@ -1482,6 +2003,19 @@ +@@ -1482,6 +2007,19 @@ ); let event = touch_event.upcast::(); event.fire(cx, &touch_dispatch_target); @@ -743,7 +747,7 @@ event.flags().into() } -@@ -1616,6 +2150,16 @@ +@@ -1616,6 +2154,16 @@ return Default::default(); }; @@ -760,7 +764,7 @@ let Some(el) = hit_test_result .node .inclusive_ancestors(ShadowIncluding::Yes) -@@ -2180,6 +2724,26 @@ +@@ -2180,6 +2728,26 @@ return; } diff --git a/patches/components/shared/embedder/input_events.rs.patch b/patches/components/shared/embedder/input_events.rs.patch index b216ee4..a715044 100644 --- a/patches/components/shared/embedder/input_events.rs.patch +++ b/patches/components/shared/embedder/input_events.rs.patch @@ -1,6 +1,18 @@ --- original +++ modified -@@ -69,6 +69,17 @@ +@@ -39,6 +39,11 @@ + /// is sent while Servo is shutting down or when it is in an intermediate state. + /// Typically these events should be considered to be consumed. + const DispatchFailed = 1 << 2; ++ /// The touch event was forwarded by the chrome to an embedded `` (the touch ++ /// belongs to that embedded webview, not the chrome). The compositor uses this to keep ++ /// the whole sequence flowing to the embedded webview (no move throttling) and to stop ++ /// the chrome from panning / clicking a touch it doesn't own. ++ const ForwardedToEmbeddedWebView = 1 << 3; + } + } + +@@ -69,6 +74,17 @@ pub id: InputEventId, } @@ -18,7 +30,7 @@ impl From for InputEventAndId { fn from(event: InputEvent) -> Self { Self { -@@ -101,6 +112,31 @@ +@@ -101,6 +117,31 @@ InputEvent::Wheel(event) => Some(event.point), } } @@ -50,7 +62,7 @@ } #[derive(Clone, Debug, Default, Deserialize, Serialize)] -@@ -149,6 +185,11 @@ +@@ -149,6 +190,11 @@ pub action: MouseButtonAction, pub button: MouseButton, pub point: WebViewPoint, @@ -62,7 +74,7 @@ } impl MouseButtonEvent { -@@ -157,8 +198,23 @@ +@@ -157,8 +203,23 @@ action, button, point, @@ -86,7 +98,7 @@ } /// The types of mouse buttons. -@@ -240,7 +296,7 @@ +@@ -240,7 +301,7 @@ } /// The type of input represented by a multi-touch event. -- 2.51.2