diff options
author | Delan Azabani <dazabani@igalia.com> | 2025-03-06 02:47:13 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-05 18:47:13 +0000 |
commit | 69e749947910480e97ffaf22031316ebe7f67b9c (patch) | |
tree | 27e3a28bf12adbae06221e9219b60aaa356e94db | |
parent | 16aeeaec85ec1eef985224fae8f38aa31f0eb22b (diff) | |
download | servo-69e749947910480e97ffaf22031316ebe7f67b9c.tar.gz servo-69e749947910480e97ffaf22031316ebe7f67b9c.zip |
compositor: Make input event handling per-WebView (#35716)
This is another step in the move to having a per-WebView renderer. In
this step event handling is made per-WebView. Most events sent to Servo
are sent via the WebView API already, so this just moves more event
handling code to the per-WebView render portion of the compositor.
- ServoRenderer is given shared ownership and interior mutability as
it is now shared among all WebView(Renderers).
- Some messages coming from other parts of Servo must now carry a
WebViewId as well so that they can be associated with a particular
WebView.
- There needs to be some reorganization of `ServoRenderer` in order to
avoid issues with double borrow of `RefCells`.
Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
-rw-r--r-- | components/compositing/compositor.rs | 1146 | ||||
-rw-r--r-- | components/compositing/webview.rs | 684 | ||||
-rw-r--r-- | components/constellation/constellation.rs | 28 | ||||
-rw-r--r-- | components/constellation/tracing.rs | 2 | ||||
-rw-r--r-- | components/servo/webview.rs | 23 | ||||
-rw-r--r-- | components/shared/compositing/constellation_msg.rs | 2 | ||||
-rw-r--r-- | components/shared/compositing/lib.rs | 6 | ||||
-rw-r--r-- | components/shared/embedder/webdriver.rs | 4 | ||||
-rw-r--r-- | components/webdriver_server/actions.rs | 8 |
9 files changed, 1016 insertions, 887 deletions
diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 8bbbe76916f..bb4bb416edd 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -8,6 +8,7 @@ use std::env; use std::fs::{File, create_dir_all}; use std::io::Write; use std::iter::once; +use std::mem::take; use std::rc::Rc; use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -22,10 +23,9 @@ use compositing_traits::{ use crossbeam_channel::Sender; use dpi::PhysicalSize; use embedder_traits::{ - Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, - ShutdownState, TouchEvent, TouchEventType, TouchId, + Cursor, InputEvent, MouseButtonEvent, MouseMoveEvent, ShutdownState, TouchEventType, }; -use euclid::{Box2D, Point2D, Rect, Scale, Size2D, Transform3D, Vector2D}; +use euclid::{Box2D, Point2D, Rect, Scale, Size2D, Transform3D}; use fnv::FnvHashMap; use ipc_channel::ipc::{self, IpcSharedMemory}; use libc::c_void; @@ -34,8 +34,7 @@ use pixels::{CorsStatus, Image, PixelFormat}; use profile_traits::time::{self as profile_time, ProfilerCategory}; use profile_traits::time_profile; use script_traits::{ - AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, TouchEventResult, - WindowSizeData, WindowSizeType, + AnimationState, AnimationTickType, ScriptThreadMessage, WindowSizeData, WindowSizeType, }; use servo_config::opts; use servo_geometry::DeviceIndependentPixel; @@ -59,7 +58,6 @@ use webrender_traits::{ }; use crate::InitialCompositorState; -use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; use crate::webview::{UnknownWebView, WebView, WebViewManager}; use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods}; @@ -89,13 +87,10 @@ enum ReadyState { } /// Data that is shared by all WebView renderers. pub struct ServoRenderer { - /// Our top-level browsing contexts. - webviews: WebViewManager<WebView>, - /// This is a temporary map between [`PipelineId`]s and their associated [`WebViewId`]. Once /// all renderer operations become per-`WebView` this map can be removed, but we still sometimes /// need to work backwards to figure out what `WebView` is associated with a `Pipeline`. - pipeline_to_webview_map: Rc<RefCell<HashMap<PipelineId, WebViewId>>>, + pub(crate) pipeline_to_webview_map: HashMap<PipelineId, WebViewId>, /// Tracks whether we are in the process of shutting down, or have shut down and should close /// the compositor. This is shared with the `Servo` instance. @@ -105,13 +100,16 @@ pub struct ServoRenderer { compositor_receiver: CompositorReceiver, /// The channel on which messages can be sent to the constellation. - constellation_sender: Sender<ConstellationMsg>, + pub(crate) constellation_sender: Sender<ConstellationMsg>, /// The channel on which messages can be sent to the time profiler. time_profiler_chan: profile_time::ProfilerChan, /// The WebRender [`RenderApi`] interface used to communicate with WebRender. - webrender_api: RenderApi, + pub(crate) webrender_api: RenderApi, + + /// The active webrender document. + pub(crate) webrender_document: DocumentId, /// The GL bindings for webrender webrender_gl: Rc<dyn gleam::gl::Gl>, @@ -123,12 +121,21 @@ pub struct ServoRenderer { #[cfg(feature = "webxr")] /// Some XR devices want to run on the main thread. webxr_main_thread: webxr::MainThreadRegistry, + + /// True to translate mouse input into touch events. + pub(crate) convert_mouse_to_touch: bool, + + /// Current mouse cursor. + cursor: Cursor, } /// NB: Never block on the constellation, because sometimes the constellation blocks on us. pub struct IOCompositor { /// Data that is shared by all WebView renderers. - global: ServoRenderer, + global: Rc<RefCell<ServoRenderer>>, + + /// Our top-level browsing contexts. + webviews: WebViewManager<WebView>, /// The application window. pub window: Rc<dyn WindowMethods>, @@ -152,12 +159,6 @@ pub struct IOCompositor { /// The time of the last zoom action has started. zoom_time: f64, - /// Touch input state machine - touch_handler: TouchHandler, - - /// Pending scroll/zoom events. - pending_scroll_zoom_events: Vec<ScrollZoomEvent>, - /// Used by the logic that determines when it is safe to output an /// image for the reftest framework. ready_to_save_state: ReadyState, @@ -165,24 +166,15 @@ pub struct IOCompositor { /// The webrender renderer. webrender: Option<webrender::Renderer>, - /// The active webrender document. - webrender_document: DocumentId, - /// The surfman instance that webrender targets rendering_context: Rc<dyn RenderingContext>, /// The coordinates of the native window, its view and the screen. embedder_coordinates: EmbedderCoordinates, - /// Current mouse cursor. - cursor: Cursor, - /// Current cursor position. cursor_pos: DevicePoint, - /// True to translate mouse input into touch events. - convert_mouse_to_touch: bool, - /// The number of frames pending to receive from WebRender. pending_frames: usize, @@ -191,25 +183,6 @@ pub struct IOCompositor { last_animation_tick: Instant, } -#[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), -} - /// Why we need to be repainted. This is used for debugging. #[derive(Clone, Copy, Default)] struct RepaintReason(u8); @@ -295,7 +268,7 @@ impl PipelineDetails { } let msg = ConstellationMsg::TickAnimation(self.id, tick_type); - if let Err(e) = compositor.global.constellation_sender.send(msg) { + if let Err(e) = compositor.global.borrow().constellation_sender.send(msg) { warn!("Sending tick to constellation failed ({:?}).", e); } true @@ -342,6 +315,98 @@ impl PipelineDetails { } } +impl ServoRenderer { + pub fn shutdown_state(&self) -> ShutdownState { + self.shutdown_state.get() + } + + pub(crate) fn hit_test_at_point<'a>( + &self, + point: DevicePoint, + details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, + ) -> Option<CompositorHitTestResult> { + self.hit_test_at_point_with_flags_and_pipeline( + point, + HitTestFlags::empty(), + None, + details_for_pipeline, + ) + .first() + .cloned() + } + + // TODO: split this into first half (global) and second half (one for whole compositor, one for webview) + pub(crate) fn hit_test_at_point_with_flags_and_pipeline<'a>( + &self, + point: DevicePoint, + flags: HitTestFlags, + pipeline_id: Option<WebRenderPipelineId>, + details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, + ) -> Vec<CompositorHitTestResult> { + // DevicePoint and WorldPoint are the same for us. + let world_point = WorldPoint::from_untyped(point.to_untyped()); + let results = + self.webrender_api + .hit_test(self.webrender_document, pipeline_id, world_point, flags); + + results + .items + .iter() + .filter_map(|item| { + let pipeline_id = item.pipeline.into(); + let details = details_for_pipeline(pipeline_id)?; + + // If the epoch in the tag does not match the current epoch of the pipeline, + // then the hit test is against an old version of the display list and we + // should ignore this hit test for now. + match details.most_recent_display_list_epoch { + Some(epoch) if epoch.as_u16() == item.tag.1 => {}, + _ => return None, + } + + let info = &details.hit_test_items[item.tag.0 as usize]; + Some(CompositorHitTestResult { + pipeline_id, + point_in_viewport: item.point_in_viewport.to_untyped(), + point_relative_to_item: item.point_relative_to_item.to_untyped(), + node: UntrustedNodeAddress(info.node as *const c_void), + cursor: info.cursor, + scroll_tree_node: info.scroll_tree_node, + }) + }) + .collect() + } + + pub(crate) fn send_transaction(&mut self, transaction: Transaction) { + self.webrender_api + .send_transaction(self.webrender_document, transaction); + } + + pub(crate) fn update_cursor(&mut self, result: &CompositorHitTestResult) { + let cursor = match result.cursor { + Some(cursor) if cursor != self.cursor => cursor, + _ => return, + }; + + let Some(webview_id) = self + .pipeline_to_webview_map + .get(&result.pipeline_id) + .cloned() + else { + warn!("Couldn't update cursor for non-WebView-associated pipeline"); + return; + }; + + self.cursor = cursor; + if let Err(e) = self + .constellation_sender + .send(ConstellationMsg::SetCursor(webview_id, cursor)) + { + warn!("Sending event to constellation failed ({:?}).", e); + } + } +} + impl IOCompositor { pub fn new( window: Rc<dyn WindowMethods>, @@ -350,24 +415,25 @@ impl IOCompositor { version_string: String, ) -> Self { let compositor = IOCompositor { - global: ServoRenderer { + global: Rc::new(RefCell::new(ServoRenderer { shutdown_state: state.shutdown_state, - webviews: WebViewManager::default(), pipeline_to_webview_map: Default::default(), compositor_receiver: state.receiver, constellation_sender: state.constellation_chan, time_profiler_chan: state.time_profiler_chan, webrender_api: state.webrender_api, + webrender_document: state.webrender_document, webrender_gl: state.webrender_gl, version_string, #[cfg(feature = "webxr")] webxr_main_thread: state.webxr_main_thread, - }, + convert_mouse_to_touch, + cursor: Cursor::None, + })), + webviews: WebViewManager::default(), embedder_coordinates: window.get_coordinates(), window, needs_repaint: Cell::default(), - touch_handler: TouchHandler::new(), - pending_scroll_zoom_events: Vec::new(), page_zoom: Scale::new(1.0), viewport_zoom: PinchZoomFactor::new(1.0), min_viewport_zoom: Some(PinchZoomFactor::new(1.0)), @@ -376,26 +442,21 @@ impl IOCompositor { zoom_time: 0f64, ready_to_save_state: ReadyState::Unknown, webrender: Some(state.webrender), - webrender_document: state.webrender_document, rendering_context: state.rendering_context, - cursor: Cursor::None, cursor_pos: DevicePoint::new(0.0, 0.0), - convert_mouse_to_touch, pending_frames: 0, last_animation_tick: Instant::now(), }; - let gl = &compositor.global.webrender_gl; - info!("Running on {}", gl.get_string(gleam::gl::RENDERER)); - info!("OpenGL Version {}", gl.get_string(gleam::gl::VERSION)); + { + let gl = &compositor.global.borrow().webrender_gl; + info!("Running on {}", gl.get_string(gleam::gl::RENDERER)); + info!("OpenGL Version {}", gl.get_string(gleam::gl::VERSION)); + } compositor.assert_gl_framebuffer_complete(); compositor } - pub fn shutdown_state(&self) -> ShutdownState { - self.global.shutdown_state.get() - } - pub fn deinit(&mut self) { if let Err(err) = self.rendering_context.make_current() { warn!("Failed to make the rendering context current: {:?}", err); @@ -415,38 +476,12 @@ impl IOCompositor { !self.needs_repaint.get().is_empty() } - fn update_cursor(&mut self, result: &CompositorHitTestResult) { - let cursor = match result.cursor { - Some(cursor) if cursor != self.cursor => cursor, - _ => return, - }; - - let Some(webview_id) = self - .global - .pipeline_to_webview_map - .borrow() - .get(&result.pipeline_id) - .cloned() - else { - warn!("Couldn't update cursor for non-WebView-associated pipeline"); - return; - }; - - self.cursor = cursor; - if let Err(e) = self - .global - .constellation_sender - .send(ConstellationMsg::SetCursor(webview_id, cursor)) - { - warn!("Sending event to constellation failed ({:?}).", e); - } - } - pub fn finish_shutting_down(&mut self) { // Drain compositor port, sometimes messages contain channels that are blocking // another thread from finishing (i.e. SetFrameTree). while self .global + .borrow_mut() .compositor_receiver .try_recv_compositor_msg() .is_some() @@ -455,6 +490,7 @@ impl IOCompositor { // Tell the profiler, memory profiler, and scrolling timer to shut down. if let Ok((sender, receiver)) = ipc::channel() { self.global + .borrow() .time_profiler_chan .send(profile_time::ProfilerMsg::Exit(sender)); let _ = receiver.recv(); @@ -483,7 +519,7 @@ impl IOCompositor { animation_state, ) => { let mut throttled = true; - if let Some(webview) = self.global.webviews.get_mut(webview_id) { + if let Some(webview) = self.webviews.get_mut(webview_id) { throttled = webview.change_running_animations_state(pipeline_id, animation_state); } @@ -496,7 +532,7 @@ impl IOCompositor { if !throttled && animation_state == AnimationState::AnimationCallbacksPresent { // We need to fetch the WebView again in order to avoid a double borrow. - if let Some(webview) = self.global.webviews.get(webview_id) { + if let Some(webview) = self.webviews.get(webview_id) { webview.tick_animations_for_pipeline(pipeline_id, self); } } @@ -504,15 +540,18 @@ impl IOCompositor { CompositorMsg::CreateOrUpdateWebView(frame_tree) => { self.set_frame_tree_for_webview(&frame_tree); - self.send_scroll_positions_to_layout_for_pipeline(frame_tree.pipeline.id); }, CompositorMsg::RemoveWebView(top_level_browsing_context_id) => { self.remove_webview(top_level_browsing_context_id); }, - CompositorMsg::TouchEventProcessed(result) => { - self.on_touch_event_processed(result); + CompositorMsg::TouchEventProcessed(webview_id, result) => { + let Some(webview) = self.webviews.get_mut(webview_id) else { + warn!("Handling input event for unknown webview: {webview_id}"); + return; + }; + webview.on_touch_event_processed(result); }, CompositorMsg::CreatePng(page_rect, reply) => { @@ -540,7 +579,7 @@ impl IOCompositor { }, CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => { - if let Some(webview) = self.global.webviews.get_mut(webview_id) { + if let Some(webview) = self.webviews.get_mut(webview_id) { webview.set_throttled(pipeline_id, throttled); self.process_animations(true); } @@ -551,7 +590,7 @@ impl IOCompositor { "Compositor got pipeline exited: {:?} {:?}", webview_id, pipeline_id ); - if let Some(webview) = self.global.webviews.get_mut(webview_id) { + if let Some(webview) = self.webviews.get_mut(webview_id) { webview.remove_pipeline(pipeline_id); } let _ = sender.send(()); @@ -561,8 +600,13 @@ impl IOCompositor { self.pending_frames -= 1; if recomposite_needed { - if let Some(result) = self.hit_test_at_point(self.cursor_pos) { - self.update_cursor(&result); + let details_for_pipeline = |pipeline_id| self.details_for_pipeline(pipeline_id); + let result = self + .global + .borrow() + .hit_test_at_point(self.cursor_pos, details_for_pipeline); + if let Some(result) = result { + self.global.borrow_mut().update_cursor(&result); } } @@ -577,24 +621,32 @@ impl IOCompositor { } }, - CompositorMsg::WebDriverMouseButtonEvent(action, button, x, y) => { + CompositorMsg::WebDriverMouseButtonEvent(webview_id, action, button, x, y) => { let dppx = self.device_pixels_per_page_pixel(); let point = dppx.transform_point(Point2D::new(x, y)); - self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent { + let Some(webview) = self.webviews.get_mut(webview_id) else { + warn!("Handling input event for unknown webview: {webview_id}"); + return; + }; + webview.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent { point, action, button, })); }, - CompositorMsg::WebDriverMouseMoveEvent(x, y) => { + CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y) => { let dppx = self.device_pixels_per_page_pixel(); let point = dppx.transform_point(Point2D::new(x, y)); - self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point })); + let Some(webview) = self.webviews.get_mut(webview_id) else { + warn!("Handling input event for unknown webview: {webview_id}"); + return; + }; + webview.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent { point })); }, CompositorMsg::PendingPaintMetric(webview_id, pipeline_id, epoch) => { - if let Some(webview) = self.global.webviews.get_mut(webview_id) { + if let Some(webview) = self.webviews.get_mut(webview_id) { webview.add_pending_paint_metric(pipeline_id, epoch); } }, @@ -617,9 +669,7 @@ impl IOCompositor { let mut txn = Transaction::new(); txn.set_display_list(WebRenderEpoch(0), (pipeline, Default::default())); self.generate_frame(&mut txn, RenderReasons::SCENE); - self.global - .webrender_api - .send_transaction(self.webrender_document, txn); + self.global.borrow_mut().send_transaction(txn); }, CrossProcessCompositorMessage::SendScrollNode( @@ -628,7 +678,7 @@ impl IOCompositor { point, external_scroll_id, ) => { - let Some(webview) = self.global.webviews.get_mut(webview_id) else { + let Some(webview) = self.webviews.get_mut(webview_id) else { return; }; @@ -658,9 +708,7 @@ impl IOCompositor { }], ); self.generate_frame(&mut txn, RenderReasons::APZ); - self.global - .webrender_api - .send_transaction(self.webrender_document, txn); + self.global.borrow_mut().send_transaction(txn); }, CrossProcessCompositorMessage::SendDisplayList { @@ -710,12 +758,12 @@ impl IOCompositor { ) .entered(); - let Some(webview) = self.global.webviews.get_mut(webview_id) else { + let Some(webview) = self.webviews.get_mut(webview_id) else { return warn!("Could not find WebView for incoming display list"); }; let pipeline_id = display_list_info.pipeline_id; - let details = webview.pipeline_details(pipeline_id.into()); + let details = webview.ensure_pipeline_details(pipeline_id.into()); details.most_recent_display_list_epoch = Some(display_list_info.epoch); details.hit_test_items = display_list_info.hit_test_info; details.install_new_scroll_tree(display_list_info.scroll_tree); @@ -725,9 +773,7 @@ impl IOCompositor { .set_display_list(display_list_info.epoch, (pipeline_id, built_display_list)); self.update_transaction_with_all_scroll_offsets(&mut transaction); self.generate_frame(&mut transaction, RenderReasons::SCENE); - self.global - .webrender_api - .send_transaction(self.webrender_document, transaction); + self.global.borrow_mut().send_transaction(transaction); }, CrossProcessCompositorMessage::HitTest(pipeline, point, flags, sender) => { @@ -741,14 +787,23 @@ impl IOCompositor { // would be to listen to the TransactionNotifier for previous per-pipeline // transactions, but that isn't easily compatible with the event loop wakeup // mechanism from libserver. - self.global.webrender_api.flush_scene_builder(); - - let result = self.hit_test_at_point_with_flags_and_pipeline(point, flags, pipeline); + self.global.borrow().webrender_api.flush_scene_builder(); + + let details_for_pipeline = |pipeline_id| self.details_for_pipeline(pipeline_id); + let result = self + .global + .borrow() + .hit_test_at_point_with_flags_and_pipeline( + point, + flags, + pipeline, + details_for_pipeline, + ); let _ = sender.send(result); }, CrossProcessCompositorMessage::GenerateImageKey(sender) => { - let _ = sender.send(self.global.webrender_api.generate_image_key()); + let _ = sender.send(self.global.borrow().webrender_api.generate_image_key()); }, CrossProcessCompositorMessage::UpdateImages(updates) => { @@ -764,9 +819,7 @@ impl IOCompositor { }, } } - self.global - .webrender_api - .send_transaction(self.webrender_document, txn); + self.global.borrow_mut().send_transaction(txn); }, CrossProcessCompositorMessage::AddFont(font_key, data, index) => { @@ -776,9 +829,7 @@ impl IOCompositor { CrossProcessCompositorMessage::AddSystemFont(font_key, native_handle) => { let mut transaction = Transaction::new(); transaction.add_native_font(font_key, native_handle); - self.global - .webrender_api - .send_transaction(self.webrender_document, transaction); + self.global.borrow_mut().send_transaction(transaction); }, CrossProcessCompositorMessage::AddFontInstance( @@ -800,17 +851,13 @@ impl IOCompositor { transaction.delete_font(key); } - self.global - .webrender_api - .send_transaction(self.webrender_document, transaction); + self.global.borrow_mut().send_transaction(transaction); }, CrossProcessCompositorMessage::AddImage(key, desc, data) => { let mut txn = Transaction::new(); txn.add_image(key, desc, data.into(), None); - self.global - .webrender_api - .send_transaction(self.webrender_document, txn); + self.global.borrow_mut().send_transaction(txn); }, CrossProcessCompositorMessage::GenerateFontKeys( @@ -819,10 +866,15 @@ impl IOCompositor { result_sender, ) => { let font_keys = (0..number_of_font_keys) - .map(|_| self.global.webrender_api.generate_font_key()) + .map(|_| self.global.borrow().webrender_api.generate_font_key()) .collect(); let font_instance_keys = (0..number_of_font_instance_keys) - .map(|_| self.global.webrender_api.generate_font_instance_key()) + .map(|_| { + self.global + .borrow() + .webrender_api + .generate_font_instance_key() + }) .collect(); let _ = result_sender.send((font_keys, font_instance_keys)); }, @@ -863,7 +915,7 @@ impl IOCompositor { "Compositor got pipeline exited: {:?} {:?}", webview_id, pipeline_id ); - if let Some(webview) = self.global.webviews.get_mut(webview_id) { + if let Some(webview) = self.webviews.get_mut(webview_id) { webview.remove_pipeline(pipeline_id); } let _ = sender.send(()); @@ -871,7 +923,7 @@ impl IOCompositor { CompositorMsg::CrossProcess(CrossProcessCompositorMessage::GenerateImageKey( sender, )) => { - let _ = sender.send(self.global.webrender_api.generate_image_key()); + let _ = sender.send(self.global.borrow().webrender_api.generate_image_key()); }, CompositorMsg::CrossProcess(CrossProcessCompositorMessage::GenerateFontKeys( number_of_font_keys, @@ -879,10 +931,15 @@ impl IOCompositor { result_sender, )) => { let font_keys = (0..number_of_font_keys) - .map(|_| self.global.webrender_api.generate_font_key()) + .map(|_| self.global.borrow().webrender_api.generate_font_key()) .collect(); let font_instance_keys = (0..number_of_font_instance_keys) - .map(|_| self.global.webrender_api.generate_font_instance_key()) + .map(|_| { + self.global + .borrow() + .webrender_api + .generate_font_instance_key() + }) .collect(); let _ = result_sender.send((font_keys, font_instance_keys)); }, @@ -919,7 +976,7 @@ impl IOCompositor { } /// Queue a new frame in the transaction and increase the pending frames count. - fn generate_frame(&mut self, transaction: &mut Transaction, reason: RenderReasons) { + pub(crate) fn generate_frame(&mut self, transaction: &mut Transaction, reason: RenderReasons) { self.pending_frames += 1; transaction.generate_frame(0, true /* present */, reason); } @@ -931,15 +988,16 @@ impl IOCompositor { let mut transaction = Transaction::new(); self.send_root_pipeline_display_list_in_transaction(&mut transaction); self.generate_frame(&mut transaction, RenderReasons::SCENE); - self.global - .webrender_api - .send_transaction(self.webrender_document, transaction); + self.global.borrow_mut().send_transaction(transaction); } /// Set the root pipeline for our WebRender scene to a display list that consists of an iframe /// for each visible top-level browsing context, applying a transformation on the root for /// pinch zoom, page zoom, and HiDPI scaling. - fn send_root_pipeline_display_list_in_transaction(&self, transaction: &mut Transaction) { + pub(crate) fn send_root_pipeline_display_list_in_transaction( + &self, + transaction: &mut Transaction, + ) { // Every display list needs a pipeline, but we'd like to choose one that is unlikely // to conflict with our content pipelines, which start at (1, 1). (0, 0) is WebRender's // dummy pipeline, so we choose (0, 1). @@ -972,7 +1030,7 @@ impl IOCompositor { let root_clip_id = builder.define_clip_rect(zoom_reference_frame, scaled_viewport_rect); let clip_chain_id = builder.define_clip_chain(None, [root_clip_id]); - for (_, webview) in self.global.webviews.painting_order() { + for (_, webview) in self.webviews.painting_order() { if let Some(pipeline_id) = webview.root_pipeline_id { let scaled_webview_rect = webview.rect / zoom_factor; builder.push_iframe( @@ -1006,7 +1064,7 @@ impl IOCompositor { /// TODO(mrobinson): Could we only send offsets for the branch being modified /// and not the entire scene? fn update_transaction_with_all_scroll_offsets(&self, transaction: &mut Transaction) { - for webview in self.global.webviews.iter() { + for webview in self.webviews.iter() { for details in webview.pipelines.values() { for node in details.scroll_tree.nodes.iter() { let (Some(offset), Some(external_id)) = (node.offset(), node.external_id()) @@ -1029,20 +1087,18 @@ impl IOCompositor { pub fn add_webview(&mut self, webview_id: WebViewId) { let size = self.rendering_context.size2d().to_f32(); - self.global.webviews.entry(webview_id).or_insert(WebView { - id: webview_id, - root_pipeline_id: None, - rect: Box2D::from_origin_and_size(Point2D::origin(), size), - pipelines: Default::default(), - pipeline_to_webview_map: self.global.pipeline_to_webview_map.clone(), - }); + self.webviews.entry(webview_id).or_insert(WebView::new( + webview_id, + Box2D::from_origin_and_size(Point2D::origin(), size), + self.global.clone(), + )); } fn set_frame_tree_for_webview(&mut self, frame_tree: &SendableFrameTree) { debug!("{}: Setting frame tree for webview", frame_tree.pipeline.id); let webview_id = frame_tree.pipeline.top_level_browsing_context_id; - let Some(webview) = self.global.webviews.get_mut(webview_id) else { + let Some(webview) = self.webviews.get_mut(webview_id) else { warn!( "Attempted to set frame tree on unknown WebView (perhaps closed?): {webview_id:?}" ); @@ -1055,7 +1111,7 @@ impl IOCompositor { fn remove_webview(&mut self, webview_id: WebViewId) { debug!("{}: Removing", webview_id); - if self.global.webviews.remove(webview_id).is_err() { + if self.webviews.remove(webview_id).is_err() { warn!("{webview_id}: Removing unknown webview"); return; }; @@ -1067,7 +1123,7 @@ impl IOCompositor { debug!("{webview_id}: Moving and/or resizing webview; rect={rect:?}"); let rect_changed; let size_changed; - match self.global.webviews.get_mut(webview_id) { + match self.webviews.get_mut(webview_id) { Some(webview) => { rect_changed = rect != webview.rect; size_changed = rect.size() != webview.rect.size(); @@ -1096,16 +1152,15 @@ impl IOCompositor { debug!("{webview_id}: Showing webview; hide_others={hide_others}"); let painting_order_changed = if hide_others { let result = self - .global .webviews .painting_order() .map(|(&id, _)| id) .ne(once(webview_id)); - self.global.webviews.hide_all(); - self.global.webviews.show(webview_id)?; + self.webviews.hide_all(); + self.webviews.show(webview_id)?; result } else { - self.global.webviews.show(webview_id)? + self.webviews.show(webview_id)? }; if painting_order_changed { self.send_root_pipeline_display_list(); @@ -1115,7 +1170,7 @@ impl IOCompositor { pub fn hide_webview(&mut self, webview_id: WebViewId) -> Result<(), UnknownWebView> { debug!("{webview_id}: Hiding webview"); - if self.global.webviews.hide(webview_id)? { + if self.webviews.hide(webview_id)? { self.send_root_pipeline_display_list(); } Ok(()) @@ -1129,16 +1184,15 @@ impl IOCompositor { debug!("{webview_id}: Raising webview to top; hide_others={hide_others}"); let painting_order_changed = if hide_others { let result = self - .global .webviews .painting_order() .map(|(&id, _)| id) .ne(once(webview_id)); - self.global.webviews.hide_all(); - self.global.webviews.raise_to_top(webview_id)?; + self.webviews.hide_all(); + self.webviews.raise_to_top(webview_id)?; result } else { - self.global.webviews.raise_to_top(webview_id)? + self.webviews.raise_to_top(webview_id)? }; if painting_order_changed { self.send_root_pipeline_display_list(); @@ -1163,7 +1217,7 @@ impl IOCompositor { }, WindowSizeType::Resize, ); - if let Err(e) = self.global.constellation_sender.send(msg) { + if let Err(e) = self.global.borrow().constellation_sender.send(msg) { warn!("Sending window resize to constellation failed ({:?}).", e); } } @@ -1173,7 +1227,7 @@ impl IOCompositor { } pub fn resize_rendering_context(&mut self, new_size: PhysicalSize<u32>) -> bool { - if self.shutdown_state() != ShutdownState::NotShuttingDown { + if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { return false; } @@ -1193,589 +1247,13 @@ impl IOCompositor { Point2D::new(new_size.width as i32, new_size.height as i32), ); transaction.set_document_view(output_region); - self.global - .webrender_api - .send_transaction(self.webrender_document, transaction); + self.global.borrow_mut().send_transaction(transaction); self.update_after_zoom_or_hidpi_change(); self.set_needs_repaint(RepaintReason::Resize); true } - 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 Some(result) = self.hit_test_at_point(point) else { - return; - }; - - self.update_cursor(&result); - - if let Err(error) = self - .global - .constellation_sender - .send(ConstellationMsg::ForwardInputEvent(event, Some(result))) - { - warn!("Sending event to constellation failed ({error:?})."); - } - } - - pub fn on_input_event(&mut self, event: InputEvent) { - if self.shutdown_state() != ShutdownState::NotShuttingDown { - return; - } - - if let InputEvent::Touch(event) = event { - self.on_touch_event(event); - return; - } - - if self.convert_mouse_to_touch { - match event { - InputEvent::MouseButton(event) => { - match event.action { - MouseButtonAction::Click => {}, - MouseButtonAction::Down => self.on_touch_down(TouchEvent::new( - TouchEventType::Down, - TouchId(0), - event.point, - )), - MouseButtonAction::Up => self.on_touch_up(TouchEvent::new( - TouchEventType::Up, - TouchId(0), - event.point, - )), - } - return; - }, - InputEvent::MouseMove(event) => { - self.on_touch_move(TouchEvent::new( - TouchEventType::Move, - TouchId(0), - event.point, - )); - return; - }, - _ => {}, - } - } - - self.dispatch_input_event(event); - } - - fn hit_test_at_point(&self, point: DevicePoint) -> Option<CompositorHitTestResult> { - self.hit_test_at_point_with_flags_and_pipeline(point, HitTestFlags::empty(), None) - .first() - .cloned() - } - - fn hit_test_at_point_with_flags_and_pipeline( - &self, - point: DevicePoint, - flags: HitTestFlags, - pipeline_id: Option<WebRenderPipelineId>, - ) -> Vec<CompositorHitTestResult> { - // DevicePoint and WorldPoint are the same for us. - let world_point = WorldPoint::from_untyped(point.to_untyped()); - let results = self.global.webrender_api.hit_test( - self.webrender_document, - pipeline_id, - world_point, - flags, - ); - - results - .items - .iter() - .filter_map(|item| { - let pipeline_id = item.pipeline.into(); - let details = self.details_for_pipeline(pipeline_id)?; - - // If the epoch in the tag does not match the current epoch of the pipeline, - // then the hit test is against an old version of the display list and we - // should ignore this hit test for now. - match details.most_recent_display_list_epoch { - Some(epoch) if epoch.as_u16() == item.tag.1 => {}, - _ => return None, - } - - let info = &details.hit_test_items[item.tag.0 as usize]; - Some(CompositorHitTestResult { - pipeline_id, - point_in_viewport: item.point_in_viewport.to_untyped(), - point_relative_to_item: item.point_relative_to_item.to_untyped(), - node: UntrustedNodeAddress(info.node as *const c_void), - cursor: info.cursor, - scroll_tree_node: info.scroll_tree_node, - }) - }) - .collect() - } - - fn send_touch_event(&self, mut event: TouchEvent) -> bool { - let Some(result) = self.hit_test_at_point(event.point) else { - return false; - }; - - event.init_sequence_id(self.touch_handler.current_sequence_id); - let event = InputEvent::Touch(event); - if let Err(e) = self - .global - .constellation_sender - .send(ConstellationMsg::ForwardInputEvent(event, Some(result))) - { - warn!("Sending event to constellation failed ({:?}).", e); - false - } else { - true - } - } - - pub fn on_touch_event(&mut self, event: TouchEvent) { - if self.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); - } - - 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) { - 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 fn on_scroll_event( - &mut self, - scroll_location: ScrollLocation, - cursor: DeviceIntPoint, - event_type: TouchEventType, - ) { - if self.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, - })); - } - - fn process_pending_scroll_events(&mut self) { - // 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).clamp(MIN_ZOOM, MAX_ZOOM), - ); - 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 { - self.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); - } - - self.generate_frame(&mut transaction, RenderReasons::APZ); - self.global - .webrender_api - .send_transaction(self.webrender_document, 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 hit_test_results = - self.hit_test_at_point_with_flags_and_pipeline(cursor, HitTestFlags::FIND_ALL, None); - - // 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.details_for_pipeline_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 - } - /// If there are any animations running, dispatches appropriate messages to the constellation. fn process_animations(&mut self, force: bool) { // When running animations in order to dump a screenshot (not after a full composite), don't send @@ -1790,12 +1268,11 @@ impl IOCompositor { self.last_animation_tick = Instant::now(); #[cfg(feature = "webxr")] - let webxr_running = self.global.webxr_main_thread.running(); + let webxr_running = self.global.borrow().webxr_main_thread.running(); #[cfg(not(feature = "webxr"))] let webxr_running = false; let any_webviews_animating = !self - .global .webviews .iter() .all(|webview| !webview.tick_all_animations(self)); @@ -1813,7 +1290,7 @@ impl IOCompositor { self.embedder_coordinates.hidpi_factor } - fn device_pixels_per_page_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> { + pub(crate) fn device_pixels_per_page_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> { self.device_pixels_per_page_pixel_not_including_page_zoom() * self.pinch_zoom_level() } @@ -1824,7 +1301,7 @@ impl IOCompositor { } pub fn on_zoom_reset_window_event(&mut self) { - if self.shutdown_state() != ShutdownState::NotShuttingDown { + if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1833,7 +1310,7 @@ impl IOCompositor { } pub fn on_zoom_window_event(&mut self, magnification: f32) { - if self.shutdown_state() != ShutdownState::NotShuttingDown { + if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1843,7 +1320,7 @@ impl IOCompositor { } fn update_after_zoom_or_hidpi_change(&mut self) { - for (top_level_browsing_context_id, webview) in self.global.webviews.painting_order() { + for (top_level_browsing_context_id, webview) in self.webviews.painting_order() { self.send_window_size_message_for_top_level_browser_context( webview.rect, *top_level_browsing_context_id, @@ -1854,91 +1331,26 @@ impl IOCompositor { self.send_root_pipeline_display_list(); } - /// Simulate a pinch zoom - pub fn on_pinch_zoom_window_event(&mut self, magnification: f32) { - if self.shutdown_state() != ShutdownState::NotShuttingDown { - return; - } - - // TODO: Scroll to keep the center in view? - self.pending_scroll_zoom_events - .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 details_for_pipeline(&self, pipeline_id: PipelineId) -> Option<&PipelineDetails> { let webview_id = self .global - .pipeline_to_webview_map .borrow() - .get(&pipeline_id) - .cloned()?; - self.global - .webviews - .get(webview_id)? - .pipelines - .get(&pipeline_id) - } - - fn details_for_pipeline_mut( - &mut self, - pipeline_id: PipelineId, - ) -> Option<&mut PipelineDetails> { - let webview_id = self - .global .pipeline_to_webview_map - .borrow() .get(&pipeline_id) .cloned()?; - self.global - .webviews - .get_mut(webview_id)? - .pipelines - .get_mut(&pipeline_id) - } - - fn send_scroll_positions_to_layout_for_pipeline(&self, pipeline_id: PipelineId) { - let Some(details) = self.details_for_pipeline(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, - }); - } - }); - - if let Some(pipeline) = details.pipeline.as_ref() { - let message = ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states); - let _ = pipeline.script_chan.send(message); - } + self.webviews.get(webview_id)?.pipelines.get(&pipeline_id) } // Check if any pipelines currently have active animations or animation callbacks. fn animations_or_animation_callbacks_running(&self) -> bool { - self.global - .webviews + self.webviews .iter() .any(WebView::animations_or_animation_callbacks_running) } /// Returns true if any animation callbacks (ie `requestAnimationFrame`) are waiting for a response. fn animation_callbacks_running(&self) -> bool { - self.global - .webviews + self.webviews .iter() .any(WebView::animation_callbacks_running) } @@ -1956,11 +1368,11 @@ impl IOCompositor { // This gets sent to the constellation for comparison with the current // frame tree. let mut pipeline_epochs = HashMap::new(); - for id in self.global.webviews.iter().flat_map(WebView::pipeline_ids) { + for id in self.webviews.iter().flat_map(WebView::pipeline_ids) { if let Some(WebRenderEpoch(epoch)) = self .webrender .as_ref() - .and_then(|wr| wr.current_epoch(self.webrender_document, id.into())) + .and_then(|wr| wr.current_epoch(self.webrender_document(), id.into())) { let epoch = Epoch(epoch); pipeline_epochs.insert(*id, epoch); @@ -1970,7 +1382,7 @@ impl IOCompositor { // Pass the pipeline/epoch states to the constellation and check // if it's safe to output the image. let msg = ConstellationMsg::IsReadyToSaveImage(pipeline_epochs); - if let Err(e) = self.global.constellation_sender.send(msg) { + if let Err(e) = self.global.borrow().constellation_sender.send(msg) { warn!("Sending ready to save to constellation failed ({:?}).", e); } self.ready_to_save_state = ReadyState::WaitingForConstellationReply; @@ -2079,10 +1491,11 @@ impl IOCompositor { self.rendering_context.prepare_for_rendering(); + let time_profiler_chan = self.global.borrow().time_profiler_chan.clone(); time_profile!( ProfilerCategory::Compositing, None, - self.global.time_profiler_chan.clone(), + time_profiler_chan, || { trace!("Compositing"); @@ -2109,7 +1522,8 @@ impl IOCompositor { /// the list. fn send_pending_paint_metrics_messages_after_composite(&mut self) { let paint_time = CrossProcessInstant::now(); - for webview_details in self.global.webviews.iter_mut() { + let document_id = self.webrender_document(); + for webview_details in self.webviews.iter_mut() { // For each pipeline, determine the current epoch and update paint timing if necessary. for (pipeline_id, pipeline) in webview_details.pipelines.iter_mut() { if pipeline.pending_paint_metrics.is_empty() { @@ -2122,7 +1536,7 @@ impl IOCompositor { let Some(WebRenderEpoch(current_epoch)) = self .webrender .as_ref() - .and_then(|wr| wr.current_epoch(self.webrender_document, pipeline_id.into())) + .and_then(|wr| wr.current_epoch(document_id, pipeline_id.into())) else { continue; }; @@ -2156,7 +1570,7 @@ impl IOCompositor { } fn clear_background(&self) { - let gl = &self.global.webrender_gl; + let gl = &self.global.borrow().webrender_gl; self.assert_gl_framebuffer_complete(); // Always clear the entire RenderingContext, regardless of how many WebViews there are @@ -2174,15 +1588,19 @@ impl IOCompositor { #[track_caller] fn assert_no_gl_error(&self) { - debug_assert_eq!(self.global.webrender_gl.get_error(), gleam::gl::NO_ERROR); + debug_assert_eq!( + self.global.borrow().webrender_gl.get_error(), + gleam::gl::NO_ERROR + ); } #[track_caller] fn assert_gl_framebuffer_complete(&self) { debug_assert_eq!( ( - self.global.webrender_gl.get_error(), + self.global.borrow().webrender_gl.get_error(), self.global + .borrow() .webrender_gl .check_frame_buffer_status(gleam::gl::FRAMEBUFFER) ), @@ -2198,7 +1616,12 @@ impl IOCompositor { // Check for new messages coming from the other threads in the system. let mut compositor_messages = vec![]; let mut found_recomposite_msg = false; - while let Some(msg) = self.global.compositor_receiver.try_recv_compositor_msg() { + while let Some(msg) = self + .global + .borrow_mut() + .compositor_receiver + .try_recv_compositor_msg() + { match msg { CompositorMsg::NewWebRenderFrameReady(..) if found_recomposite_msg => { // Only take one of duplicate NewWebRendeFrameReady messages, but do subtract @@ -2215,7 +1638,7 @@ impl IOCompositor { for msg in compositor_messages { self.handle_browser_message(msg); - if self.shutdown_state() == ShutdownState::FinishedShuttingDown { + if self.global.borrow().shutdown_state() == ShutdownState::FinishedShuttingDown { return; } } @@ -2226,7 +1649,7 @@ impl IOCompositor { tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] pub fn perform_updates(&mut self) -> bool { - if self.shutdown_state() == ShutdownState::FinishedShuttingDown { + if self.global.borrow().shutdown_state() == ShutdownState::FinishedShuttingDown { return false; } @@ -2241,23 +1664,25 @@ impl IOCompositor { #[cfg(feature = "webxr")] // Run the WebXR main thread - self.global.webxr_main_thread.run_one_frame(); + self.global.borrow_mut().webxr_main_thread.run_one_frame(); // The WebXR thread may make a different context current if let Err(err) = self.rendering_context.make_current() { warn!("Failed to make the rendering context current: {:?}", err); } - if !self.pending_scroll_zoom_events.is_empty() { - self.process_pending_scroll_events() + let mut webviews = take(&mut self.webviews); + for webview in webviews.iter_mut() { + webview.process_pending_scroll_events(self); } - self.shutdown_state() != ShutdownState::FinishedShuttingDown + self.webviews = webviews; + self.global.borrow().shutdown_state() != ShutdownState::FinishedShuttingDown } pub 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 { + pub(crate) 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); } @@ -2288,9 +1713,7 @@ impl IOCompositor { let mut txn = Transaction::new(); self.generate_frame(&mut txn, RenderReasons::TESTING); - self.global - .webrender_api - .send_transaction(self.webrender_document, txn); + self.global.borrow_mut().send_transaction(txn); } pub fn capture_webrender(&mut self) { @@ -2315,12 +1738,13 @@ impl IOCompositor { println!("Saving WebRender capture to {capture_path:?}"); self.global + .borrow() .webrender_api .save_capture(capture_path.clone(), CaptureBits::all()); let version_file_path = capture_path.join("servo-version.txt"); if let Err(error) = File::create(version_file_path) - .and_then(|mut file| write!(file, "{}", self.global.version_string)) + .and_then(|mut file| write!(file, "{}", self.global.borrow().version_string)) { eprintln!("Unable to write servo version for WebRender Capture: {error:?}"); } @@ -2348,16 +1772,50 @@ impl IOCompositor { Vec::new(), ); - self.global - .webrender_api - .send_transaction(self.webrender_document, transaction); + self.global.borrow_mut().send_transaction(transaction); } fn add_font(&mut self, font_key: FontKey, index: u32, data: Arc<IpcSharedMemory>) { let mut transaction = Transaction::new(); transaction.add_raw_font(font_key, (**data).into(), index); - self.global - .webrender_api - .send_transaction(self.webrender_document, transaction); + self.global.borrow_mut().send_transaction(transaction); + } + + pub fn notify_input_event(&mut self, webview_id: WebViewId, event: InputEvent) { + if let Some(webview) = self.webviews.get_mut(webview_id) { + webview.notify_input_event(event); + } + } + + pub fn notify_scroll_event( + &mut self, + webview_id: WebViewId, + scroll_location: ScrollLocation, + cursor: DeviceIntPoint, + event_type: TouchEventType, + ) { + if let Some(webview) = self.webviews.get_mut(webview_id) { + webview.notify_scroll_event(scroll_location, cursor, event_type); + } + } + + pub fn on_vsync(&mut self, webview_id: WebViewId) { + if let Some(webview) = self.webviews.get_mut(webview_id) { + webview.on_vsync(); + } + } + + pub fn set_pinch_zoom(&mut self, webview_id: WebViewId, magnification: f32) { + if let Some(webview) = self.webviews.get_mut(webview_id) { + webview.set_pinch_zoom(magnification); + } + } + + fn webrender_document(&self) -> DocumentId { + self.global.borrow().webrender_document + } + + fn shutdown_state(&self) -> ShutdownState { + self.global.borrow().shutdown_state() } } diff --git a/components/compositing/webview.rs b/components/compositing/webview.rs index d22419633e8..a4da68d8d52 100644 --- a/components/compositing/webview.rs +++ b/components/compositing/webview.rs @@ -8,14 +8,44 @@ use std::collections::hash_map::{Entry, Keys, Values, ValuesMut}; use std::rc::Rc; use base::id::{PipelineId, WebViewId}; -use compositing_traits::SendableFrameTree; +use compositing_traits::{ConstellationMsg, SendableFrameTree}; +use embedder_traits::{ + InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, ShutdownState, + TouchEvent, TouchEventType, TouchId, +}; +use euclid::{Point2D, Scale, Vector2D}; use fnv::FnvHashSet; -use log::debug; -use script_traits::AnimationState; -use webrender_api::units::{DeviceRect, LayoutVector2D}; +use log::{debug, warn}; +use script_traits::{AnimationState, ScriptThreadMessage, ScrollState, TouchEventResult}; +use webrender::Transaction; +use webrender_api::units::{DeviceIntPoint, DevicePoint, DeviceRect, LayoutVector2D}; +use webrender_api::{ + ExternalScrollId, HitTestFlags, RenderReasons, SampledScrollOffset, ScrollLocation, +}; +use webrender_traits::CompositorHitTestResult; use crate::IOCompositor; -use crate::compositor::PipelineDetails; +use crate::compositor::{PipelineDetails, ServoRenderer}; +use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; + +#[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), +} pub(crate) struct WebView { /// The [`WebViewId`] of the `WebView` associated with this [`WebViewDetails`]. @@ -25,21 +55,36 @@ pub(crate) struct WebView { pub rect: DeviceRect, /// Tracks details about each active pipeline that the compositor knows about. pub pipelines: HashMap<PipelineId, PipelineDetails>, - /// This is a temporary map between [`PipelineId`]s and their associated [`WebViewId`]. Once - /// all renderer operations become per-`WebView` this map can be removed, but we still sometimes - /// need to work backwards to figure out what `WebView` is associated with a `Pipeline`. - pub pipeline_to_webview_map: Rc<RefCell<HashMap<PipelineId, WebViewId>>>, + /// 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, } impl Drop for WebView { fn drop(&mut self) { - self.pipeline_to_webview_map + self.global .borrow_mut() + .pipeline_to_webview_map .retain(|_, webview_id| self.id != *webview_id); } } impl WebView { + pub(crate) fn new(id: WebViewId, rect: DeviceRect, global: Rc<RefCell<ServoRenderer>>) -> Self { + Self { + id, + root_pipeline_id: None, + rect, + pipelines: Default::default(), + touch_handler: TouchHandler::new(), + global, + pending_scroll_zoom_events: Default::default(), + } + } + pub(crate) fn animations_or_animation_callbacks_running(&self) -> bool { self.pipelines .values() @@ -56,22 +101,28 @@ impl WebView { self.pipelines.keys() } - pub(crate) fn pipeline_details(&mut self, pipeline_id: PipelineId) -> &mut PipelineDetails { + /// 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.pipeline_to_webview_map + 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.pipeline_details(pipeline_id).throttled = throttled; + self.ensure_pipeline_details(pipeline_id).throttled = throttled; } pub(crate) fn remove_pipeline(&mut self, pipeline_id: PipelineId) { - self.pipeline_to_webview_map + self.global .borrow_mut() + .pipeline_to_webview_map .remove(&pipeline_id); self.pipelines.remove(&pipeline_id); } @@ -89,6 +140,28 @@ impl WebView { 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, + }); + } + }); + + if let Some(pipeline) = details.pipeline.as_ref() { + let message = ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states); + let _ = pipeline.script_chan.send(message); + } } pub(crate) fn set_frame_tree_on_pipeline_details( @@ -97,7 +170,7 @@ impl WebView { parent_pipeline_id: Option<PipelineId>, ) { let pipeline_id = frame_tree.pipeline.id; - let pipeline_details = self.pipeline_details(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; @@ -143,7 +216,7 @@ impl WebView { pipeline_id: PipelineId, animation_state: AnimationState, ) -> bool { - let pipeline_details = self.pipeline_details(pipeline_id); + let pipeline_details = self.ensure_pipeline_details(pipeline_id); match animation_state { AnimationState::AnimationsPresent => { pipeline_details.animations_running = true; @@ -180,10 +253,587 @@ impl WebView { } pub(crate) fn add_pending_paint_metric(&mut self, pipeline_id: PipelineId, epoch: base::Epoch) { - self.pipeline_details(pipeline_id) + self.ensure_pipeline_details(pipeline_id) .pending_paint_metrics .push(epoch); } + + /// 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, + ); + } + } + + 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(&result); + + if let Err(error) = + self.global + .borrow() + .constellation_sender + .send(ConstellationMsg::ForwardInputEvent( + self.id, + event, + Some(result), + )) + { + warn!("Sending event to constellation failed ({error:?})."); + } + } + + pub 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.action { + MouseButtonAction::Click => {}, + MouseButtonAction::Down => self.on_touch_down(TouchEvent::new( + TouchEventType::Down, + TouchId(0), + event.point, + )), + MouseButtonAction::Up => self.on_touch_up(TouchEvent::new( + TouchEventType::Up, + TouchId(0), + event.point, + )), + } + return; + }, + InputEvent::MouseMove(event) => { + self.on_touch_move(TouchEvent::new( + TouchEventType::Move, + TouchId(0), + event.point, + )); + 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(ConstellationMsg::ForwardInputEvent( + self.id, + event, + Some(result), + )) + { + warn!("Sending event to constellation failed ({:?}).", e); + false + } else { + true + } + } + + pub 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) { + 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 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 = compositor + .set_pinch_zoom_level(compositor.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, + compositor, + ) + }); + 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, + compositor: &mut IOCompositor, + ) -> Option<(PipelineId, ExternalScrollId, LayoutVector2D)> { + let scroll_location = match scroll_location { + ScrollLocation::Delta(delta) => { + let device_pixels_per_page = compositor.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 + } + + /// Simulate a pinch zoom + pub 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)); + } } #[derive(Debug)] pub struct WebViewManager<WebView> { diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 7f8ca662dfe..3502709f078 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -1411,8 +1411,8 @@ where FromCompositorMsg::LogEntry(top_level_browsing_context_id, thread_name, entry) => { self.handle_log_entry(top_level_browsing_context_id, thread_name, entry); }, - FromCompositorMsg::ForwardInputEvent(event, hit_test) => { - self.forward_input_event(event, hit_test); + FromCompositorMsg::ForwardInputEvent(webview_id, event, hit_test) => { + self.forward_input_event(webview_id, event, hit_test); }, FromCompositorMsg::SetCursor(webview_id, cursor) => { self.handle_set_cursor_msg(webview_id, cursor) @@ -1631,7 +1631,7 @@ where }, FromScriptMsg::TouchEventProcessed(result) => self .compositor_proxy - .send(CompositorMsg::TouchEventProcessed(result)), + .send(CompositorMsg::TouchEventProcessed(webview_id, result)), FromScriptMsg::GetBrowsingContextInfo(pipeline_id, response_sender) => { let result = self .pipelines @@ -2893,6 +2893,7 @@ where fn forward_input_event( &mut self, + webview_id: WebViewId, event: InputEvent, hit_test_result: Option<CompositorHitTestResult>, ) { @@ -2914,13 +2915,13 @@ where let pipeline_id = match &hit_test_result { Some(hit_test) => hit_test.pipeline_id, None => { - // If there's no hit test, send to the currently focused WebView. + // If there's no hit test, send to the focused browsing context of the given webview. let Some(browsing_context_id) = self .webviews - .focused_webview() - .map(|(_, webview)| webview.focused_browsing_context_id) + .get(webview_id) + .map(|webview| webview.focused_browsing_context_id) else { - warn!("Handling InputEvent with no focused WebView"); + warn!("Handling InputEvent for an unknown webview: {webview_id}"); return; }; @@ -4577,18 +4578,25 @@ where self.handle_send_error(pipeline_id, e) } }, - WebDriverCommandMsg::MouseButtonAction(mouse_event_type, mouse_button, x, y) => { + WebDriverCommandMsg::MouseButtonAction( + webview_id, + mouse_event_type, + mouse_button, + x, + y, + ) => { self.compositor_proxy .send(CompositorMsg::WebDriverMouseButtonEvent( + webview_id, mouse_event_type, mouse_button, x, y, )); }, - WebDriverCommandMsg::MouseMoveAction(x, y) => { + WebDriverCommandMsg::MouseMoveAction(webview_id, x, y) => { self.compositor_proxy - .send(CompositorMsg::WebDriverMouseMoveEvent(x, y)); + .send(CompositorMsg::WebDriverMouseMoveEvent(webview_id, x, y)); }, WebDriverCommandMsg::TakeScreenshot(_, rect, response_sender) => { self.compositor_proxy diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 0c098b7bb45..392a6bbcc70 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -82,7 +82,7 @@ mod from_compositor { Self::SendError(..) => target!("SendError"), Self::FocusWebView(..) => target!("FocusWebView"), Self::BlurWebView => target!("BlurWebView"), - Self::ForwardInputEvent(event, ..) => event.log_target(), + Self::ForwardInputEvent(_webview_id, event, ..) => event.log_target(), Self::SetCursor(..) => target!("SetCursor"), Self::ToggleProfiler(..) => target!("EnableProfiler"), Self::ExitFullScreen(_) => target!("ExitFullScreen"), diff --git a/components/servo/webview.rs b/components/servo/webview.rs index ba60ccd6bdc..3aa7c2a9e51 100644 --- a/components/servo/webview.rs +++ b/components/servo/webview.rs @@ -339,23 +339,30 @@ impl WebView { point: DeviceIntPoint, touch_event_action: TouchEventType, ) { - self.inner() - .compositor - .borrow_mut() - .on_scroll_event(location, point, touch_event_action); + self.inner().compositor.borrow_mut().notify_scroll_event( + self.id(), + location, + point, + touch_event_action, + ); } pub fn notify_input_event(&self, event: InputEvent) { // Events with a `point` first go to the compositor for hit testing. if event.point().is_some() { - self.inner().compositor.borrow_mut().on_input_event(event); + self.inner() + .compositor + .borrow_mut() + .notify_input_event(self.id(), event); return; } self.inner() .constellation_proxy .send(ConstellationMsg::ForwardInputEvent( - event, None, /* hit_test */ + self.id(), + event, + None, /* hit_test */ )) } @@ -366,7 +373,7 @@ impl WebView { } pub fn notify_vsync(&self) { - self.inner().compositor.borrow_mut().on_vsync(); + self.inner().compositor.borrow_mut().on_vsync(self.id()); } pub fn resize(&self, new_size: PhysicalSize<u32>) { @@ -401,7 +408,7 @@ impl WebView { self.inner() .compositor .borrow_mut() - .on_pinch_zoom_window_event(new_pinch_zoom); + .set_pinch_zoom(self.id(), new_pinch_zoom); } pub fn exit_fullscreen(&self) { diff --git a/components/shared/compositing/constellation_msg.rs b/components/shared/compositing/constellation_msg.rs index a07b943d7e6..412cfcba32a 100644 --- a/components/shared/compositing/constellation_msg.rs +++ b/components/shared/compositing/constellation_msg.rs @@ -62,7 +62,7 @@ pub enum ConstellationMsg { /// Make none of the webviews focused. BlurWebView, /// Forward an input event to an appropriate ScriptTask. - ForwardInputEvent(InputEvent, Option<CompositorHitTestResult>), + ForwardInputEvent(WebViewId, InputEvent, Option<CompositorHitTestResult>), /// Requesting a change to the onscreen cursor. SetCursor(WebViewId, Cursor), /// Enable the sampling profiler, with a given sampling rate and max total sampling duration. diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index 737b105b1ba..ddca92a3c57 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -65,7 +65,7 @@ pub enum CompositorMsg { /// Remove a webview. RemoveWebView(TopLevelBrowsingContextId), /// Script has handled a touch event, and either prevented or allowed default actions. - TouchEventProcessed(TouchEventResult), + TouchEventProcessed(WebViewId, TouchEventResult), /// Composite to a PNG file and return the Image over a passed channel. CreatePng(Option<Rect<f32, CSSPixel>>, IpcSender<Option<Image>>), /// A reply to the compositor asking if the output image is stable. @@ -89,9 +89,9 @@ pub enum CompositorMsg { /// The load of a page has completed LoadComplete(TopLevelBrowsingContextId), /// WebDriver mouse button event - WebDriverMouseButtonEvent(MouseButtonAction, MouseButton, f32, f32), + WebDriverMouseButtonEvent(WebViewId, MouseButtonAction, MouseButton, f32, f32), /// WebDriver mouse move event - WebDriverMouseMoveEvent(f32, f32), + WebDriverMouseMoveEvent(WebViewId, f32, f32), /// Messages forwarded to the compositor by the constellation from other crates. These /// messages are mainly passed on from the compositor to WebRender. diff --git a/components/shared/embedder/webdriver.rs b/components/shared/embedder/webdriver.rs index 1da996038a8..349a45edf1b 100644 --- a/components/shared/embedder/webdriver.rs +++ b/components/shared/embedder/webdriver.rs @@ -41,9 +41,9 @@ pub enum WebDriverCommandMsg { /// Act as if keys were pressed or release in the browsing context with the given ID. KeyboardAction(BrowsingContextId, KeyboardEvent), /// Act as if the mouse was clicked in the browsing context with the given ID. - MouseButtonAction(MouseButtonAction, MouseButton, f32, f32), + MouseButtonAction(WebViewId, MouseButtonAction, MouseButton, f32, f32), /// Act as if the mouse was moved in the browsing context with the given ID. - MouseMoveAction(f32, f32), + MouseMoveAction(WebViewId, f32, f32), /// Set the window size. SetWindowSize(WebViewId, DeviceIntSize, IpcSender<Size2D<f32, CSSPixel>>), /// Take a screenshot of the window. diff --git a/components/webdriver_server/actions.rs b/components/webdriver_server/actions.rs index 8e3f6751101..0c5f24cac85 100644 --- a/components/webdriver_server/actions.rs +++ b/components/webdriver_server/actions.rs @@ -279,6 +279,7 @@ impl Handler { let button = (action.button as u16).into(); let cmd_msg = WebDriverCommandMsg::MouseButtonAction( + session.top_level_browsing_context_id, MouseButtonAction::Down, button, pointer_input_state.x as f32, @@ -325,6 +326,7 @@ impl Handler { let button = (action.button as u16).into(); let cmd_msg = WebDriverCommandMsg::MouseButtonAction( + session.top_level_browsing_context_id, MouseButtonAction::Up, button, pointer_input_state.x as f32, @@ -469,7 +471,11 @@ impl Handler { // Step 7 if x != current_x || y != current_y { // Step 7.2 - let cmd_msg = WebDriverCommandMsg::MouseMoveAction(x as f32, y as f32); + let cmd_msg = WebDriverCommandMsg::MouseMoveAction( + session.top_level_browsing_context_id, + x as f32, + y as f32, + ); self.constellation_chan .send(ConstellationMsg::WebDriverCommand(cmd_msg)) .unwrap(); |