diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 6a20ee72c10..c3226162662 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -1896,6 +1896,16 @@ impl IOCompositor { .push(ScrollZoomEvent::PinchZoom(magnification)); } + /// On a Window refresh tick (e.g. vsync) + pub fn on_vsync(&mut self) { + if let Some(fling_action) = self.touch_handler.on_vsync() { + self.on_scroll_window_event( + ScrollLocation::Delta(fling_action.delta), + fling_action.cursor, + ); + } + } + fn send_scroll_positions_to_layout_for_pipeline(&self, pipeline_id: &PipelineId) { let details = match self.pipeline_details.get(pipeline_id) { Some(details) => details, diff --git a/components/compositing/touch.rs b/components/compositing/touch.rs index 0acd0690b43..5005009d44e 100644 --- a/components/compositing/touch.rs +++ b/components/compositing/touch.rs @@ -3,14 +3,24 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use euclid::{Point2D, Scale, Vector2D}; -use log::warn; +use log::{debug, warn}; use script_traits::{EventResult, TouchId}; use style_traits::DevicePixel; +use webrender_api::units::{DeviceIntPoint, LayoutVector2D}; use self::TouchState::*; +// TODO: All `_SCREEN_PX` units below are currently actually used as `DevicePixel` +// without multiplying with the `hidpi_factor`. This should be fixed and the +// constants adjusted accordingly. /// Minimum number of `DeviceIndependentPixel` to begin touch scrolling. const TOUCH_PAN_MIN_SCREEN_PX: f32 = 20.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; pub struct TouchHandler { pub state: TouchState, @@ -30,9 +40,7 @@ impl TouchPoint { } /// The states of the touch input state machine. -/// -/// TODO: Add support for "flinging" (scrolling inertia) -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum TouchState { /// Not tracking any touch point Nothing, @@ -45,7 +53,14 @@ pub enum TouchState { /// Contains the initial touch location. Touching, /// A single touch point is active and has started panning. - Panning, + Panning { + velocity: Vector2D, + }, + /// No active touch points, but there is still scrolling velocity + Flinging { + velocity: Vector2D, + cursor: DeviceIntPoint, + }, /// A two-finger pinch zoom gesture is active. Pinching, /// A multi-touch gesture is in progress. Contains the number of active touch points. @@ -67,6 +82,11 @@ pub enum TouchAction { NoAction, } +pub(crate) struct FlingAction { + pub delta: LayoutVector2D, + pub cursor: DeviceIntPoint, +} + impl TouchHandler { pub fn new() -> Self { TouchHandler { @@ -81,13 +101,36 @@ impl TouchHandler { self.state = match self.state { Nothing => WaitingForScript, - Touching | Panning => Pinching, + Flinging { .. } => Touching, + Touching | Panning { .. } => Pinching, WaitingForScript => WaitingForScript, DefaultPrevented => DefaultPrevented, Pinching | MultiTouch => MultiTouch, }; } + pub fn on_vsync(&mut self) -> Option { + let Flinging { + velocity, + ref cursor, + } = &mut self.state + else { + return None; + }; + if velocity.length().abs() < FLING_MIN_SCREEN_PX { + self.state = Nothing; + return None; + } + // 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; + debug_assert!(velocity.length() <= FLING_MAX_SCREEN_PX); + Some(FlingAction { + delta: LayoutVector2D::new(velocity.x, velocity.y), + cursor: *cursor, + }) + } + pub fn on_touch_move(&mut self, id: TouchId, point: Point2D) -> TouchAction { let idx = match self.active_touch_points.iter_mut().position(|t| t.id == id) { Some(i) => i, @@ -105,16 +148,24 @@ impl TouchHandler { if delta.x.abs() > TOUCH_PAN_MIN_SCREEN_PX || delta.y.abs() > TOUCH_PAN_MIN_SCREEN_PX { - self.state = Panning; + self.state = Panning { + velocity: Vector2D::new(delta.x, delta.y), + }; TouchAction::Scroll(delta) } else { TouchAction::NoAction } }, - Panning => { + Panning { ref mut velocity } => { let delta = point - old_point; + // TODO: Probably we should track 1-3 more points and use a smarter algorithm + *velocity += delta; + *velocity /= 2.0; TouchAction::Scroll(delta) }, + Flinging { .. } => { + unreachable!("Touch Move event received without preceding down.") + }, DefaultPrevented => TouchAction::DispatchEvent, Pinching => { let (d0, c0) = self.pinch_distance_and_center(); @@ -139,15 +190,14 @@ impl TouchHandler { action } - pub fn on_touch_up(&mut self, id: TouchId, _point: Point2D) -> TouchAction { - match self.active_touch_points.iter().position(|t| t.id == id) { - Some(i) => { - self.active_touch_points.swap_remove(i); - }, + pub fn on_touch_up(&mut self, id: TouchId, point: Point2D) -> TouchAction { + let old = match self.active_touch_points.iter().position(|t| t.id == id) { + Some(i) => Some(self.active_touch_points.swap_remove(i).point), None => { warn!("Got a touch up event for a non-active touch point"); + None }, - } + }; match self.state { Touching => { // FIXME: If the duration exceeds some threshold, send a contextmenu event instead. @@ -155,14 +205,38 @@ impl TouchHandler { self.state = Nothing; TouchAction::Click }, - Nothing | Panning => { + Nothing => { + self.state = Nothing; + TouchAction::NoAction + }, + Panning { velocity } if velocity.length().abs() >= FLING_MIN_SCREEN_PX => { + // 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:?}." + ); + debug_assert!((point.x as i64) < (i32::MAX as i64)); + debug_assert!((point.y as i64) < (i32::MAX as i64)); + let cursor = DeviceIntPoint::new(point.x as i32, point.y as i32); + // 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); + self.state = Flinging { velocity, cursor }; + TouchAction::NoAction + }, + Panning { .. } => { self.state = Nothing; TouchAction::NoAction }, Pinching => { - self.state = Panning; + self.state = Panning { + velocity: Vector2D::new(0.0, 0.0), + }; TouchAction::NoAction }, + Flinging { .. } => { + unreachable!("On touchup received, but already flinging.") + }, WaitingForScript | DefaultPrevented | MultiTouch => { if self.active_touch_points.is_empty() { self.state = Nothing; @@ -184,11 +258,13 @@ impl TouchHandler { } match self.state { Nothing => {}, - Touching | Panning => { + Touching | Panning { .. } | Flinging { .. } => { self.state = Nothing; }, Pinching => { - self.state = Panning; + self.state = Panning { + velocity: Vector2D::new(0.0, 0.0), + }; }, WaitingForScript | DefaultPrevented | MultiTouch => { if self.active_touch_points.is_empty() { diff --git a/components/compositing/windowing.rs b/components/compositing/windowing.rs index 31c4380f8ba..7f56d29dc62 100644 --- a/components/compositing/windowing.rs +++ b/components/compositing/windowing.rs @@ -128,6 +128,8 @@ pub enum EmbedderEvent { ReplaceNativeSurface(*mut c_void, DeviceIntSize), /// Sent when new Gamepad information is available. Gamepad(GamepadEvent), + /// Vertical Synchronization tick + Vsync, } impl Debug for EmbedderEvent { @@ -187,6 +189,7 @@ impl Debug for EmbedderEvent { EmbedderEvent::InvalidateNativeSurface => write!(f, "InvalidateNativeSurface"), EmbedderEvent::ReplaceNativeSurface(..) => write!(f, "ReplaceNativeSurface"), EmbedderEvent::Gamepad(..) => write!(f, "Gamepad"), + EmbedderEvent::Vsync => write!(f, "Vsync"), } } } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 5a039024b6c..df2cebed998 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -863,6 +863,9 @@ where warn!("Sending Gamepad event to constellation failed ({:?}).", e); } }, + EmbedderEvent::Vsync => { + self.compositor.on_vsync(); + }, } false } diff --git a/ports/servoshell/desktop/tracing.rs b/ports/servoshell/desktop/tracing.rs index 4fa6cf53d3f..5feabb7afe2 100644 --- a/ports/servoshell/desktop/tracing.rs +++ b/ports/servoshell/desktop/tracing.rs @@ -233,6 +233,7 @@ mod to_servo { Self::InvalidateNativeSurface => target!("InvalidateNativeSurface"), Self::ReplaceNativeSurface(..) => target!("ReplaceNativeSurface"), Self::Gamepad(..) => target!("Gamepad"), + Self::Vsync => target!("Vsync"), } } } diff --git a/ports/servoshell/egl/ohos.rs b/ports/servoshell/egl/ohos.rs index cf0cab2b45d..f5e76f0cf49 100644 --- a/ports/servoshell/egl/ohos.rs +++ b/ports/servoshell/egl/ohos.rs @@ -21,6 +21,7 @@ use ohos_sys::xcomponent::{ OH_NativeXComponent_RegisterCallback, OH_NativeXComponent_TouchEvent, OH_NativeXComponent_TouchEventType, }; +use servo::compositing::windowing::EmbedderEvent; use servo::embedder_traits::PromptResult; use servo::euclid::Point2D; use servo::style::Zero; @@ -146,12 +147,10 @@ impl ServoAction { panic!("Received Initialize event, even though servo is already initialized") }, - Vsync => { - servo.perform_updates().expect("Infallible"); - servo.present_if_needed(); - // Todo: perform_updates() (before or after present) if animating? - Ok(()) - }, + Vsync => servo + .process_event(EmbedderEvent::Vsync) + .and_then(|()| servo.perform_updates()) + .and_then(|()| Ok(servo.present_if_needed())), }; if let Err(e) = res { error!("Failed to do {self:?} with error {e}"); @@ -315,6 +314,7 @@ fn initialize_logging_once() { // Show GL errors by default. "canvas::webgl_thread", "compositing::compositor", + "compositing::touch", "constellation::constellation", ]; for &module in &filters {