diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 612774a7675..567614bd5cf 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -46,7 +46,7 @@ use style_traits::{CSSPixel, DevicePixel, PinchZoomFactor}; use webrender; use webrender::{CaptureBits, RenderApi, Transaction}; use webrender_api::units::{ - DeviceIntPoint, DeviceIntSize, DevicePoint, LayoutPoint, LayoutRect, LayoutSize, + DeviceIntPoint, DeviceIntSize, DevicePoint, DeviceRect, LayoutPoint, LayoutRect, LayoutSize, LayoutVector2D, WorldPoint, }; use webrender_api::{ @@ -58,6 +58,7 @@ use webrender_api::{ use crate::gl::RenderTargetInfo; use crate::touch::{TouchAction, TouchHandler}; +use crate::webview::WebViewManager; use crate::windowing::{ self, EmbedderCoordinates, MouseWindowEvent, WebRenderDebugOption, WindowMethods, }; @@ -133,8 +134,12 @@ pub struct IOCompositor { /// The root content pipeline ie the pipeline which contains the main frame /// to display. In the WebRender scene, this will be the only child of another /// pipeline which applies a pinch zoom transformation. + #[deprecated] root_content_pipeline: RootPipeline, + /// Our top-level browsing contexts. + webviews: WebViewManager, + /// Tracks details about each active pipeline that the compositor knows about. pipeline_details: HashMap, @@ -289,6 +294,9 @@ struct PipelineDetails { /// The pipeline associated with this PipelineDetails object. pipeline: Option, + /// The id of the parent pipeline, if any. + parent_pipeline_id: Option, + /// The epoch of the most recent display list for this pipeline. Note that this display /// list might not be displayed, as WebRender processes display lists asynchronously. most_recent_display_list_epoch: Option, @@ -315,6 +323,7 @@ impl PipelineDetails { fn new() -> PipelineDetails { PipelineDetails { pipeline: None, + parent_pipeline_id: None, most_recent_display_list_epoch: None, animations_running: false, animation_callbacks_running: false, @@ -364,6 +373,12 @@ pub enum CompositeTarget { PngFile(Rc), } +#[derive(Debug)] +pub struct WebView { + pub pipeline_id: Option, + pub rect: DeviceRect, +} + impl IOCompositor { fn new( window: Rc, @@ -374,6 +389,17 @@ impl IOCompositor { convert_mouse_to_touch: bool, top_level_browsing_context_id: TopLevelBrowsingContextId, ) -> Self { + let embedder_coordinates = window.get_coordinates(); + let mut webviews = WebViewManager::default(); + webviews.add( + top_level_browsing_context_id, + WebView { + pipeline_id: None, + rect: embedder_coordinates.get_viewport().to_f32(), + }, + ); + webviews.show(top_level_browsing_context_id); + IOCompositor { embedder_coordinates: window.get_coordinates(), window, @@ -382,6 +408,7 @@ impl IOCompositor { top_level_browsing_context_id, id: None, }, + webviews, pipeline_details: HashMap::new(), scale: Scale::new(1.0), composition_request: CompositionRequest::NoCompositingNecessary, @@ -547,11 +574,73 @@ impl IOCompositor { self.change_running_animations_state(pipeline_id, animation_state); }, - (CompositorMsg::SetFrameTree(frame_tree), ShutdownState::NotShuttingDown) => { - self.set_frame_tree(&frame_tree); + (CompositorMsg::UpdateWebView(frame_tree), ShutdownState::NotShuttingDown) => { + self.update_browser(&frame_tree); self.send_scroll_positions_to_layout_for_pipeline(&frame_tree.pipeline.id); }, + ( + CompositorMsg::RemoveWebView(top_level_browsing_context_id), + ShutdownState::NotShuttingDown, + ) => { + self.remove_webview(top_level_browsing_context_id); + + let painting_order = self.webviews.painting_order().map(|(&id, _)| id).collect(); + let msg = ConstellationMsg::WebViewPaintingOrder(painting_order); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Sending event to constellation failed ({:?}).", e); + } + }, + + ( + CompositorMsg::MoveResizeWebView(top_level_browsing_context_id, rect), + ShutdownState::NotShuttingDown, + ) => { + self.move_resize_webview(top_level_browsing_context_id, rect); + }, + + ( + CompositorMsg::ShowWebView(top_level_browsing_context_id), + ShutdownState::NotShuttingDown, + ) => { + self.webviews.show(top_level_browsing_context_id); + self.update_root_pipeline(); + + let painting_order = self.webviews.painting_order().map(|(&id, _)| id).collect(); + let msg = ConstellationMsg::WebViewPaintingOrder(painting_order); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Sending event to constellation failed ({:?}).", e); + } + }, + + ( + CompositorMsg::HideWebView(top_level_browsing_context_id), + ShutdownState::NotShuttingDown, + ) => { + self.webviews.hide(top_level_browsing_context_id); + self.update_root_pipeline(); + + let painting_order = self.webviews.painting_order().map(|(&id, _)| id).collect(); + let msg = ConstellationMsg::WebViewPaintingOrder(painting_order); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Sending event to constellation failed ({:?}).", e); + } + }, + + ( + CompositorMsg::RaiseWebViewToTop(top_level_browsing_context_id), + ShutdownState::NotShuttingDown, + ) => { + self.webviews.raise_to_top(top_level_browsing_context_id); + self.update_root_pipeline(); + + let painting_order = self.webviews.painting_order().map(|(&id, _)| id).collect(); + let msg = ConstellationMsg::WebViewPaintingOrder(painting_order); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Sending event to constellation failed ({:?}).", e); + } + }, + (CompositorMsg::Recomposite(reason), ShutdownState::NotShuttingDown) => { self.waiting_on_pending_frame = false; self.composition_request = CompositionRequest::CompositeNow(reason) @@ -935,20 +1024,34 @@ impl IOCompositor { } } - /// Set the root pipeline for our WebRender scene. If there is no pinch zoom applied, - /// the root pipeline is the root content pipeline. If there is pinch zoom, the root - /// content pipeline is wrapped in a display list that applies a pinch zoom - /// transformation to it. - fn set_root_content_pipeline_handling_pinch_zoom(&self, transaction: &mut Transaction) { - let root_content_pipeline = match self.root_content_pipeline.id { - Some(id) => id.to_webrender(), - None => return, - }; + /// 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 the pinch zoom transformation if any. + fn update_root_pipeline(&mut self) { + let mut txn = Transaction::new(); + self.update_root_pipeline_with_txn(&mut txn); + txn.generate_frame(0); + self.webrender_api + .send_transaction(self.webrender_document, txn); + } + /// 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 the pinch zoom transformation if any. + fn update_root_pipeline_with_txn(&self, transaction: &mut Transaction) { let zoom_factor = self.pinch_zoom_level(); - if zoom_factor == 1.0 { - transaction.set_root_pipeline(root_content_pipeline); - return; + + if !cfg!(feature = "multiview") { + if self.root_content_pipeline.id.is_none() { + return; + } + if zoom_factor == 1.0 { + let root_content_pipeline = self + .root_content_pipeline + .id + .expect("checked above") + .to_webrender(); + transaction.set_root_pipeline(root_content_pipeline); + return; + } } // Every display list needs a pipeline, but we'd like to choose one that is unlikely @@ -958,11 +1061,6 @@ impl IOCompositor { transaction.set_root_pipeline(root_pipeline); let mut builder = webrender_api::DisplayListBuilder::new(root_pipeline); - let viewport_size = LayoutSize::new( - self.embedder_coordinates.get_viewport().width() as f32, - self.embedder_coordinates.get_viewport().height() as f32, - ); - let viewport_rect = LayoutRect::new(LayoutPoint::zero(), viewport_size); let zoom_reference_frame = builder.push_reference_frame( LayoutPoint::zero(), SpatialId::root_reference_frame(root_pipeline), @@ -974,53 +1072,165 @@ impl IOCompositor { }, ); - builder.push_iframe( - viewport_rect, - viewport_rect, - &SpaceAndClipInfo { - spatial_id: zoom_reference_frame, - clip_id: ClipId::root(root_pipeline), - }, - root_content_pipeline, - true, - ); + let viewport_size = if cfg!(feature = "multiview") { + let dppx = self.page_zoom * self.hidpi_factor(); + let viewport_size = self.embedder_coordinates.get_viewport().size.to_f32() / dppx; + let viewport_size = LayoutSize::from_untyped(viewport_size.to_untyped()); + for (_, browser) in self.webviews.painting_order() { + if let Some(pipeline_id) = browser.pipeline_id { + let rect = browser.rect / dppx; + builder.push_iframe( + LayoutRect::from_untyped(&rect.to_untyped()), + LayoutRect::from_untyped(&rect.to_untyped()), + &SpaceAndClipInfo { + spatial_id: zoom_reference_frame, + clip_id: ClipId::root(pipeline_id.to_webrender()), + }, + pipeline_id.to_webrender(), + true, + ); + } + } + viewport_size + } else { + let root_content_pipeline = self + .root_content_pipeline + .id + .expect("checked above") + .to_webrender(); + let viewport_size = LayoutSize::new( + self.embedder_coordinates.get_viewport().width() as f32, + self.embedder_coordinates.get_viewport().height() as f32, + ); + let viewport_rect = LayoutRect::new(LayoutPoint::zero(), viewport_size); + builder.push_iframe( + viewport_rect, + viewport_rect, + &SpaceAndClipInfo { + spatial_id: zoom_reference_frame, + clip_id: ClipId::root(root_pipeline), + }, + root_content_pipeline, + true, + ); + viewport_size + }; + let built_display_list = builder.finalize(); // NB: We are always passing 0 as the epoch here, but this doesn't seem to // be an issue. WebRender will still update the scene and generate a new // frame even though the epoch hasn't changed. + let viewport_size = LayoutSize::from_untyped(viewport_size.to_untyped()); transaction.set_display_list( WebRenderEpoch(0), None, - viewport_rect.size, + viewport_size, built_display_list, false, ); } - fn set_frame_tree(&mut self, frame_tree: &SendableFrameTree) { - debug!( - "Setting the frame tree for pipeline {:?}", - frame_tree.pipeline.id - ); + fn update_browser(&mut self, frame_tree: &SendableFrameTree) { + debug!("{}: Updating browser", frame_tree.pipeline.id); - self.root_content_pipeline = RootPipeline { - top_level_browsing_context_id: frame_tree.pipeline.top_level_browsing_context_id, - id: Some(frame_tree.pipeline.id), - }; + if !cfg!(feature = "multiview") { + self.root_content_pipeline = RootPipeline { + top_level_browsing_context_id: frame_tree.pipeline.top_level_browsing_context_id, + id: Some(frame_tree.pipeline.id), + }; + } - let mut txn = Transaction::new(); - self.set_root_content_pipeline_handling_pinch_zoom(&mut txn); - txn.generate_frame(0); - self.webrender_api - .send_transaction(self.webrender_document, txn); + let top_level_browsing_context_id = frame_tree.pipeline.top_level_browsing_context_id; + if let Some(browser) = self.webviews.get_mut(top_level_browsing_context_id) { + let new_pipeline_id = Some(frame_tree.pipeline.id); + if new_pipeline_id != browser.pipeline_id { + debug!( + "{:?}: Updating browser from pipeline {:?} to {:?}", + top_level_browsing_context_id, browser.pipeline_id, new_pipeline_id + ); + } + browser.pipeline_id = new_pipeline_id; + } else { + let top_level_browsing_context_id = frame_tree.pipeline.top_level_browsing_context_id; + let pipeline_id = Some(frame_tree.pipeline.id); + debug!( + "{:?}: Creating new browser with pipeline {:?}", + top_level_browsing_context_id, pipeline_id + ); + self.webviews.add( + top_level_browsing_context_id, + WebView { + pipeline_id, + rect: self.embedder_coordinates.get_viewport().to_f32(), + }, + ); + } - self.create_pipeline_details_for_frame_tree(&frame_tree); + if !cfg!(feature = "multiview") { + let browser_ids = self + .webviews + .painting_order() + .map(|(&id, _)| id) + .collect::>(); + for browser_id in browser_ids { + if browser_id != top_level_browsing_context_id { + self.webviews.hide(browser_id); + } + } + self.webviews.raise_to_top(top_level_browsing_context_id); + } + + self.update_root_pipeline(); + self.update_pipeline_details_tree(&frame_tree, None); self.reset_scroll_tree_for_unattached_pipelines(&frame_tree); self.frame_tree_id.next(); } + fn remove_webview(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) { + debug!("{}: Removing", top_level_browsing_context_id); + let Some(browser) = self.webviews.remove(top_level_browsing_context_id) + else { return }; + + self.update_root_pipeline(); + if let Some(pipeline_id) = browser.pipeline_id { + self.remove_pipeline_details_tree(pipeline_id); + } + + self.frame_tree_id.next(); + } + + pub fn move_resize_webview( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + rect: DeviceRect, + ) { + let dppx = self.page_zoom * self.hidpi_factor(); + if let Some(webview) = self.webviews.get_mut(top_level_browsing_context_id) { + let initial_viewport = rect.size.to_f32() / dppx; + let data = WindowSizeData { + device_pixel_ratio: dppx, + initial_viewport, + }; + if rect.size != webview.rect.size { + let size_type = WindowSizeType::Resize; // FIXME always this value in practice + let msg = + ConstellationMsg::WindowSize(top_level_browsing_context_id, data, size_type); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Sending window resize to constellation failed ({:?}).", e); + } + } + webview.rect = rect; + self.update_root_pipeline(); + } else { + warn!( + "{}: MoveResizeBrowser on unknown top-level browsing context", + top_level_browsing_context_id + ); + } + } + fn reset_scroll_tree_for_unattached_pipelines(&mut self, frame_tree: &SendableFrameTree) { // TODO(mrobinson): Eventually this can selectively preserve the scroll trees // state for some unattached pipelines in order to preserve scroll position when @@ -1048,11 +1258,35 @@ impl IOCompositor { }) } - fn create_pipeline_details_for_frame_tree(&mut self, frame_tree: &SendableFrameTree) { - self.pipeline_details(frame_tree.pipeline.id).pipeline = Some(frame_tree.pipeline.clone()); + fn update_pipeline_details_tree( + &mut self, + frame_tree: &SendableFrameTree, + parent_pipeline_id: Option, + ) { + let pipeline_id = frame_tree.pipeline.id; + let pipeline_details = self.pipeline_details(pipeline_id); + pipeline_details.pipeline = Some(frame_tree.pipeline.clone()); + pipeline_details.parent_pipeline_id = parent_pipeline_id; for kid in &frame_tree.children { - self.create_pipeline_details_for_frame_tree(kid); + self.update_pipeline_details_tree(kid, Some(pipeline_id)); + } + } + + fn remove_pipeline_details_tree(&mut self, pipeline_id: PipelineId) { + self.pipeline_details.remove(&pipeline_id); + + let children = self + .pipeline_details + .iter() + .filter(|(_, pipeline_details)| { + pipeline_details.parent_pipeline_id == Some(pipeline_id) + }) + .map(|(&pipeline_id, _)| pipeline_id) + .collect::>(); + + for kid in children { + self.remove_pipeline_details_tree(kid); } } @@ -1060,16 +1294,11 @@ impl IOCompositor { self.pipeline_details.remove(&pipeline_id); } + #[deprecated] fn send_window_size(&mut self, size_type: WindowSizeType) { - let dppx = self.page_zoom * self.embedder_coordinates.hidpi_factor; + self.update_webrender_document_view(); - let mut transaction = Transaction::new(); - transaction.set_document_view( - self.embedder_coordinates.get_viewport(), - self.embedder_coordinates.hidpi_factor.get(), - ); - self.webrender_api - .send_transaction(self.webrender_document, transaction); + let dppx = self.page_zoom * self.embedder_coordinates.hidpi_factor; let initial_viewport = self.embedder_coordinates.viewport.size.to_f32() / dppx; @@ -1088,6 +1317,16 @@ impl IOCompositor { } } + fn update_webrender_document_view(&mut self) { + let mut transaction = Transaction::new(); + transaction.set_document_view( + self.embedder_coordinates.get_viewport(), + self.embedder_coordinates.hidpi_factor.get(), + ); + self.webrender_api + .send_transaction(self.webrender_document, transaction); + } + pub fn on_resize_window_event(&mut self) -> bool { trace!("Compositor resize requested"); @@ -1110,7 +1349,11 @@ impl IOCompositor { return false; } - self.send_window_size(WindowSizeType::Resize); + if cfg!(feature = "multiview") { + self.update_webrender_document_view(); + } else { + self.send_window_size(WindowSizeType::Resize); + } self.composite_if_necessary(CompositingReason::Resize); return true; } @@ -1135,9 +1378,10 @@ impl IOCompositor { MouseWindowEvent::MouseUp(_, p) => p, }; - let result = match self.hit_test_at_device_point(point) { - Some(result) => result, - None => return, + let Some(result) = self.hit_test_at_device_point(point) else { + // TODO notify embedder? + // let msg = ConstellationMsg::UnconsumedEvent(...); + return; }; let (button, event_type) = match mouse_window_event { @@ -1181,12 +1425,14 @@ impl IOCompositor { flags: HitTestFlags, pipeline_id: Option, ) -> Vec { - let root_pipeline_id = match self.root_content_pipeline.id { - Some(root_pipeline_id) => root_pipeline_id, - None => return vec![], - }; - if self.pipeline(root_pipeline_id).is_none() { - return vec![]; + if !cfg!(feature = "multiview") { + let root_pipeline_id = match self.root_content_pipeline.id { + Some(root_pipeline_id) => root_pipeline_id, + None => return vec![], + }; + if self.pipeline(root_pipeline_id).is_none() { + return vec![]; + } } let results = self.webrender_api @@ -1442,7 +1688,7 @@ impl IOCompositor { let mut transaction = Transaction::new(); if zoom_changed { - self.set_root_content_pipeline_handling_pinch_zoom(&mut transaction); + self.update_root_pipeline_with_txn(&mut transaction); } if let Some((pipeline_id, external_id, offset)) = scroll_result { @@ -1555,7 +1801,11 @@ impl IOCompositor { pub fn on_zoom_reset_window_event(&mut self) { self.page_zoom = Scale::new(1.0); self.update_zoom_transform(); - self.send_window_size(WindowSizeType::Resize); + if cfg!(feature = "multiview") { + self.update_webrender_document_view(); + } else { + self.send_window_size(WindowSizeType::Resize); + } self.update_page_zoom_for_webrender(); } @@ -1566,7 +1816,11 @@ impl IOCompositor { .min(MAX_ZOOM), ); self.update_zoom_transform(); - self.send_window_size(WindowSizeType::Resize); + if cfg!(feature = "multiview") { + self.update_webrender_document_view(); + } else { + self.send_window_size(WindowSizeType::Resize); + } self.update_page_zoom_for_webrender(); } @@ -1715,7 +1969,7 @@ impl IOCompositor { rect: Option>, ) -> Result, UnableToComposite> { if self.waiting_on_present { - debug!("tried to composite while waiting on present"); + trace!("tried to composite while waiting on present"); return Err(UnableToComposite::NotReadyToPaintImage( NotReadyToPaint::WaitingOnConstellation, )); @@ -1813,6 +2067,10 @@ impl IOCompositor { // and check if it is the one layout is expecting, let epoch = Epoch(epoch); if *pending_epoch != epoch { + warn!( + "{}: paint metrics: pending {:?} should be {:?}", + id, pending_epoch, epoch + ); continue; } // in which case, we remove it from the list of pending metrics, @@ -1917,14 +2175,14 @@ impl IOCompositor { }, }; - // Nottify embedder that servo is ready to present. + // Notify embedder that servo is ready to present. // Embedder should call `present` to tell compositor to continue rendering. self.waiting_on_present = true; - let msg = ConstellationMsg::ReadyToPresent( - self.root_content_pipeline.top_level_browsing_context_id, - ); - if let Err(e) = self.constellation_chan.send(msg) { - warn!("Sending event to constellation failed ({:?}).", e); + for (&top_level_browsing_context_id, _) in self.webviews.painting_order() { + let msg = ConstellationMsg::ReadyToPresent(top_level_browsing_context_id); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Sending event to constellation failed ({:?}).", e); + } } self.composition_request = CompositionRequest::NoCompositingNecessary; @@ -1969,14 +2227,6 @@ impl IOCompositor { self.assert_gl_framebuffer_complete(); // Set the viewport background based on prefs. - let viewport = self.embedder_coordinates.get_flipped_viewport(); - gl.scissor( - viewport.origin.x, - viewport.origin.y, - viewport.size.width, - viewport.size.height, - ); - let color = servo_config::pref!(shell.background_color.rgba); gl.clear_color( color[0] as f32, @@ -1984,9 +2234,23 @@ impl IOCompositor { color[2] as f32, color[3] as f32, ); - gl.enable(gleam::gl::SCISSOR_TEST); - gl.clear(gleam::gl::COLOR_BUFFER_BIT); - gl.disable(gleam::gl::SCISSOR_TEST); + + // Clear the viewport rect of each top-level browsing context. + for (_, webview) in self.webviews.painting_order() { + let rect = self + .embedder_coordinates + .flipped_rect(&webview.rect.to_i32()); + gl.scissor( + rect.origin.x, + rect.origin.y, + rect.size.width, + rect.size.height, + ); + gl.enable(gleam::gl::SCISSOR_TEST); + gl.clear(gleam::gl::COLOR_BUFFER_BIT); + gl.disable(gleam::gl::SCISSOR_TEST); + } + self.assert_gl_framebuffer_complete(); } diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index a3ae2b48b3b..4f0573d16a7 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -18,6 +18,7 @@ pub use crate::compositor::{CompositeTarget, IOCompositor, ShutdownState}; mod compositor; mod gl; mod touch; +mod webview; pub mod windowing; /// Data used to construct a compositor. diff --git a/components/compositing/webview.rs b/components/compositing/webview.rs new file mode 100644 index 00000000000..6fc3eb217eb --- /dev/null +++ b/components/compositing/webview.rs @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; + +use msg::constellation_msg::TopLevelBrowsingContextId; + +#[derive(Debug)] +pub struct WebViewManager { + /// Our top-level browsing contexts. In the WebRender scene, their pipelines are the children of + /// a single root pipeline that also applies any pinch zoom transformation. + webviews: HashMap, + + /// The order to paint them in, topmost last. + painting_order: Vec, +} + +impl Default for WebViewManager { + fn default() -> Self { + Self { + webviews: Default::default(), + painting_order: Default::default(), + } + } +} + +impl WebViewManager { + pub fn add( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + webview: WebView, + ) { + debug_assert!(!self.webviews.contains_key(&top_level_browsing_context_id)); + self.webviews.insert(top_level_browsing_context_id, webview); + } + + pub fn remove( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + ) -> Option { + self.painting_order + .retain(|b| *b != top_level_browsing_context_id); + self.webviews.remove(&top_level_browsing_context_id) + } + + pub fn get( + &self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + ) -> Option<&WebView> { + self.webviews.get(&top_level_browsing_context_id) + } + + pub fn get_mut( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + ) -> Option<&mut WebView> { + self.webviews.get_mut(&top_level_browsing_context_id) + } + + pub fn show(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) { + debug_assert!(self.webviews.contains_key(&top_level_browsing_context_id)); + if !self.painting_order.contains(&top_level_browsing_context_id) { + self.painting_order.push(top_level_browsing_context_id); + } + } + + pub fn hide(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) { + debug_assert!(self.webviews.contains_key(&top_level_browsing_context_id)); + self.painting_order + .retain(|b| *b != top_level_browsing_context_id); + } + + pub fn raise_to_top(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) { + self.hide(top_level_browsing_context_id); + self.show(top_level_browsing_context_id); + } + + pub fn painting_order(&self) -> impl Iterator { + self.painting_order + .iter() + .flat_map(move |browser_id| self.webviews.get(browser_id).map(|b| (browser_id, b))) + } +} + +#[cfg(test)] +mod test { + use std::num::NonZeroU32; + + use msg::constellation_msg::{ + BrowsingContextId, BrowsingContextIndex, PipelineNamespace, PipelineNamespaceId, + TopLevelBrowsingContextId, + }; + + use crate::webview::WebViewManager; + + fn top_level_id(namespace_id: u32, index: u32) -> TopLevelBrowsingContextId { + TopLevelBrowsingContextId(BrowsingContextId { + namespace_id: PipelineNamespaceId(namespace_id), + index: BrowsingContextIndex(NonZeroU32::new(index).unwrap()), + }) + } + + fn webviews_sorted( + webviews: &WebViewManager, + ) -> Vec<(TopLevelBrowsingContextId, WebView)> { + let mut keys = webviews.webviews.keys().collect::>(); + keys.sort(); + keys.iter() + .map(|&id| (*id, webviews.webviews.get(id).cloned().unwrap())) + .collect() + } + + #[test] + fn test() { + PipelineNamespace::install(PipelineNamespaceId(0)); + let mut browsers = WebViewManager::default(); + + // add() adds the browser to the map, but not the painting order. + browsers.add(TopLevelBrowsingContextId::new(), 'a'); + browsers.add(TopLevelBrowsingContextId::new(), 'b'); + browsers.add(TopLevelBrowsingContextId::new(), 'c'); + assert_eq!( + webviews_sorted(&browsers), + vec![ + (top_level_id(0, 1), 'a'), + (top_level_id(0, 2), 'b'), + (top_level_id(0, 3), 'c'), + ] + ); + assert!(browsers.painting_order.is_empty()); + + // For browsers not yet visible, both show() and raise_to_top() add the given browser on top. + browsers.show(top_level_id(0, 2)); + assert_eq!(browsers.painting_order, vec![top_level_id(0, 2)]); + browsers.raise_to_top(top_level_id(0, 1)); + assert_eq!( + browsers.painting_order, + vec![top_level_id(0, 2), top_level_id(0, 1)] + ); + browsers.show(top_level_id(0, 3)); + assert_eq!( + browsers.painting_order, + vec![top_level_id(0, 2), top_level_id(0, 1), top_level_id(0, 3)] + ); + + // For browsers already visible, show() does nothing, while raise_to_top() makes it on top. + browsers.show(top_level_id(0, 1)); + assert_eq!( + browsers.painting_order, + vec![top_level_id(0, 2), top_level_id(0, 1), top_level_id(0, 3)] + ); + browsers.raise_to_top(top_level_id(0, 1)); + assert_eq!( + browsers.painting_order, + vec![top_level_id(0, 2), top_level_id(0, 3), top_level_id(0, 1)] + ); + + // hide() removes the browser from the painting order, but not the map. + browsers.hide(top_level_id(0, 3)); + assert_eq!( + browsers.painting_order, + vec![top_level_id(0, 2), top_level_id(0, 1)] + ); + assert_eq!( + webviews_sorted(&browsers), + vec![ + (top_level_id(0, 1), 'a'), + (top_level_id(0, 2), 'b'), + (top_level_id(0, 3), 'c'), + ] + ); + + // painting_order() returns only the visible browsers, in painting order. + let mut painting_order = browsers.painting_order(); + assert_eq!(painting_order.next(), Some((&top_level_id(0, 2), &'b'))); + assert_eq!(painting_order.next(), Some((&top_level_id(0, 1), &'a'))); + assert_eq!(painting_order.next(), None); + drop(painting_order); + + // remove() removes the given browser from both the map and the painting order. + browsers.remove(top_level_id(0, 1)); + browsers.remove(top_level_id(0, 2)); + browsers.remove(top_level_id(0, 3)); + assert!(webviews_sorted(&browsers).is_empty()); + assert!(browsers.painting_order.is_empty()); + } +} diff --git a/components/compositing/windowing.rs b/components/compositing/windowing.rs index a1a36ee688d..4662d7c62f5 100644 --- a/components/compositing/windowing.rs +++ b/components/compositing/windowing.rs @@ -19,7 +19,7 @@ use script_traits::{ use servo_geometry::DeviceIndependentPixel; use servo_url::ServoUrl; use style_traits::DevicePixel; -use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint}; +use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceRect}; use webrender_api::ScrollLocation; #[derive(Clone)] @@ -51,7 +51,7 @@ pub enum EmbedderEvent { /// message, the window must make the same GL context as in `PrepareRenderingEvent` current. Refresh, /// Sent when the window is resized. - Resize, + WindowResize, /// Sent when a navigation request from script is allowed/refused. AllowNavigationResponse(PipelineId, bool), /// Sent when a new URL is to be loaded. @@ -89,9 +89,19 @@ pub enum EmbedderEvent { CloseWebView(TopLevelBrowsingContextId), /// Panic a top level browsing context. SendError(Option, String), + /// Make a top-level browsing context visible. + MoveResizeWebView(TopLevelBrowsingContextId, DeviceRect), + /// Make a top-level browsing context visible. + ShowWebView(TopLevelBrowsingContextId), + /// Make a top-level browsing context invisible. + HideWebView(TopLevelBrowsingContextId), + /// Make a top-level browsing context visible and paint on top of all others. + RaiseWebViewToTop(TopLevelBrowsingContextId), /// Make a top level browsing context visible, hiding the previous /// visible one. FocusWebView(TopLevelBrowsingContextId), + /// Make none of the top-level browsing contexts focused. + BlurWebView, /// Toggles a debug flag in WebRender ToggleWebRenderDebug(WebRenderDebugOption), /// Capture current WebRender @@ -124,7 +134,7 @@ impl Debug for EmbedderEvent { match *self { EmbedderEvent::Idle => write!(f, "Idle"), EmbedderEvent::Refresh => write!(f, "Refresh"), - EmbedderEvent::Resize => write!(f, "Resize"), + EmbedderEvent::WindowResize => write!(f, "Resize"), EmbedderEvent::Keyboard(..) => write!(f, "Keyboard"), EmbedderEvent::AllowNavigationResponse(..) => write!(f, "AllowNavigationResponse"), EmbedderEvent::LoadUrl(..) => write!(f, "LoadUrl"), @@ -142,7 +152,12 @@ impl Debug for EmbedderEvent { EmbedderEvent::NewWebView(..) => write!(f, "NewWebView"), EmbedderEvent::SendError(..) => write!(f, "SendError"), EmbedderEvent::CloseWebView(..) => write!(f, "CloseWebView"), + EmbedderEvent::MoveResizeWebView(..) => write!(f, "MoveResizeWebView"), + EmbedderEvent::ShowWebView(..) => write!(f, "ShowWebView"), + EmbedderEvent::HideWebView(..) => write!(f, "HideWebView"), + EmbedderEvent::RaiseWebViewToTop(..) => write!(f, "RaiseWebViewToTop"), EmbedderEvent::FocusWebView(..) => write!(f, "FocusWebView"), + EmbedderEvent::BlurWebView => write!(f, "BlurWebView"), EmbedderEvent::ToggleWebRenderDebug(..) => write!(f, "ToggleWebRenderDebug"), EmbedderEvent::CaptureWebRender => write!(f, "CaptureWebRender"), EmbedderEvent::ToggleSamplingProfiler(..) => write!(f, "ToggleSamplingProfiler"), @@ -211,15 +226,21 @@ pub struct EmbedderCoordinates { impl EmbedderCoordinates { /// Get the unflipped viewport rectangle for use with the WebRender API. pub fn get_viewport(&self) -> DeviceIntRect { - DeviceIntRect::from_untyped(&self.viewport.to_untyped()) + self.viewport.clone() } - /// Get the flipped viewport rectangle. This should be used when drawing directly - /// to the framebuffer with OpenGL commands. - pub fn get_flipped_viewport(&self) -> DeviceIntRect { + /// Flip the given rect. + /// This should be used when drawing directly to the framebuffer with OpenGL commands. + pub fn flipped_rect(&self, rect: &DeviceIntRect) -> DeviceIntRect { let fb_height = self.framebuffer.height; - let mut view = self.viewport.clone(); - view.origin.y = fb_height - view.origin.y - view.size.height; - DeviceIntRect::from_untyped(&view.to_untyped()) + let mut result = rect.clone(); + result.origin.y = fb_height - result.origin.y - result.size.height; + result + } + + /// Get the flipped viewport rectangle. + /// This should be used when drawing directly to the framebuffer with OpenGL commands. + pub fn get_flipped_viewport(&self) -> DeviceIntRect { + self.flipped_rect(&self.get_viewport()) } } diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 1acd379d7bf..e7076741678 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -1477,6 +1477,70 @@ where } self.handle_panic(top_level_browsing_context_id, error, None); }, + FromCompositorMsg::MoveResizeWebView(top_level_browsing_context_id, rect) => { + if self.webviews.get(top_level_browsing_context_id).is_none() { + return warn!( + "{}: MoveResizeWebView on unknown top-level browsing context", + top_level_browsing_context_id + ); + } + self.compositor_proxy.send(CompositorMsg::MoveResizeWebView( + top_level_browsing_context_id, + rect, + )); + }, + FromCompositorMsg::ShowWebView(top_level_browsing_context_id) => { + if self.webviews.get(top_level_browsing_context_id).is_none() { + return warn!( + "{}: ShowWebView on unknown top-level browsing context", + top_level_browsing_context_id + ); + } + self.compositor_proxy + .send(CompositorMsg::ShowWebView(top_level_browsing_context_id)); + self.webviews + .set_webview_visibility(top_level_browsing_context_id, true); + self.notify_webview_visibility( + top_level_browsing_context_id, + self.webviews + .is_effectively_visible(top_level_browsing_context_id), + ); + }, + FromCompositorMsg::HideWebView(top_level_browsing_context_id) => { + if self.webviews.get(top_level_browsing_context_id).is_none() { + return warn!( + "{}: HideWebView on unknown top-level browsing context", + top_level_browsing_context_id + ); + } + self.compositor_proxy + .send(CompositorMsg::HideWebView(top_level_browsing_context_id)); + self.webviews + .set_webview_visibility(top_level_browsing_context_id, false); + self.notify_webview_visibility( + top_level_browsing_context_id, + self.webviews + .is_effectively_visible(top_level_browsing_context_id), + ); + }, + FromCompositorMsg::RaiseWebViewToTop(top_level_browsing_context_id) => { + if self.webviews.get(top_level_browsing_context_id).is_none() { + return warn!( + "{}: RaiseWebViewToTop on unknown top-level browsing context", + top_level_browsing_context_id + ); + } + self.compositor_proxy.send(CompositorMsg::RaiseWebViewToTop( + top_level_browsing_context_id, + )); + self.webviews + .set_webview_visibility(top_level_browsing_context_id, true); + self.notify_webview_visibility( + top_level_browsing_context_id, + self.webviews + .is_effectively_visible(top_level_browsing_context_id), + ); + }, FromCompositorMsg::FocusWebView(top_level_browsing_context_id) => { if self.webviews.get(top_level_browsing_context_id).is_none() { return warn!("{top_level_browsing_context_id}: FocusWebView on unknown top-level browsing context"); @@ -1487,7 +1551,7 @@ where EmbedderMsg::WebViewFocused(top_level_browsing_context_id), )); if !cfg!(feature = "multiview") { - self.update_frame_tree_if_focused(top_level_browsing_context_id); + self.update_webview(top_level_browsing_context_id); } }, FromCompositorMsg::BlurWebView => { @@ -1543,7 +1607,16 @@ where self.handle_media_session_action_msg(action); }, FromCompositorMsg::WebViewVisibilityChanged(webview_id, visible) => { + // TODO is this correct? + self.webviews.set_native_window_visibility(visible); self.notify_webview_visibility(webview_id, visible); + let webviews = self.webviews.iter().map(|(&id, _)| id).collect::>(); + for top_level_browsing_context_id in webviews { + let visible = self + .webviews + .is_effectively_visible(top_level_browsing_context_id); + self.notify_webview_visibility(top_level_browsing_context_id, visible); + } }, FromCompositorMsg::ReadyToPresent(top_level_browsing_context_id) => { self.embedder_proxy.send(( @@ -1554,6 +1627,10 @@ where FromCompositorMsg::Gamepad(gamepad_event) => { self.handle_gamepad_msg(gamepad_event); }, + FromCompositorMsg::WebViewPaintingOrder(browser_ids) => { + self.embedder_proxy + .send((None, EmbedderMsg::WebViewPaintingOrder(browser_ids))); + }, } } @@ -3032,7 +3109,8 @@ where .send((None, EmbedderMsg::WebViewBlurred)); } self.webviews.remove(top_level_browsing_context_id); - // TODO Send the compositor a RemoveWebView event. + self.compositor_proxy + .send(CompositorMsg::RemoveWebView(top_level_browsing_context_id)); self.embedder_proxy.send(( Some(top_level_browsing_context_id), EmbedderMsg::WebViewClosed(top_level_browsing_context_id), @@ -3820,7 +3898,7 @@ where self.notify_history_changed(top_level_browsing_context_id); self.trim_history(top_level_browsing_context_id); - self.update_frame_tree_if_focused(top_level_browsing_context_id); + self.update_webview(top_level_browsing_context_id); } fn update_browsing_context( @@ -4783,7 +4861,7 @@ where } self.notify_history_changed(change.top_level_browsing_context_id); - self.update_frame_tree_if_focused(change.top_level_browsing_context_id); + self.update_webview(change.top_level_browsing_context_id); } fn focused_browsing_context_is_descendant_of( @@ -5393,17 +5471,18 @@ where }) } - /// Send the frame tree for the given webview to the compositor. - fn update_frame_tree_if_focused( - &mut self, - top_level_browsing_context_id: TopLevelBrowsingContextId, - ) { - // Only send the frame tree if the given webview is focused. - if let Some(focused_webview_id) = self.webviews.focused_webview().map(|(id, _)| id) { - if top_level_browsing_context_id != focused_webview_id { - return; + /// Send the frame tree for the given web view to the compositor. + fn update_webview(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) { + if !cfg!(feature = "multiview") { + if let Some(focused_browser_id) = self.webviews.focused_webview().map(|(id, _)| id) { + if top_level_browsing_context_id != focused_browser_id { + return; + } + } else { + self.webviews.focus(top_level_browsing_context_id); } } + // Note that this function can panic, due to ipc-channel creation failure. // avoiding this panic would require a mechanism for dealing // with low-resource scenarios. @@ -5411,7 +5490,7 @@ where if let Some(frame_tree) = self.browsing_context_to_sendable(browsing_context_id) { debug!("{}: Sending frame tree", browsing_context_id); self.compositor_proxy - .send(CompositorMsg::SetFrameTree(frame_tree)); + .send(CompositorMsg::UpdateWebView(frame_tree)); } } diff --git a/components/constellation/webview.rs b/components/constellation/webview.rs index cb2b85de468..9ae53170b39 100644 --- a/components/constellation/webview.rs +++ b/components/constellation/webview.rs @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use msg::constellation_msg::TopLevelBrowsingContextId; @@ -17,6 +17,12 @@ pub struct WebViewManager { /// Whether the latest webview in focus order is currently focused. is_focused: bool, + + /// The web view that would be visible in a containing native window. + visible_webviews: HashSet, + + /// Whether our native window is visible, or true if there is no such window. + native_window_is_visible: bool, } impl Default for WebViewManager { @@ -25,6 +31,8 @@ impl Default for WebViewManager { webviews: HashMap::default(), focus_order: Vec::default(), is_focused: false, + visible_webviews: HashSet::default(), + native_window_is_visible: true, } } } @@ -93,6 +101,38 @@ impl WebViewManager { pub fn unfocus(&mut self) { self.is_focused = false; } + + pub fn set_webview_visibility( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + visible: bool, + ) { + debug_assert!(self.webviews.contains_key(&top_level_browsing_context_id)); + if visible { + self.visible_webviews.insert(top_level_browsing_context_id); + } else { + self.visible_webviews.remove(&top_level_browsing_context_id); + } + } + + pub fn set_native_window_visibility(&mut self, visible: bool) { + self.native_window_is_visible = visible; + } + + /// Returns true iff the browser is visible and the native window is visible. + pub fn is_effectively_visible( + &self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + ) -> bool { + debug_assert!(self.webviews.contains_key(&top_level_browsing_context_id)); + self.native_window_is_visible && + self.visible_webviews + .contains(&top_level_browsing_context_id) + } + + pub fn iter(&self) -> impl Iterator { + self.webviews.iter() + } } #[cfg(test)] @@ -185,6 +225,26 @@ mod test { ); assert_eq!(webviews.is_focused, true); + // is_effectively_visible() checks that the given browser is visible. + webviews.set_webview_visibility(top_level_id(0, 1), true); + webviews.set_webview_visibility(top_level_id(0, 3), true); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 1)), true); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 2)), false); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 3)), true); + + // is_effectively_visible() checks that the native window is visible. + webviews.set_native_window_visibility(false); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 1)), false); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 2)), false); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 3)), false); + + // set_native_window_visibility() does not destroy or prevent changes to browser visibility state. + webviews.set_webview_visibility(top_level_id(0, 1), false); + webviews.set_native_window_visibility(true); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 1)), false); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 2)), false); + assert_eq!(webviews.is_effectively_visible(top_level_id(0, 3)), true); + // remove() clears the “is focused” flag iff the given webview was focused. webviews.remove(top_level_id(0, 2)); assert_eq!(webviews.is_focused, true); diff --git a/components/servo/lib.rs b/components/servo/lib.rs index c21c6af4ede..4e89a572af2 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -226,6 +226,21 @@ pub struct InitializedServo { pub browser_id: TopLevelBrowsingContextId, } +/// Utility to help forward embedder event to constellation. +macro_rules! forward_to_constellation { + ($self:ident, $variant:ident $(($($rest:tt),*))?) => { + { + let msg = ConstellationMsg::$variant$(($($rest),*))?; + if let Err(e) = $self.constellation_chan.send(msg) { + warn!( + "Sending {} message to constellation failed ({:?}).", + stringify!($variant), e, + ); + } + } + }; +} + impl Servo where Window: WindowMethods + 'static + ?Sized, @@ -599,7 +614,7 @@ where self.compositor.composite(); }, - EmbedderEvent::Resize => { + EmbedderEvent::WindowResize => { return self.compositor.on_resize_window_event(); }, EmbedderEvent::InvalidateNativeSurface => { @@ -762,6 +777,21 @@ where } }, + EmbedderEvent::MoveResizeWebView(top_level_browsing_context_id, rect) => { + self.compositor + .move_resize_webview(top_level_browsing_context_id, rect); + }, + EmbedderEvent::ShowWebView(top_level_browsing_context_id) => { + forward_to_constellation!(self, ShowWebView(top_level_browsing_context_id)) + }, + EmbedderEvent::HideWebView(top_level_browsing_context_id) => { + forward_to_constellation!(self, HideWebView(top_level_browsing_context_id)) + }, + EmbedderEvent::RaiseWebViewToTop(top_level_browsing_context_id) => { + forward_to_constellation!(self, RaiseWebViewToTop(top_level_browsing_context_id)) + }, + EmbedderEvent::BlurWebView => forward_to_constellation!(self, BlurWebView), + EmbedderEvent::SendError(top_level_browsing_context_id, e) => { let msg = ConstellationMsg::SendError(top_level_browsing_context_id, e); if let Err(e) = self.constellation_chan.send(msg) { @@ -883,13 +913,9 @@ where self.compositor.present(); } - pub fn recomposite(&mut self) { - self.compositor.composite(); - } - /// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to /// [`CompositeTarget::Fbo`], or None otherwise. - pub fn offscreen_framebuffer_id(&self) -> Option { + pub fn output_framebuffer_id(&self) -> Option { self.compositor.offscreen_framebuffer_id() } } diff --git a/components/shared/compositing/constellation_msg.rs b/components/shared/compositing/constellation_msg.rs index 263d0f510a8..9261b3833fd 100644 --- a/components/shared/compositing/constellation_msg.rs +++ b/components/shared/compositing/constellation_msg.rs @@ -18,6 +18,7 @@ use script_traits::{ WebDriverCommandMsg, WindowSizeData, WindowSizeType, }; use servo_url::ServoUrl; +use webrender_api::units::DeviceRect; /// Messages to the constellation. pub enum ConstellationMsg { @@ -60,6 +61,14 @@ pub enum ConstellationMsg { CloseWebView(TopLevelBrowsingContextId), /// Panic a top level browsing context. SendError(Option, String), + /// Make a top-level browsing context visible. + MoveResizeWebView(TopLevelBrowsingContextId, DeviceRect), + /// Make a top-level browsing context visible. + ShowWebView(TopLevelBrowsingContextId), + /// Make a top-level browsing context invisible. + HideWebView(TopLevelBrowsingContextId), + /// Make a top-level browsing context visible and paint on top of all others. + RaiseWebViewToTop(TopLevelBrowsingContextId), /// Make a top-level browsing context focused. FocusWebView(TopLevelBrowsingContextId), /// Make none of the top-level browsing contexts focused. @@ -80,10 +89,12 @@ pub enum ConstellationMsg { WebViewVisibilityChanged(TopLevelBrowsingContextId, bool), /// Virtual keyboard was dismissed IMEDismissed, - /// Compositing done, but external code needs to present. + /// Notify the embedder that it needs to present a new frame. ReadyToPresent(TopLevelBrowsingContextId), /// Gamepad state has changed Gamepad(GamepadEvent), + /// Notify the embedder of an updated browser painting order. + WebViewPaintingOrder(Vec), } impl fmt::Debug for ConstellationMsg { @@ -106,6 +117,10 @@ impl fmt::Debug for ConstellationMsg { LogEntry(..) => "LogEntry", NewWebView(..) => "NewWebView", CloseWebView(..) => "CloseWebView", + MoveResizeWebView(..) => "MoveResizeWebView", + ShowWebView(..) => "ShowWebView", + HideWebView(..) => "HideWebView", + RaiseWebViewToTop(..) => "RaiseWebViewToTop", FocusWebView(..) => "FocusWebView", BlurWebView => "BlurWebView", SendError(..) => "SendError", @@ -120,6 +135,7 @@ impl fmt::Debug for ConstellationMsg { ClearCache => "ClearCache", ReadyToPresent(..) => "ReadyToPresent", Gamepad(..) => "Gamepad", + WebViewPaintingOrder(..) => "WebViewPaintingOrder", }; write!(formatter, "ConstellationMsg::{}", variant) } diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index b3810344439..d9e873ab1c0 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -24,7 +24,7 @@ use script_traits::{ ScriptToCompositorMsg, }; use style_traits::CSSPixel; -use webrender_api::units::{DeviceIntPoint, DeviceIntSize}; +use webrender_api::units::{DeviceIntPoint, DeviceIntSize, DeviceRect}; use webrender_api::{self, FontInstanceKey, FontKey, ImageKey}; /// Why we performed a composite. This is used for debugging. @@ -106,8 +106,18 @@ pub enum CompositorMsg { ShutdownComplete, /// Alerts the compositor that the given pipeline has changed whether it is running animations. ChangeRunningAnimationsState(PipelineId, AnimationState), - /// Replaces the current frame tree, typically called during main frame navigation. - SetFrameTree(SendableFrameTree), + /// Add or update a web view, given its frame tree. + UpdateWebView(SendableFrameTree), + /// Remove a web view. + RemoveWebView(TopLevelBrowsingContextId), + /// Make a web view visible. + MoveResizeWebView(TopLevelBrowsingContextId, DeviceRect), + /// Make a web view visible. + ShowWebView(TopLevelBrowsingContextId), + /// Make a web view invisible. + HideWebView(TopLevelBrowsingContextId), + /// Make a web view visible and paint on top of all others. + RaiseWebViewToTop(TopLevelBrowsingContextId), /// Composite. Recomposite(CompositingReason), /// Script has handled a touch event, and either prevented or allowed default actions. @@ -192,7 +202,12 @@ impl Debug for CompositorMsg { CompositorMsg::ChangeRunningAnimationsState(_, state) => { write!(f, "ChangeRunningAnimationsState({:?})", state) }, - CompositorMsg::SetFrameTree(..) => write!(f, "SetFrameTree"), + CompositorMsg::UpdateWebView(..) => write!(f, "UpdateWebView"), + CompositorMsg::RemoveWebView(..) => write!(f, "RemoveWebView"), + CompositorMsg::MoveResizeWebView(..) => write!(f, "MoveResizeWebView"), + CompositorMsg::ShowWebView(..) => write!(f, "ShowWebView"), + CompositorMsg::HideWebView(..) => write!(f, "HideWebView"), + CompositorMsg::RaiseWebViewToTop(..) => write!(f, "RaiseWebViewToTop"), CompositorMsg::Recomposite(..) => write!(f, "Recomposite"), CompositorMsg::TouchEventProcessed(..) => write!(f, "TouchEventProcessed"), CompositorMsg::CreatePng(..) => write!(f, "CreatePng"), diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index a03993359c4..01878fa6713 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -156,14 +156,16 @@ pub enum EmbedderMsg { AllowNavigationRequest(PipelineId, ServoUrl), /// Whether or not to allow script to open a new tab/browser AllowOpeningWebView(IpcSender), - /// A browser was created + /// A web view was created WebViewOpened(TopLevelBrowsingContextId), - /// A browser was destroyed + /// A web view was destroyed WebViewClosed(TopLevelBrowsingContextId), - /// A browser gained focus for keyboard events + /// A web view gained focus for keyboard events WebViewFocused(TopLevelBrowsingContextId), - /// All browsers lost focus for keyboard events + /// All web views lost focus for keyboard events WebViewBlurred, + /// The order to paint the web views + WebViewPaintingOrder(Vec), /// Wether or not to unload a document AllowUnload(IpcSender), /// Sends an unconsumed key event back to the embedder. @@ -262,6 +264,7 @@ impl Debug for EmbedderMsg { EmbedderMsg::WebViewClosed(..) => write!(f, "WebViewClosed"), EmbedderMsg::WebViewFocused(..) => write!(f, "WebViewFocused"), EmbedderMsg::WebViewBlurred => write!(f, "WebViewUnfocused"), + EmbedderMsg::WebViewPaintingOrder(..) => write!(f, "WebViewPaintingOrder"), EmbedderMsg::ReportProfile(..) => write!(f, "ReportProfile"), EmbedderMsg::MediaSessionEvent(..) => write!(f, "MediaSessionEvent"), EmbedderMsg::OnDevtoolsStarted(..) => write!(f, "OnDevtoolsStarted"), diff --git a/ports/jniapi/src/simpleservo.rs b/ports/jniapi/src/simpleservo.rs index 3238eb182d1..3be21918661 100644 --- a/ports/jniapi/src/simpleservo.rs +++ b/ports/jniapi/src/simpleservo.rs @@ -435,7 +435,7 @@ impl ServoGlue { pub fn resize(&mut self, coordinates: Coordinates) -> Result<(), &'static str> { info!("resize"); *self.callbacks.coordinates.borrow_mut() = coordinates; - self.process_event(EmbedderEvent::Resize) + self.process_event(EmbedderEvent::WindowResize) } /// Start scrolling. @@ -831,7 +831,8 @@ impl ServoGlue { EmbedderMsg::HeadParsed | EmbedderMsg::SetFullscreenState(..) | EmbedderMsg::ReportProfile(..) | - EmbedderMsg::EventDelivered(..) => {}, + EmbedderMsg::EventDelivered(..) | + EmbedderMsg::WebViewPaintingOrder(..) => {}, } } diff --git a/ports/servoshell/app.rs b/ports/servoshell/app.rs index 099b613ea62..d7757781f74 100644 --- a/ports/servoshell/app.rs +++ b/ports/servoshell/app.rs @@ -126,17 +126,15 @@ impl App { if let Some(mut minibrowser) = app.minibrowser() { // Servo is not yet initialised, so there is no `servo_framebuffer_id`. - minibrowser.update(window.winit_window().unwrap(), None, "init"); + minibrowser.update( + window.winit_window().unwrap(), + &mut app.webviews.borrow_mut(), + None, + "init", + ); window.set_toolbar_height(minibrowser.toolbar_height); } - // Whether or not to recomposite during the next RedrawRequested event. - // Normally this is true, including for RedrawRequested events that come from the platform - // (e.g. X11 without picom or similar) when an offscreen or obscured window becomes visible. - // If we are calling request_redraw in response to the compositor having painted to this - // frame, set this to false, so we can avoid an unnecessary recomposite. - let mut need_recomposite = true; - let t_start = Instant::now(); let mut t = t_start; let ev_waker = events_loop.create_event_loop_waker(); @@ -226,22 +224,17 @@ impl App { // WARNING: do not defer painting or presenting to some later tick of the event // loop or servoshell may become unresponsive! (servo#30312) - if need_recomposite { - trace!("need_recomposite"); - app.servo.as_mut().unwrap().recomposite(); - } if let Some(mut minibrowser) = app.minibrowser() { minibrowser.update( window.winit_window().unwrap(), - app.servo.as_ref().unwrap().offscreen_framebuffer_id(), + &mut app.webviews.borrow_mut(), + app.servo.as_ref().unwrap().output_framebuffer_id(), "RedrawRequested", ); minibrowser.paint(window.winit_window().unwrap()); } - app.servo.as_mut().unwrap().present(); - // By default, the next RedrawRequested event will need to recomposite. - need_recomposite = true; + app.servo.as_mut().unwrap().present(); } // Handle the event @@ -284,7 +277,8 @@ impl App { _ => {}, } } - if !consumed { + // TODO decide how to prioritise events between egui and servo + if true || !consumed { app.queue_embedder_events_for_winit_event(event); } @@ -324,7 +318,8 @@ impl App { // redraw, doing so would delay the location update by two frames. minibrowser.update( window.winit_window().unwrap(), - app.servo.as_ref().unwrap().offscreen_framebuffer_id(), + webviews, + app.servo.as_ref().unwrap().output_framebuffer_id(), "update_location_in_toolbar", ); } @@ -335,14 +330,14 @@ impl App { // The window was resized. trace!("PumpResult::Present::Immediate"); - // Resizes are unusual in that we need to repaint synchronously. - // TODO(servo#30049) can we replace this with the simpler Servo::recomposite? - app.servo.as_mut().unwrap().repaint_synchronously(); - + // If we had resized any of the viewports in response to this, we would need to + // call Servo::repaint_synchronously. At the moment we don’t, so there won’t be + // any paint scheduled, and calling it would hang the compositor forever. if let Some(mut minibrowser) = app.minibrowser() { minibrowser.update( window.winit_window().unwrap(), - app.servo.as_ref().unwrap().offscreen_framebuffer_id(), + &mut app.webviews.borrow_mut(), + app.servo.as_ref().unwrap().output_framebuffer_id(), "PumpResult::Present::Immediate", ); minibrowser.paint(window.winit_window().unwrap()); @@ -360,10 +355,6 @@ impl App { } else { app.servo.as_mut().unwrap().present(); } - - // We don’t need the compositor to paint to this frame during the redraw event. - // TODO(servo#30331) broken on macOS? - // need_recomposite = false; }, Present::None => {}, } diff --git a/ports/servoshell/headed_window.rs b/ports/servoshell/headed_window.rs index 756e8531267..9f0ba38dbbd 100644 --- a/ports/servoshell/headed_window.rs +++ b/ports/servoshell/headed_window.rs @@ -8,7 +8,6 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::rc::Rc; -use euclid::num::Zero; use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D}; use log::{debug, info, trace}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; @@ -399,9 +398,7 @@ impl WindowPortsMethods for Window { } }, winit::event::WindowEvent::CursorMoved { position, .. } => { - let toolbar_height = self.toolbar_height.get() * self.hidpi_factor(); - let mut position = winit_position_to_euclid_point(position).to_f32(); - position -= Size2D::from_lengths(Length::zero(), toolbar_height); + let position = winit_position_to_euclid_point(position); self.mouse_pos.set(position.to_i32()); self.event_queue .borrow_mut() @@ -476,7 +473,9 @@ impl WindowPortsMethods for Window { .resize(physical_size.to_i32()) .expect("Failed to resize"); self.inner_size.set(new_size); - self.event_queue.borrow_mut().push(EmbedderEvent::Resize); + self.event_queue + .borrow_mut() + .push(EmbedderEvent::WindowResize); } }, _ => {}, @@ -510,6 +509,10 @@ impl WindowPortsMethods for Window { Some(&self.winit_window) } + fn toolbar_height(&self) -> Length { + self.toolbar_height.get() + } + fn set_toolbar_height(&self, height: Length) { self.toolbar_height.set(height); } @@ -520,13 +523,8 @@ impl WindowMethods for Window { let window_size = winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32(); let window_origin = self.winit_window.outer_position().unwrap_or_default(); let window_origin = winit_position_to_euclid_point(window_origin).to_i32(); - let inner_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32(); - - // Subtract the minibrowser toolbar height if any - let toolbar_height = self.toolbar_height.get() * self.hidpi_factor(); - let viewport_size = inner_size - Size2D::from_lengths(Length::zero(), toolbar_height); - let viewport_origin = DeviceIntPoint::zero(); // bottom left + let viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32(); let viewport = DeviceIntRect::new(viewport_origin, viewport_size.to_i32()); let screen = self.screen_size.to_i32(); diff --git a/ports/servoshell/headless_window.rs b/ports/servoshell/headless_window.rs index 308a85120ce..fe040d62204 100644 --- a/ports/servoshell/headless_window.rs +++ b/ports/servoshell/headless_window.rs @@ -8,6 +8,7 @@ use std::cell::Cell; use std::rc::Rc; use std::sync::RwLock; +use euclid::num::Zero; use euclid::{Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector3D}; use log::warn; use servo::compositing::windowing::{ @@ -92,7 +93,7 @@ impl WindowPortsMethods for Window { Ok(()) => { self.inner_size.set(new_size); if let Ok(ref mut queue) = self.event_queue.write() { - queue.push(EmbedderEvent::Resize); + queue.push(EmbedderEvent::WindowResize); } }, Err(error) => warn!("Could not resize window: {error:?}"), @@ -147,6 +148,10 @@ impl WindowPortsMethods for Window { None } + fn toolbar_height(&self) -> Length { + Length::zero() + } + fn set_toolbar_height(&self, _height: Length) { unimplemented!("headless Window only") } diff --git a/ports/servoshell/minibrowser.rs b/ports/servoshell/minibrowser.rs index f5727baf438..498a813456f 100644 --- a/ports/servoshell/minibrowser.rs +++ b/ports/servoshell/minibrowser.rs @@ -7,10 +7,12 @@ use std::num::NonZeroU32; use std::sync::Arc; use std::time::Instant; -use egui::{CentralPanel, Frame, InnerResponse, Key, Modifiers, PaintCallback, TopBottomPanel}; +use egui::{ + CentralPanel, Frame, InnerResponse, Key, Modifiers, PaintCallback, Pos2, TopBottomPanel, Vec2, +}; use egui_glow::CallbackFn; use egui_winit::EventResponse; -use euclid::{Length, Point2D, Scale}; +use euclid::{Length, Point2D, Rect, Scale, Size2D}; use gleam::gl; use glow::NativeFramebuffer; use log::{trace, warn}; @@ -19,6 +21,7 @@ use servo::msg::constellation_msg::TraversalDirection; use servo::rendering_context::RenderingContext; use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_url::ServoUrl; +use servo::style_traits::DevicePixel; use crate::egui_glue::EguiGlow; use crate::events_loop::EventsLoop; @@ -121,6 +124,7 @@ impl Minibrowser { pub fn update( &mut self, window: &winit::window::Window, + webviews: &mut WebViewManager, servo_framebuffer_id: Option, reason: &'static str, ) { @@ -190,9 +194,24 @@ impl Minibrowser { }); *toolbar_height = Length::new(height); + let scale = + Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point()); + let Some(focused_webview_id) = webviews.focused_webview_id() else { return }; + let Some(webview) = webviews.get_mut(focused_webview_id) else { return }; + let mut embedder_events = vec![]; CentralPanel::default() .frame(Frame::none()) .show(ctx, |ui| { + let Pos2 { x, y } = ui.cursor().min; + let origin = Point2D::new(x, y); + let Vec2 { x, y } = ui.available_size(); + let size = Size2D::new(x, y); + let rect = Rect::new(origin, size) * scale; + if rect != webview.rect { + webview.rect = rect; + embedder_events + .push(EmbedderEvent::MoveResizeWebView(focused_webview_id, rect)); + } let min = ui.cursor().min; let size = ui.available_size(); let rect = egui::Rect::from_min_size(min, size); @@ -240,6 +259,10 @@ impl Minibrowser { }); }); + if !embedder_events.is_empty() { + webviews.handle_window_events(embedder_events); + } + *last_update = now; }); } diff --git a/ports/servoshell/webview.rs b/ports/servoshell/webview.rs index 97d4e684e66..105cf11c7cc 100644 --- a/ports/servoshell/webview.rs +++ b/ports/servoshell/webview.rs @@ -26,6 +26,7 @@ use servo::script_traits::{ }; use servo::servo_config::opts; use servo::servo_url::ServoUrl; +use servo::webrender_api::units::DeviceRect; use servo::webrender_api::ScrollLocation; use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo}; @@ -45,6 +46,10 @@ pub struct WebViewManager { /// The order in which the webviews were created. creation_order: Vec, + /// The order to paint the browsers in. + /// Modified by EmbedderMsg::BrowserPaintingOrder. + painting_order: Vec, + /// The webview that is currently focused. /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. focused_webview_id: Option, @@ -59,7 +64,9 @@ pub struct WebViewManager { } #[derive(Debug)] -pub struct WebView {} +pub struct WebView { + pub rect: DeviceRect, +} pub struct ServoEventResponse { pub need_present: bool, @@ -77,6 +84,7 @@ where current_url_string: None, webviews: HashMap::default(), creation_order: vec![], + painting_order: vec![], focused_webview_id: None, window, clipboard: match Clipboard::new() { @@ -102,6 +110,20 @@ where self.focused_webview_id } + pub fn get_mut(&mut self, webview_id: WebViewId) -> Option<&mut WebView> { + self.webviews.get_mut(&webview_id) + } + + pub fn focused_webview_id(&self) -> Option { + self.focused_webview_id.clone() + } + + pub fn painting_order(&self) -> impl Iterator { + self.painting_order + .iter() + .flat_map(move |webview_id| self.webviews.get(webview_id).map(|b| (webview_id, b))) + } + pub fn current_url_string(&self) -> Option<&str> { self.current_url_string.as_deref() } @@ -542,14 +564,28 @@ where }; }, EmbedderMsg::WebViewOpened(new_webview_id) => { - self.webviews.insert(new_webview_id, WebView {}); + let scale = self.window.hidpi_factor().get(); + let toolbar = self.window.toolbar_height().get(); + + // Adjust for our toolbar height. + // TODO adjust for egui window decorations if we end up using those + let mut rect = self.window.get_coordinates().get_viewport().to_f32(); + rect.origin.y += toolbar * scale; + rect.size.height -= toolbar * scale; + + self.webviews.insert(new_webview_id, WebView { rect }); self.creation_order.push(new_webview_id); self.event_queue .push(EmbedderEvent::FocusWebView(new_webview_id)); + self.event_queue + .push(EmbedderEvent::MoveResizeWebView(new_webview_id, rect)); + self.event_queue + .push(EmbedderEvent::RaiseWebViewToTop(new_webview_id)); }, EmbedderMsg::WebViewClosed(webview_id) => { self.webviews.retain(|&id, _| id != webview_id); self.creation_order.retain(|&id| id != webview_id); + self.painting_order.retain(|&id| id != webview_id); self.focused_webview_id = None; if let Some(&newest_webview_id) = self.creation_order.last() { self.event_queue @@ -564,6 +600,31 @@ where EmbedderMsg::WebViewBlurred => { self.focused_webview_id = None; }, + EmbedderMsg::WebViewPaintingOrder(webview_ids) => { + self.painting_order = webview_ids; + + if let Some(&newest_webview_id) = self.creation_order.last() { + let mut newest_webview_is_visible = false; + + // Hide any visible browsers other than the most recently created. + // TODO stop doing this once we have full multiple browser support + for &webview_id in self.painting_order.iter() { + if webview_id != newest_webview_id { + self.event_queue + .push(EmbedderEvent::HideWebView(webview_id)); + } else { + newest_webview_is_visible = true; + } + } + + // If the most recently created browser is not visible, show it. + // TODO stop doing this once we have full multiple browser support + if !newest_webview_is_visible { + self.event_queue + .push(EmbedderEvent::ShowWebView(newest_webview_id)); + } + } + }, EmbedderMsg::Keyboard(key_event) => { self.handle_key_from_servo(webview_id, key_event); }, @@ -670,8 +731,11 @@ where }, EmbedderMsg::EventDelivered(event) => match (webview_id, event) { (Some(webview_id), CompositorEventVariant::MouseButtonEvent) => { - // TODO Focus webview and/or raise to top if needed. trace!("{}: Got a mouse button event", webview_id); + self.event_queue + .push(EmbedderEvent::RaiseWebViewToTop(webview_id)); + self.event_queue + .push(EmbedderEvent::FocusWebView(webview_id)); }, (_, _) => {}, }, diff --git a/ports/servoshell/window_trait.rs b/ports/servoshell/window_trait.rs index bc31ef396e3..04d5db60fc8 100644 --- a/ports/servoshell/window_trait.rs +++ b/ports/servoshell/window_trait.rs @@ -47,5 +47,6 @@ pub trait WindowPortsMethods: WindowMethods { events_loop: &winit::event_loop::EventLoopWindowTarget, ) -> Box; fn winit_window(&self) -> Option<&winit::window::Window>; + fn toolbar_height(&self) -> Length; fn set_toolbar_height(&self, height: Length); } diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index 17c9f667d68..de6ac68dec1 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -142,6 +142,7 @@ class MachCommands(CommandBase): "crown", "constellation", "style_config", + "compositing", ] if not packages: packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store'])