diff options
Diffstat (limited to 'components/compositing/webview_renderer.rs')
-rw-r--r-- | components/compositing/webview_renderer.rs | 982 |
1 files changed, 982 insertions, 0 deletions
diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs new file mode 100644 index 00000000000..6ad77d46043 --- /dev/null +++ b/components/compositing/webview_renderer.rs @@ -0,0 +1,982 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::RefCell; +use std::collections::HashMap; +use std::collections::hash_map::Keys; +use std::rc::Rc; + +use base::id::{PipelineId, WebViewId}; +use compositing_traits::{SendableFrameTree, WebViewTrait}; +use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType}; +use embedder_traits::{ + AnimationState, CompositorHitTestResult, InputEvent, MouseButton, MouseButtonAction, + MouseButtonEvent, MouseMoveEvent, ShutdownState, TouchEvent, TouchEventResult, TouchEventType, + TouchId, ViewportDetails, +}; +use euclid::{Box2D, Point2D, Scale, Size2D, Vector2D}; +use fnv::FnvHashSet; +use log::{debug, warn}; +use servo_geometry::DeviceIndependentPixel; +use style_traits::{CSSPixel, PinchZoomFactor}; +use webrender::Transaction; +use webrender_api::units::{ + DeviceIntPoint, DeviceIntRect, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D, +}; +use webrender_api::{ + ExternalScrollId, HitTestFlags, RenderReasons, SampledScrollOffset, ScrollLocation, +}; + +use crate::IOCompositor; +use crate::compositor::{PipelineDetails, ServoRenderer}; +use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; + +// Default viewport constraints +const MAX_ZOOM: f32 = 8.0; +const MIN_ZOOM: f32 = 0.1; + +#[derive(Clone, Copy)] +struct ScrollEvent { + /// Scroll by this offset, or to Start or End + scroll_location: ScrollLocation, + /// Apply changes to the frame at this location + cursor: DeviceIntPoint, + /// The number of OS events that have been coalesced together into this one event. + event_count: u32, +} + +#[derive(Clone, Copy)] +enum ScrollZoomEvent { + /// An pinch zoom event that magnifies the view by the given factor. + PinchZoom(f32), + /// A scroll event that scrolls the scroll node at the given location by the + /// given amount. + Scroll(ScrollEvent), +} + +/// A renderer for a libservo `WebView`. This is essentially the [`ServoRenderer`]'s interface to a +/// libservo `WebView`, but the code here cannot depend on libservo in order to prevent circular +/// dependencies, which is why we store a `dyn WebViewTrait` here instead of the `WebView` itself. +pub(crate) struct WebViewRenderer { + /// The [`WebViewId`] of the `WebView` associated with this [`WebViewDetails`]. + pub id: WebViewId, + /// The renderer's view of the embedding layer `WebView` as a trait implementation, + /// so that the renderer doesn't need to depend on the embedding layer. This avoids + /// a dependency cycle. + pub webview: Box<dyn WebViewTrait>, + /// The root [`PipelineId`] of the currently displayed page in this WebView. + pub root_pipeline_id: Option<PipelineId>, + pub rect: DeviceRect, + /// Tracks details about each active pipeline that the compositor knows about. + pub pipelines: HashMap<PipelineId, PipelineDetails>, + /// Data that is shared by all WebView renderers. + pub(crate) global: Rc<RefCell<ServoRenderer>>, + /// Pending scroll/zoom events. + pending_scroll_zoom_events: Vec<ScrollZoomEvent>, + /// Touch input state machine + touch_handler: TouchHandler, + /// "Desktop-style" zoom that resizes the viewport to fit the window. + pub page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>, + /// "Mobile-style" zoom that does not reflow the page. + viewport_zoom: PinchZoomFactor, + /// Viewport zoom constraints provided by @viewport. + min_viewport_zoom: Option<PinchZoomFactor>, + max_viewport_zoom: Option<PinchZoomFactor>, + /// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled + /// by the embedding layer. + hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>, +} + +impl Drop for WebViewRenderer { + fn drop(&mut self) { + self.global + .borrow_mut() + .pipeline_to_webview_map + .retain(|_, webview_id| self.id != *webview_id); + } +} + +impl WebViewRenderer { + pub(crate) fn new( + global: Rc<RefCell<ServoRenderer>>, + renderer_webview: Box<dyn WebViewTrait>, + viewport_details: ViewportDetails, + ) -> Self { + let hidpi_scale_factor = viewport_details.hidpi_scale_factor; + let size = viewport_details.size * viewport_details.hidpi_scale_factor; + Self { + id: renderer_webview.id(), + webview: renderer_webview, + root_pipeline_id: None, + rect: DeviceRect::from_origin_and_size(DevicePoint::origin(), size), + pipelines: Default::default(), + touch_handler: TouchHandler::new(), + global, + pending_scroll_zoom_events: Default::default(), + page_zoom: Scale::new(1.0), + viewport_zoom: PinchZoomFactor::new(1.0), + min_viewport_zoom: Some(PinchZoomFactor::new(1.0)), + max_viewport_zoom: None, + hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), + } + } + + pub(crate) fn animations_or_animation_callbacks_running(&self) -> bool { + self.pipelines + .values() + .any(PipelineDetails::animations_or_animation_callbacks_running) + } + + pub(crate) fn animation_callbacks_running(&self) -> bool { + self.pipelines + .values() + .any(PipelineDetails::animation_callbacks_running) + } + + pub(crate) fn pipeline_ids(&self) -> Keys<'_, PipelineId, PipelineDetails> { + self.pipelines.keys() + } + + /// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed. + pub(crate) fn ensure_pipeline_details( + &mut self, + pipeline_id: PipelineId, + ) -> &mut PipelineDetails { + self.pipelines.entry(pipeline_id).or_insert_with(|| { + self.global + .borrow_mut() + .pipeline_to_webview_map + .insert(pipeline_id, self.id); + PipelineDetails::new(pipeline_id) + }) + } + + pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) { + self.ensure_pipeline_details(pipeline_id).throttled = throttled; + } + + pub(crate) fn remove_pipeline(&mut self, pipeline_id: PipelineId) { + self.global + .borrow_mut() + .pipeline_to_webview_map + .remove(&pipeline_id); + self.pipelines.remove(&pipeline_id); + } + + pub(crate) fn set_frame_tree(&mut self, frame_tree: &SendableFrameTree) { + let pipeline_id = frame_tree.pipeline.id; + let old_pipeline_id = std::mem::replace(&mut self.root_pipeline_id, Some(pipeline_id)); + + if old_pipeline_id != self.root_pipeline_id { + debug!( + "Updating webview ({:?}) from pipeline {:?} to {:?}", + 3, old_pipeline_id, self.root_pipeline_id + ); + } + + self.set_frame_tree_on_pipeline_details(frame_tree, None); + self.reset_scroll_tree_for_unattached_pipelines(frame_tree); + self.send_scroll_positions_to_layout_for_pipeline(pipeline_id); + } + + pub(crate) fn send_scroll_positions_to_layout_for_pipeline(&self, pipeline_id: PipelineId) { + let Some(details) = self.pipelines.get(&pipeline_id) else { + return; + }; + + let mut scroll_states = Vec::new(); + details.scroll_tree.nodes.iter().for_each(|node| { + if let (Some(scroll_id), Some(scroll_offset)) = (node.external_id(), node.offset()) { + scroll_states.push(ScrollState { + scroll_id, + scroll_offset, + }); + } + }); + + let _ = self.global.borrow().constellation_sender.send( + EmbedderToConstellationMessage::SetScrollStates(pipeline_id, scroll_states), + ); + } + + pub(crate) fn set_frame_tree_on_pipeline_details( + &mut self, + frame_tree: &SendableFrameTree, + parent_pipeline_id: Option<PipelineId>, + ) { + let pipeline_id = frame_tree.pipeline.id; + let pipeline_details = self.ensure_pipeline_details(pipeline_id); + pipeline_details.pipeline = Some(frame_tree.pipeline.clone()); + pipeline_details.parent_pipeline_id = parent_pipeline_id; + + for kid in &frame_tree.children { + self.set_frame_tree_on_pipeline_details(kid, Some(pipeline_id)); + } + } + + pub(crate) fn reset_scroll_tree_for_unattached_pipelines( + &mut self, + frame_tree: &SendableFrameTree, + ) { + // TODO(mrobinson): Eventually this can selectively preserve the scroll trees + // state for some unattached pipelines in order to preserve scroll position when + // navigating backward and forward. + fn collect_pipelines( + pipelines: &mut FnvHashSet<PipelineId>, + frame_tree: &SendableFrameTree, + ) { + pipelines.insert(frame_tree.pipeline.id); + for kid in &frame_tree.children { + collect_pipelines(pipelines, kid); + } + } + + let mut attached_pipelines: FnvHashSet<PipelineId> = FnvHashSet::default(); + collect_pipelines(&mut attached_pipelines, frame_tree); + + self.pipelines + .iter_mut() + .filter(|(id, _)| !attached_pipelines.contains(id)) + .for_each(|(_, details)| { + details.scroll_tree.nodes.iter_mut().for_each(|node| { + node.set_offset(LayoutVector2D::zero()); + }) + }) + } + + /// Sets or unsets the animations-running flag for the given pipeline. Returns true if + /// the pipeline is throttled. + pub(crate) fn change_running_animations_state( + &mut self, + pipeline_id: PipelineId, + animation_state: AnimationState, + ) -> bool { + let throttled = { + let pipeline_details = self.ensure_pipeline_details(pipeline_id); + match animation_state { + AnimationState::AnimationsPresent => { + pipeline_details.animations_running = true; + }, + AnimationState::AnimationCallbacksPresent => { + pipeline_details.animation_callbacks_running = true; + }, + AnimationState::NoAnimationsPresent => { + pipeline_details.animations_running = false; + }, + AnimationState::NoAnimationCallbacksPresent => { + pipeline_details.animation_callbacks_running = false; + }, + } + pipeline_details.throttled + }; + + let animating = self.pipelines.values().any(PipelineDetails::animating); + self.webview.set_animating(animating); + throttled + } + + pub(crate) fn tick_all_animations(&self, compositor: &IOCompositor) { + for pipeline_details in self.pipelines.values() { + pipeline_details.tick_animations(compositor) + } + } + + pub(crate) fn tick_animations_for_pipeline( + &self, + pipeline_id: PipelineId, + compositor: &IOCompositor, + ) { + if let Some(pipeline_details) = self.pipelines.get(&pipeline_id) { + pipeline_details.tick_animations(compositor); + } + } + + /// On a Window refresh tick (e.g. vsync) + pub(crate) 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, + ); + } + } + + pub(crate) fn dispatch_input_event(&mut self, event: InputEvent) { + // Events that do not need to do hit testing are sent directly to the + // constellation to filter down. + let Some(point) = event.point() else { + return; + }; + + // If we can't find a pipeline to send this event to, we cannot continue. + let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); + let Some(result) = self + .global + .borrow() + .hit_test_at_point(point, get_pipeline_details) + else { + return; + }; + + self.global.borrow_mut().update_cursor(point, &result); + + if let Err(error) = self.global.borrow().constellation_sender.send( + EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), + ) { + warn!("Sending event to constellation failed ({error:?})."); + } + } + + pub(crate) fn notify_input_event(&mut self, event: InputEvent) { + if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { + return; + } + + if let InputEvent::Touch(event) = event { + self.on_touch_event(event); + return; + } + + if self.global.borrow().convert_mouse_to_touch { + match event { + InputEvent::MouseButton(event) => { + match (event.button, event.action) { + (MouseButton::Left, MouseButtonAction::Down) => self.on_touch_down( + TouchEvent::new(TouchEventType::Down, TouchId(0), event.point), + ), + (MouseButton::Left, MouseButtonAction::Up) => self.on_touch_up( + TouchEvent::new(TouchEventType::Up, TouchId(0), event.point), + ), + _ => {}, + } + return; + }, + InputEvent::MouseMove(event) => { + if let Some(state) = self.touch_handler.try_get_current_touch_sequence() { + // We assume that the debug option `-Z convert-mouse-to-touch` will only + // be used on devices without native touch input, so we can directly + // reuse the touch handler for tracking the state of pressed buttons. + match state.state { + TouchSequenceState::Touching | TouchSequenceState::Panning { .. } => { + self.on_touch_move(TouchEvent::new( + TouchEventType::Move, + TouchId(0), + event.point, + )); + }, + TouchSequenceState::MultiTouch => { + // Multitouch simulation currently is not implemented. + // Since we only get one mouse move event, we would need to + // dispatch one mouse move event per currently pressed mouse button. + }, + TouchSequenceState::Pinching => { + // We only have one mouse button, so Pinching should be impossible. + #[cfg(debug_assertions)] + log::error!( + "Touch handler is in Pinching state, which should be unreachable with \ + -Z convert-mouse-to-touch debug option." + ); + }, + TouchSequenceState::PendingFling { .. } | + TouchSequenceState::Flinging { .. } | + TouchSequenceState::PendingClick(_) | + TouchSequenceState::Finished => { + // Mouse movement without a button being pressed is not + // translated to touch events. + }, + } + } + // We don't want to (directly) dispatch mouse events when simulating touch input. + return; + }, + _ => {}, + } + } + + self.dispatch_input_event(event); + } + + fn send_touch_event(&self, mut event: TouchEvent) -> bool { + let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); + let Some(result) = self + .global + .borrow() + .hit_test_at_point(event.point, get_pipeline_details) + else { + return false; + }; + + event.init_sequence_id(self.touch_handler.current_sequence_id); + let event = InputEvent::Touch(event); + if let Err(e) = self.global.borrow().constellation_sender.send( + EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), + ) { + warn!("Sending event to constellation failed ({:?}).", e); + false + } else { + true + } + } + + pub(crate) fn on_touch_event(&mut self, event: TouchEvent) { + if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { + return; + } + + match event.event_type { + TouchEventType::Down => self.on_touch_down(event), + TouchEventType::Move => self.on_touch_move(event), + TouchEventType::Up => self.on_touch_up(event), + TouchEventType::Cancel => self.on_touch_cancel(event), + } + } + + fn on_touch_down(&mut self, event: TouchEvent) { + self.touch_handler.on_touch_down(event.id, event.point); + self.send_touch_event(event); + } + + fn on_touch_move(&mut self, mut event: TouchEvent) { + let action: TouchMoveAction = self.touch_handler.on_touch_move(event.id, event.point); + if TouchMoveAction::NoAction != action { + // if first move processed and allowed, we directly process the move event, + // without waiting for the script handler. + if self + .touch_handler + .move_allowed(self.touch_handler.current_sequence_id) + { + // https://w3c.github.io/touch-events/#cancelability + event.disable_cancelable(); + match action { + TouchMoveAction::Scroll(delta, point) => self.on_scroll_window_event( + ScrollLocation::Delta(LayoutVector2D::from_untyped(delta.to_untyped())), + point.cast(), + ), + TouchMoveAction::Zoom(magnification, scroll_delta) => { + let cursor = Point2D::new(-1, -1); // Make sure this hits the base layer. + + // The order of these events doesn't matter, because zoom is handled by + // a root display list and the scroll event here is handled by the scroll + // applied to the content display list. + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::PinchZoom(magnification)); + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::Scroll(ScrollEvent { + scroll_location: ScrollLocation::Delta( + LayoutVector2D::from_untyped(scroll_delta.to_untyped()), + ), + cursor, + event_count: 1, + })); + }, + _ => {}, + } + } + // 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(self.touch_handler.current_sequence_id) && + self.send_touch_event(event) && + event.is_cancelable() + { + self.touch_handler + .set_handling_touch_move(self.touch_handler.current_sequence_id, true); + } + } + } + + fn on_touch_up(&mut self, event: TouchEvent) { + self.touch_handler.on_touch_up(event.id, event.point); + self.send_touch_event(event); + } + + fn on_touch_cancel(&mut self, event: TouchEvent) { + // Send the event to script. + self.touch_handler.on_touch_cancel(event.id, event.point); + self.send_touch_event(event); + } + + pub(crate) fn on_touch_event_processed(&mut self, result: TouchEventResult) { + match result { + TouchEventResult::DefaultPrevented(sequence_id, event_type) => { + debug!( + "Touch event {:?} in sequence {:?} prevented!", + event_type, sequence_id + ); + match event_type { + TouchEventType::Down => { + // prevents both click and move + self.touch_handler.prevent_click(sequence_id); + self.touch_handler.prevent_move(sequence_id); + self.touch_handler + .remove_pending_touch_move_action(sequence_id); + }, + TouchEventType::Move => { + // script thread processed the touch move event, mark this false. + if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) { + info.prevent_move = TouchMoveAllowed::Prevented; + if let TouchSequenceState::PendingFling { .. } = info.state { + info.state = TouchSequenceState::Finished; + } + self.touch_handler.set_handling_touch_move( + self.touch_handler.current_sequence_id, + false, + ); + self.touch_handler + .remove_pending_touch_move_action(sequence_id); + } + }, + TouchEventType::Up => { + // Note: We don't have to consider PendingFling here, since we handle that + // in the DefaultAllowed case of the touch_move event. + // Note: Removing can and should fail, if we still have an active Fling, + let Some(info) = + &mut self.touch_handler.get_touch_sequence_mut(sequence_id) + else { + // The sequence ID could already be removed, e.g. if Fling finished, + // before the touch_up event was handled (since fling can start + // immediately if move was previously allowed, and clicks are anyway not + // happening from fling). + return; + }; + match info.state { + TouchSequenceState::PendingClick(_) => { + info.state = TouchSequenceState::Finished; + self.touch_handler.remove_touch_sequence(sequence_id); + }, + TouchSequenceState::Flinging { .. } => { + // We can't remove the touch sequence yet + }, + TouchSequenceState::Finished => { + self.touch_handler.remove_touch_sequence(sequence_id); + }, + TouchSequenceState::Touching | + TouchSequenceState::Panning { .. } | + TouchSequenceState::Pinching | + TouchSequenceState::MultiTouch | + TouchSequenceState::PendingFling { .. } => { + // It's possible to transition from Pinch to pan, Which means that + // a touch_up event for a pinch might have arrived here, but we + // already transitioned to pan or even PendingFling. + // We don't need to do anything in these cases though. + }, + } + }, + TouchEventType::Cancel => { + // We could still have pending event handlers, so we remove the pending + // actions, and try to remove the touch sequence. + self.touch_handler + .remove_pending_touch_move_action(sequence_id); + self.touch_handler.try_remove_touch_sequence(sequence_id); + }, + } + }, + TouchEventResult::DefaultAllowed(sequence_id, event_type) => { + debug!( + "Touch event {:?} in sequence {:?} allowed", + event_type, sequence_id + ); + match event_type { + TouchEventType::Down => {}, + TouchEventType::Move => { + if let Some(action) = + self.touch_handler.pending_touch_move_action(sequence_id) + { + match action { + TouchMoveAction::Scroll(delta, point) => self + .on_scroll_window_event( + ScrollLocation::Delta(LayoutVector2D::from_untyped( + delta.to_untyped(), + )), + point.cast(), + ), + TouchMoveAction::Zoom(magnification, scroll_delta) => { + let cursor = Point2D::new(-1, -1); + // Make sure this hits the base layer. + // The order of these events doesn't matter, because zoom is handled by + // a root display list and the scroll event here is handled by the scroll + // applied to the content display list. + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::PinchZoom(magnification)); + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::Scroll(ScrollEvent { + scroll_location: ScrollLocation::Delta( + LayoutVector2D::from_untyped( + scroll_delta.to_untyped(), + ), + ), + cursor, + event_count: 1, + })); + }, + TouchMoveAction::NoAction => { + // This shouldn't happen, but we can also just ignore it. + }, + } + self.touch_handler + .remove_pending_touch_move_action(sequence_id); + } + self.touch_handler + .set_handling_touch_move(self.touch_handler.current_sequence_id, false); + if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) { + if info.prevent_move == TouchMoveAllowed::Pending { + info.prevent_move = TouchMoveAllowed::Allowed; + if let TouchSequenceState::PendingFling { velocity, cursor } = + info.state + { + info.state = TouchSequenceState::Flinging { velocity, cursor } + } + } + } + }, + TouchEventType::Up => { + let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) + else { + // The sequence was already removed because there is no default action. + return; + }; + match info.state { + TouchSequenceState::PendingClick(point) => { + info.state = TouchSequenceState::Finished; + // PreventDefault from touch_down may have been processed after + // touch_up already occurred. + if !info.prevent_click { + self.simulate_mouse_click(point); + } + self.touch_handler.remove_touch_sequence(sequence_id); + }, + TouchSequenceState::Flinging { .. } => { + // We can't remove the touch sequence yet + }, + TouchSequenceState::Finished => { + self.touch_handler.remove_touch_sequence(sequence_id); + }, + TouchSequenceState::Panning { .. } | + TouchSequenceState::Pinching | + TouchSequenceState::PendingFling { .. } => { + // It's possible to transition from Pinch to pan, Which means that + // a touch_up event for a pinch might have arrived here, but we + // already transitioned to pan or even PendingFling. + // We don't need to do anything in these cases though. + }, + TouchSequenceState::MultiTouch | TouchSequenceState::Touching => { + // We transitioned to touching from multi-touch or pinching. + }, + } + }, + TouchEventType::Cancel => { + self.touch_handler + .remove_pending_touch_move_action(sequence_id); + self.touch_handler.try_remove_touch_sequence(sequence_id); + }, + } + }, + } + } + + /// <http://w3c.github.io/touch-events/#mouse-events> + fn simulate_mouse_click(&mut self, point: DevicePoint) { + let button = MouseButton::Left; + self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point })); + self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent { + button, + action: MouseButtonAction::Down, + point, + })); + self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent { + button, + action: MouseButtonAction::Up, + point, + })); + self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent { + button, + action: MouseButtonAction::Click, + point, + })); + } + + pub(crate) fn notify_scroll_event( + &mut self, + scroll_location: ScrollLocation, + cursor: DeviceIntPoint, + event_type: TouchEventType, + ) { + if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { + return; + } + + match event_type { + TouchEventType::Move => self.on_scroll_window_event(scroll_location, cursor), + TouchEventType::Up | TouchEventType::Cancel => { + self.on_scroll_window_event(scroll_location, cursor); + }, + TouchEventType::Down => { + self.on_scroll_window_event(scroll_location, cursor); + }, + } + } + + fn on_scroll_window_event(&mut self, scroll_location: ScrollLocation, cursor: DeviceIntPoint) { + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::Scroll(ScrollEvent { + scroll_location, + cursor, + event_count: 1, + })); + } + + pub(crate) fn process_pending_scroll_events(&mut self, compositor: &mut IOCompositor) { + if self.pending_scroll_zoom_events.is_empty() { + return; + } + + // Batch up all scroll events into one, or else we'll do way too much painting. + let mut combined_scroll_event: Option<ScrollEvent> = None; + let mut combined_magnification = 1.0; + for scroll_event in self.pending_scroll_zoom_events.drain(..) { + match scroll_event { + ScrollZoomEvent::PinchZoom(magnification) => { + combined_magnification *= magnification + }, + ScrollZoomEvent::Scroll(scroll_event_info) => { + let combined_event = match combined_scroll_event.as_mut() { + None => { + combined_scroll_event = Some(scroll_event_info); + continue; + }, + Some(combined_event) => combined_event, + }; + + match ( + combined_event.scroll_location, + scroll_event_info.scroll_location, + ) { + (ScrollLocation::Delta(old_delta), ScrollLocation::Delta(new_delta)) => { + // Mac OS X sometimes delivers scroll events out of vsync during a + // fling. This causes events to get bunched up occasionally, causing + // nasty-looking "pops". To mitigate this, during a fling we average + // deltas instead of summing them. + let old_event_count = Scale::new(combined_event.event_count as f32); + combined_event.event_count += 1; + let new_event_count = Scale::new(combined_event.event_count as f32); + combined_event.scroll_location = ScrollLocation::Delta( + (old_delta * old_event_count + new_delta) / new_event_count, + ); + }, + (ScrollLocation::Start, _) | (ScrollLocation::End, _) => { + // Once we see Start or End, we shouldn't process any more events. + break; + }, + (_, ScrollLocation::Start) | (_, ScrollLocation::End) => { + // If this is an event which is scrolling to the start or end of the page, + // disregard other pending events and exit the loop. + *combined_event = scroll_event_info; + break; + }, + } + }, + } + } + + let zoom_changed = + self.set_pinch_zoom_level(self.pinch_zoom_level().get() * combined_magnification); + let scroll_result = combined_scroll_event.and_then(|combined_event| { + self.scroll_node_at_device_point( + combined_event.cursor.to_f32(), + combined_event.scroll_location, + ) + }); + if !zoom_changed && scroll_result.is_none() { + return; + } + + let mut transaction = Transaction::new(); + if zoom_changed { + compositor.send_root_pipeline_display_list_in_transaction(&mut transaction); + } + + if let Some((pipeline_id, external_id, offset)) = scroll_result { + let offset = LayoutVector2D::new(-offset.x, -offset.y); + transaction.set_scroll_offsets( + external_id, + vec![SampledScrollOffset { + offset, + generation: 0, + }], + ); + self.send_scroll_positions_to_layout_for_pipeline(pipeline_id); + } + + compositor.generate_frame(&mut transaction, RenderReasons::APZ); + self.global.borrow_mut().send_transaction(transaction); + } + + /// Perform a hit test at the given [`DevicePoint`] and apply the [`ScrollLocation`] + /// scrolling to the applicable scroll node under that point. If a scroll was + /// performed, returns the [`PipelineId`] of the node scrolled, the id, and the final + /// scroll delta. + fn scroll_node_at_device_point( + &mut self, + cursor: DevicePoint, + scroll_location: ScrollLocation, + ) -> Option<(PipelineId, ExternalScrollId, LayoutVector2D)> { + let scroll_location = match scroll_location { + ScrollLocation::Delta(delta) => { + let device_pixels_per_page = self.device_pixels_per_page_pixel(); + let scaled_delta = (Vector2D::from_untyped(delta.to_untyped()) / + device_pixels_per_page) + .to_untyped(); + let calculated_delta = LayoutVector2D::from_untyped(scaled_delta); + ScrollLocation::Delta(calculated_delta) + }, + // Leave ScrollLocation unchanged if it is Start or End location. + ScrollLocation::Start | ScrollLocation::End => scroll_location, + }; + + let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); + let hit_test_results = self + .global + .borrow() + .hit_test_at_point_with_flags_and_pipeline( + cursor, + HitTestFlags::FIND_ALL, + None, + get_pipeline_details, + ); + + // Iterate through all hit test results, processing only the first node of each pipeline. + // This is needed to propagate the scroll events from a pipeline representing an iframe to + // its ancestor pipelines. + let mut previous_pipeline_id = None; + for CompositorHitTestResult { + pipeline_id, + scroll_tree_node, + .. + } in hit_test_results.iter() + { + let pipeline_details = self.pipelines.get_mut(pipeline_id)?; + if previous_pipeline_id.replace(pipeline_id) != Some(pipeline_id) { + let scroll_result = pipeline_details + .scroll_tree + .scroll_node_or_ancestor(scroll_tree_node, scroll_location); + if let Some((external_id, offset)) = scroll_result { + return Some((*pipeline_id, external_id, offset)); + } + } + } + None + } + + pub(crate) fn pinch_zoom_level(&self) -> Scale<f32, DevicePixel, DevicePixel> { + Scale::new(self.viewport_zoom.get()) + } + + fn set_pinch_zoom_level(&mut self, mut zoom: f32) -> bool { + if let Some(min) = self.min_viewport_zoom { + zoom = f32::max(min.get(), zoom); + } + if let Some(max) = self.max_viewport_zoom { + zoom = f32::min(max.get(), zoom); + } + + let old_zoom = std::mem::replace(&mut self.viewport_zoom, PinchZoomFactor::new(zoom)); + old_zoom != self.viewport_zoom + } + + pub(crate) fn set_page_zoom(&mut self, magnification: f32) { + self.page_zoom = + Scale::new((self.page_zoom.get() * magnification).clamp(MIN_ZOOM, MAX_ZOOM)); + } + + pub(crate) fn device_pixels_per_page_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> { + self.page_zoom * self.hidpi_scale_factor * self.pinch_zoom_level() + } + + pub(crate) fn device_pixels_per_page_pixel_not_including_pinch_zoom( + &self, + ) -> Scale<f32, CSSPixel, DevicePixel> { + self.page_zoom * self.hidpi_scale_factor + } + + /// Simulate a pinch zoom + pub(crate) fn set_pinch_zoom(&mut self, magnification: f32) { + if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { + return; + } + + // TODO: Scroll to keep the center in view? + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::PinchZoom(magnification)); + } + + fn send_window_size_message(&self) { + // The device pixel ratio used by the style system should include the scale from page pixels + // to device pixels, but not including any pinch zoom. + let device_pixel_ratio = self.device_pixels_per_page_pixel_not_including_pinch_zoom(); + let initial_viewport = self.rect.size().to_f32() / device_pixel_ratio; + let msg = EmbedderToConstellationMessage::ChangeViewportDetails( + self.id, + ViewportDetails { + hidpi_scale_factor: device_pixel_ratio, + size: initial_viewport, + }, + WindowSizeType::Resize, + ); + if let Err(e) = self.global.borrow().constellation_sender.send(msg) { + warn!("Sending window resize to constellation failed ({:?}).", e); + } + } + + /// Set the `hidpi_scale_factor` for this renderer, returning `true` if the value actually changed. + pub(crate) fn set_hidpi_scale_factor( + &mut self, + new_scale: Scale<f32, DeviceIndependentPixel, DevicePixel>, + ) -> bool { + let old_scale_factor = std::mem::replace(&mut self.hidpi_scale_factor, new_scale); + if self.hidpi_scale_factor == old_scale_factor { + return false; + } + + self.send_window_size_message(); + true + } + + /// Set the `rect` for this renderer, returning `true` if the value actually changed. + pub(crate) fn set_rect(&mut self, new_rect: DeviceRect) -> bool { + let old_rect = std::mem::replace(&mut self.rect, new_rect); + if old_rect.size() != self.rect.size() { + self.send_window_size_message(); + } + old_rect != self.rect + } + + pub(crate) fn client_window_rect( + &self, + rendering_context_size: Size2D<u32, DevicePixel>, + ) -> Box2D<i32, DeviceIndependentPixel> { + let screen_geometry = self.webview.screen_geometry().unwrap_or_default(); + let rect = DeviceIntRect::from_origin_and_size( + screen_geometry.offset, + rendering_context_size.to_i32(), + ) + .to_f32() / + self.hidpi_scale_factor; + rect.to_i32() + } + + pub(crate) fn screen_size(&self) -> Size2D<i32, DeviceIndependentPixel> { + let screen_geometry = self.webview.screen_geometry().unwrap_or_default(); + (screen_geometry.size.to_f32() / self.hidpi_scale_factor).to_i32() + } + + pub(crate) fn available_screen_size(&self) -> Size2D<i32, DeviceIndependentPixel> { + let screen_geometry = self.webview.screen_geometry().unwrap_or_default(); + (screen_geometry.available_size.to_f32() / self.hidpi_scale_factor).to_i32() + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct UnknownWebView(pub WebViewId); |