From 386a8834a95c0eb8380d99502dc8a93a574d8679 Mon Sep 17 00:00:00 2001 From: webbeef Date: Sat, 11 Jul 2026 22:51:25 -0700 Subject: [PATCH] webview: cleanup layout <-> paint Signed-off-by: webbeef --- .../constellation/constellation.rs.patch | 75 ++++++++---- patches/components/layout/dom.rs.patch | 10 ++ patches/components/layout/replaced.rs.patch | 40 ++++++- patches/components/paint/paint.rs.patch | 110 +++--------------- patches/components/paint/painter.rs.patch | 67 ++++++----- patches/components/paint/tracing.rs.patch | 5 +- .../paint/webview_renderer.rs.patch | 96 ++++++++------- .../script/dom/document/document.rs.patch | 98 +++------------- .../dom/html/htmlwebviewelement.rs.patch | 23 +++- .../script/dom/node/layout_dom.rs.patch | 10 +- .../layout_dom/servo_layout_node.rs.patch | 13 +++ .../shared/layout/layout_node.rs.patch | 15 +++ patches/components/shared/paint/lib.rs.patch | 72 ++++-------- ui/system/desktop/overview_card.css | 4 +- ui/system/desktop/overview_items.js | 18 +++ ui/system/desktop/overview_slot.css | 10 +- 16 files changed, 337 insertions(+), 329 deletions(-) create mode 100644 patches/components/layout/dom.rs.patch create mode 100644 patches/components/script/layout_dom/servo_layout_node.rs.patch create mode 100644 patches/components/shared/layout/layout_node.rs.patch diff --git a/patches/components/constellation/constellation.rs.patch b/patches/components/constellation/constellation.rs.patch index 57a4d6d..2017542 100644 --- a/patches/components/constellation/constellation.rs.patch +++ b/patches/components/constellation/constellation.rs.patch @@ -505,7 +505,7 @@ ); }, #[cfg(feature = "webgpu")] -@@ -2099,7 +2341,997 @@ +@@ -2099,9 +2341,1018 @@ } }, }, @@ -571,9 +571,28 @@ + ) => { + // Only allow if this is a valid embedded webview + if self.webviews.contains_key(&embedded_webview_id) { ++ // The render size is the embedded webview's on-screen box in device ++ // pixels, derived from its viewport details (the same size that lays out ++ // its content). This is what the painter renders the screenshot at, so it ++ // does not depend on the compositor's per-webview rect. ++ let size = self ++ .browsing_contexts ++ .get(&BrowsingContextId::from(embedded_webview_id)) ++ .map(|browsing_context| { ++ let viewport = &browsing_context.viewport_details; ++ (viewport.size * viewport.hidpi_scale_factor) ++ .round() ++ .to_i32() ++ }) ++ .unwrap_or_default(); + self.paint_proxy + .cross_process_paint_api -+ .request_encoded_screenshot(embedded_webview_id, request, response_sender); ++ .request_encoded_screenshot( ++ embedded_webview_id, ++ size, ++ request, ++ response_sender, ++ ); + } else { + let _ = response_sender.send(Err( + embedder_traits::EmbeddedWebViewScreenshotError::WebViewDoesNotExist, @@ -1179,9 +1198,9 @@ + let _ = callback.send(None); + } + }, -+ } -+ } -+ + } + } + + fn handle_pairing_event(&mut self, event: PairingEvent) { + if let PairingEvent::MessageReceived { ref from, ref data } = event { + debug!("P2P message received from {from}, {} bytes", data.len()); @@ -1493,17 +1512,19 @@ + if !channels.is_empty() { + self.pairing.sync_channels_to_peer(id, &channels); + } - } ++ } + + for event_loop in self.event_loops() { + if self.embedder_error_listeners.contains(&event_loop.id()) { + let _ = event_loop.send(ScriptThreadMessage::DispatchPairingEvent(event.clone())); + } + } - } - ++ } ++ /// Check the origin of a message against that of the pipeline it came from. -@@ -2418,6 +3650,55 @@ + /// Note: this is still limited as a security check, + /// see +@@ -2418,6 +3669,55 @@ TransferState::TransferInProgress(queue) => queue.push_back(task), TransferState::CompletionFailed(queue) => queue.push_back(task), TransferState::CompletionRequested(_, queue) => queue.push_back(task), @@ -1559,7 +1580,7 @@ } } -@@ -3204,6 +4485,101 @@ +@@ -3204,6 +4504,101 @@ ); } @@ -1661,7 +1682,7 @@ fn forward_input_event( &mut self, webview_id: WebViewId, -@@ -3223,6 +4599,34 @@ +@@ -3223,6 +4618,34 @@ let pressed_mouse_buttons = self.pressed_mouse_buttons; let active_keyboard_modifiers = self.active_keyboard_modifiers; @@ -1696,7 +1717,7 @@ let event_id = event.id; let Some(webview) = self.webviews.get_mut(&webview_id) else { warn!("Got input event for unknown WebViewId: {webview_id:?}"); -@@ -3325,6 +4729,40 @@ +@@ -3325,6 +4748,40 @@ /// fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { debug!("{webview_id}: Closing"); @@ -1737,7 +1758,7 @@ let browsing_context_id = BrowsingContextId::from(webview_id); // Step 5. Remove traversable from the user agent's top-level traversable set. let browsing_context = -@@ -3601,8 +5039,27 @@ +@@ -3601,8 +5058,27 @@ opener_webview_id, opener_pipeline_id, response_sender, @@ -1765,7 +1786,7 @@ let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { warn!("Failed to create channel"); let _ = response_sender.send(None); -@@ -3701,6 +5158,392 @@ +@@ -3701,6 +5177,402 @@ }); } @@ -1869,6 +1890,11 @@ + new_webview_id, + (new_browsing_context_id, parent_pipeline_id), + ); ++ // Tell the painter it is embedded, so it is composited via its parent's push_iframe ++ // rather than pushed as a top-level webview in the root display list. ++ self.paint_proxy ++ .cross_process_paint_api ++ .register_embedded_webview(new_webview_id); + + self.webviews.insert( + new_webview_id, @@ -2056,6 +2082,11 @@ + new_webview_id, + (new_browsing_context_id, parent_pipeline_id), + ); ++ // Tell the painter it is embedded, so it is composited via its parent's push_iframe ++ // rather than pushed as a top-level webview in the root display list. ++ self.paint_proxy ++ .cross_process_paint_api ++ .register_embedded_webview(new_webview_id); + + assert!(!self.pipelines.contains_key(&new_pipeline_id)); + self.pipelines.insert(new_pipeline_id, pipeline); @@ -2158,7 +2189,7 @@ #[servo_tracing::instrument(skip_all)] fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { let Some(pipeline) = self.pipelines.get(&pipeline_id) else { -@@ -4250,7 +6093,7 @@ +@@ -4250,7 +6122,7 @@ }, }; @@ -2167,7 +2198,7 @@ match self.browsing_contexts.get_mut(&browsing_context_id) { Some(browsing_context) => { let old_pipeline_id = browsing_context.pipeline_id; -@@ -4259,6 +6102,7 @@ +@@ -4259,6 +6131,7 @@ old_pipeline_id, browsing_context.parent_pipeline_id, browsing_context.webview_id, @@ -2175,7 +2206,7 @@ ) }, None => { -@@ -4268,6 +6112,15 @@ +@@ -4268,6 +6141,15 @@ self.unload_document(old_pipeline_id); @@ -2191,7 +2222,7 @@ if let Some(new_pipeline) = self.pipelines.get(&new_pipeline_id) { if let Some(ref chan) = self.devtools_sender { let state = NavigationState::Start(new_pipeline.url.clone()); -@@ -4836,7 +6689,7 @@ +@@ -4836,7 +6718,7 @@ } #[servo_tracing::instrument(skip_all)] @@ -2200,7 +2231,7 @@ // Send a flat projection of the history to embedder. // The final vector is a concatenation of the URLs of the past // entries, the current entry and the future entries. -@@ -4948,9 +6801,22 @@ +@@ -4948,9 +6830,22 @@ self.constellation_to_embedder_proxy .send(ConstellationToEmbedderMsg::HistoryChanged( webview_id, @@ -2224,7 +2255,7 @@ } #[servo_tracing::instrument(skip_all)] -@@ -4969,7 +6835,7 @@ +@@ -4969,7 +6864,7 @@ webview.focused_browsing_context_id = change.browsing_context_id; } @@ -2233,7 +2264,7 @@ match self.browsing_contexts.get_mut(&change.browsing_context_id) { Some(browsing_context) => { debug!("Adding pipeline to existing browsing context."); -@@ -4976,11 +6842,15 @@ +@@ -4976,11 +6871,15 @@ let old_pipeline_id = browsing_context.pipeline_id; browsing_context.pipelines.insert(change.new_pipeline_id); browsing_context.update_current_entry(change.new_pipeline_id); @@ -2251,7 +2282,7 @@ }, }; -@@ -4988,6 +6858,18 @@ +@@ -4988,6 +6887,18 @@ self.unload_document(old_pipeline_id); } diff --git a/patches/components/layout/dom.rs.patch b/patches/components/layout/dom.rs.patch new file mode 100644 index 0000000..c48e828 --- /dev/null +++ b/patches/components/layout/dom.rs.patch @@ -0,0 +1,10 @@ +--- original ++++ modified +@@ -448,6 +448,7 @@ + (Some(pipeline_id), Some(browsing_context_id)) => Some(IFrameInfo { + pipeline_id, + browsing_context_id, ++ page_zoom_for_rendering: self.iframe_page_zoom(), + }), + _ => None, + } diff --git a/patches/components/layout/replaced.rs.patch b/patches/components/layout/replaced.rs.patch index f1c71ad..d377258 100644 --- a/patches/components/layout/replaced.rs.patch +++ b/patches/components/layout/replaced.rs.patch @@ -1,10 +1,44 @@ --- original +++ modified -@@ -563,6 +563,7 @@ +@@ -128,6 +128,9 @@ + pub(crate) struct IFrameInfo { + pub pipeline_id: PipelineId, + pub browsing_context_id: BrowsingContextId, ++ /// Page zoom for an embedded `` (a separate top-level webview), or None for a ++ /// regular iframe (which inherits page zoom from its parent). ++ pub page_zoom_for_rendering: Option, + } + + #[derive(Debug, MallocSizeOf)] +@@ -551,8 +554,18 @@ + }))] + }, + ReplacedContentKind::IFrame(iframe) => { +- let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px()); +- let hidpi_scale_factor = layout_context.style_context.device_pixel_ratio(); ++ let base_hidpi = layout_context.style_context.device_pixel_ratio().0; ++ // An embedded `` is a separate top-level webview with its own page ++ // zoom, so fold that zoom into its viewport the way a top-level webview does: ++ // shrink the layout viewport by the zoom and grow the device scale by it, so ++ // the content reflows at the zoomed size (page_zoom_for_rendering alone only ++ // applies the visual transform, without reflow). A regular iframe inherits its ++ // parent's zoom, so page_zoom is 1 and this is a no-op for it. ++ let page_zoom = iframe.page_zoom_for_rendering.unwrap_or(1.0); ++ let size = Size2D::new( ++ rect.size.width.to_f32_px() / page_zoom, ++ rect.size.height.to_f32_px() / page_zoom, ++ ); + + layout_context.iframe_sizes.lock().insert( + iframe.browsing_context_id, +@@ -561,8 +574,9 @@ + pipeline_id: iframe.pipeline_id, + viewport_details: ViewportDetails { size, - hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), +- hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), ++ hidpi_scale_factor: Scale::new(base_hidpi * page_zoom), device_size: layout_context.device_size.cast_unit(), -+ page_zoom_for_rendering: None, ++ page_zoom_for_rendering: iframe.page_zoom_for_rendering, }, }, ); diff --git a/patches/components/paint/paint.rs.patch b/patches/components/paint/paint.rs.patch index 7a03eae..5b93fea 100644 --- a/patches/components/paint/paint.rs.patch +++ b/patches/components/paint/paint.rs.patch @@ -19,108 +19,31 @@ use servo_base::id::{PainterId, PipelineId, WebViewId}; use servo_canvas_traits::webgl::{WebGLContextId, WebGLThreads}; use servo_config::pref; -@@ -45,7 +46,7 @@ - #[cfg(feature = "webgpu")] - use webgpu::canvas_context::WebGpuExternalImageMap; - use webrender::{CaptureBits, MemoryReport}; --use webrender_api::units::{DevicePixel, DevicePoint}; -+use webrender_api::units::{DevicePixel, DevicePoint, DeviceRect}; - use webrender_api::{FontInstanceKey, FontKey, ImageKey}; - - use crate::InitialPaintState; -@@ -139,6 +140,11 @@ - /// An map of external images shared between all `WebGpuExternalImages`. - #[cfg(feature = "webgpu")] - webgpu_image_map: std::cell::OnceCell, -+ -+ /// Tracks the rects of embedded webviews within their parent webviews. -+ /// Maps parent WebViewId to a map of embedded WebViewId to rect. -+ /// This is used to forward input events to embedded webviews. -+ embedded_webview_rects: RefCell>>, - } - - /// Why we need to be repainted. This is used for debugging. -@@ -220,6 +226,7 @@ - webxr_main_thread: RefCell::new(webxr_main_thread), - #[cfg(feature = "webgpu")] - webgpu_image_map: Default::default(), -+ embedded_webview_rects: Default::default(), - })) - } - -@@ -557,6 +564,92 @@ +@@ -557,6 +558,44 @@ painter.enable_lcp_calculation(&webview_id); } }, -+ PaintMessage::UpdateEmbeddedWebViewRect { -+ embedded_webview_id, -+ parent_webview_id, -+ rect, -+ } => { -+ debug!( -+ "UpdateEmbeddedWebViewRect: embedded={:?}, parent={:?}, rect={:?}", -+ embedded_webview_id, parent_webview_id, rect -+ ); -+ // Store the rect for input event routing (to transform coordinates) -+ self.embedded_webview_rects -+ .borrow_mut() -+ .entry(parent_webview_id) -+ .or_default() -+ .insert(embedded_webview_id, rect); -+ -+ // Update the embedded WebViewRenderer's rect so it's positioned correctly -+ // in the shared WebRender display list. The embedded webview shares the same -+ // Painter/RenderingContext as the parent, so its content must be positioned -+ // at the iframe's location for both rendering and hit testing to work. -+ if let Some(mut painter) = self.maybe_painter_mut(parent_webview_id.into()) { -+ // Register this as an embedded webview so it won't be rendered as a -+ // top-level iframe in the root display list. ++ PaintMessage::RegisterEmbeddedWebView(embedded_webview_id) => { ++ // Mark this webview as embedded so the painter skips it in the root display ++ // list (it is composited via its parent's push_iframe instead). Its size, ++ // position and visibility all come from the parent's layout of the ++ // element. An embedded webview shares its parent's Painter, so routing by its ++ // own id reaches that Painter. ++ if let Some(mut painter) = self.maybe_painter_mut(embedded_webview_id.into()) { + painter.register_embedded_webview(embedded_webview_id); -+ if let Some(webview_renderer) = -+ painter.webview_renderer_mut(embedded_webview_id) -+ { -+ if webview_renderer.set_rect(rect) { -+ // Rect changed, need to rebuild the display list -+ painter.send_root_pipeline_display_list(); -+ } -+ } ++ painter.send_root_pipeline_display_list(); + } + }, + PaintMessage::RemoveEmbeddedWebView(embedded_webview_id) => { -+ // Remove from all parent webviews -+ for embedded_map in self.embedded_webview_rects.borrow_mut().values_mut() { -+ embedded_map.remove(&embedded_webview_id); -+ } -+ // Also unregister from all painters ++ // Unregister from all painters + for painter in self.painters.iter() { + painter + .borrow_mut() + .unregister_embedded_webview(embedded_webview_id); + } + }, -+ PaintMessage::SetEmbeddedWebViewHidden { -+ embedded_webview_id, -+ parent_webview_id, -+ hidden, -+ } => { -+ debug!( -+ "SetEmbeddedWebViewHidden: embedded={:?}, parent={:?}, hidden={:?}", -+ embedded_webview_id, parent_webview_id, hidden -+ ); -+ // Update the embedded WebViewRenderer's hidden state -+ if let Some(mut painter) = self.maybe_painter_mut(parent_webview_id.into()) { -+ if let Some(webview_renderer) = -+ painter.webview_renderer_mut(embedded_webview_id) -+ { -+ if webview_renderer.set_hidden(hidden) { -+ // Hidden state changed, need to rebuild the display list -+ painter.send_root_pipeline_display_list(); -+ } -+ } -+ } -+ }, -+ PaintMessage::TakeEncodedScreenshot(webview_id, request, response_sender) => { -+ self.handle_take_encoded_screenshot(webview_id, request, response_sender); ++ PaintMessage::TakeEncodedScreenshot(webview_id, size, request, response_sender) => { ++ self.handle_take_encoded_screenshot(webview_id, size, request, response_sender); + }, + PaintMessage::ForwardInputEventToEmbeddedWebView(embedded_webview_id, event) => { + // Forward the input event directly to the embedded webview's painter. @@ -141,7 +64,7 @@ } } -@@ -614,6 +707,69 @@ +@@ -614,6 +653,70 @@ sender.send(ProcessReports::new(reports)); } @@ -149,6 +72,7 @@ + fn handle_take_encoded_screenshot( + &self, + webview_id: WebViewId, ++ size: webrender_api::units::DeviceIntSize, + request: embedder_traits::EmbeddedWebViewScreenshotRequest, + response_sender: GenericCallback< + Result< @@ -168,7 +92,7 @@ + return; + } + -+ let rgba_image = match painter.render_webview_to_image(webview_id) { ++ let rgba_image = match painter.render_webview_to_image(webview_id, size) { + Some(image) => image, + None => { + let _ = response_sender.send(Err(EmbeddedWebViewScreenshotError::CaptureFailed)); @@ -211,7 +135,7 @@ /// Handle messages sent to `Paint` during the shutdown process. In general, /// the things `Paint` can do in this state are limited. It's very important to /// answer any synchronous messages though as other threads might be waiting on the -@@ -766,6 +922,9 @@ +@@ -766,6 +869,9 @@ #[cfg(feature = "webxr")] self.webxr_main_thread.borrow_mut().run_one_frame(); @@ -221,7 +145,7 @@ for painter in &self.painters { painter.borrow_mut().perform_updates(); } -@@ -819,6 +978,10 @@ +@@ -819,6 +925,10 @@ if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } diff --git a/patches/components/paint/painter.rs.patch b/patches/components/paint/painter.rs.patch index 8397b13..8e22169 100644 --- a/patches/components/paint/painter.rs.patch +++ b/patches/components/paint/painter.rs.patch @@ -14,12 +14,12 @@ use webrender_api::units::{ - DevicePixel, DevicePoint, LayoutPoint, LayoutRect, LayoutSize, LayoutTransform, LayoutVector2D, - WorldPoint, -+ DeviceIntRect, DevicePixel, DevicePoint, LayoutPoint, LayoutRect, LayoutSize, LayoutTransform, -+ LayoutVector2D, WorldPoint, ++ DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint, LayoutPoint, LayoutRect, LayoutSize, ++ LayoutTransform, LayoutVector2D, WorldPoint, }; use webrender_api::{ self, BuiltDisplayList, BuiltDisplayListDescriptor, ColorF, DirtyRect, DisplayListPayload, -@@ -81,8 +82,15 @@ +@@ -81,7 +82,14 @@ pub(crate) painter_id: PainterId, /// Our [`WebViewRenderer`]s, one for every `WebView`. @@ -27,15 +27,14 @@ + /// Using BTreeMap to ensure deterministic iteration order by WebViewId, + /// which is important for proper z-ordering in the display list (parents before children). + pub(crate) webview_renderers: BTreeMap, - ++ + /// Set of WebViewIds that are embedded webviews. These should not be rendered + /// as top-level iframes in the root display list, as they are already referenced + /// by their parent's display list through IFrameFragment. + pub(crate) embedded_webview_ids: FxHashSet, -+ + /// Tracks whether or not the view needs to be repainted. pub(crate) needs_repaint: Cell, - @@ -266,6 +274,7 @@ painter_id, embedder_to_constellation_sender, @@ -93,14 +92,14 @@ if let Some(colors) = self.web_content_animator.update(&self.webview_renderers) { let mut transaction = Transaction::new(); -@@ -316,6 +342,104 @@ +@@ -316,8 +342,106 @@ self.generate_frame(&mut transaction, RenderReasons::ANIMATED_PROPERTY); self.send_transaction(transaction); } + + self.send_zoom_and_scroll_offset_updates(need_zoom, scroll_offset_updates); -+ } -+ + } + + /// Resolve a scroll that arrived on the `entry` webview at `cursor`. + /// + /// Real scroll events are already routed to the correct webview: the chrome forwards a @@ -195,9 +194,11 @@ + self.webview_renderers + .get_mut(&entry) + .and_then(|entry_renderer| entry_renderer.scroll_root_pipeline(scroll)) - } - ++ } ++ #[track_caller] + fn assert_no_gl_error(&self) { + debug_assert_eq!(self.webrender_gl.get_error(), gleam::gl::NO_ERROR); @@ -609,7 +733,16 @@ let root_clip_id = builder.define_clip_rect(root_reference_frame, viewport_rect); @@ -224,7 +225,7 @@ let mut transaction = Transaction::new(); self.send_root_pipeline_display_list_in_transaction(&mut transaction); self.generate_frame(&mut transaction, RenderReasons::SCENE); -@@ -676,6 +809,100 @@ +@@ -676,6 +809,112 @@ self.send_transaction(transaction); } @@ -232,18 +233,34 @@ + /// Builds a minimal display list with only the target webview's pipeline, + /// renders it to the back buffer (without swapping), reads pixels, then + /// restores the normal display list. -+ pub(crate) fn render_webview_to_image(&mut self, webview_id: WebViewId) -> Option { ++ pub(crate) fn render_webview_to_image( ++ &mut self, ++ webview_id: WebViewId, ++ size: DeviceIntSize, ++ ) -> Option { + let webview_renderer = self.webview_renderers.get(&webview_id)?; + let pipeline_id = webview_renderer.root_pipeline_id?; -+ let rect = webview_renderer.rect; -+ let width = rect.width() as u32; -+ let height = rect.height() as u32; ++ // The render size is supplied by the constellation from the webview's viewport ++ // details (its on-screen `` box in device pixels), so it does not depend on ++ // this renderer's `rect`. ++ let width = size.width.max(0) as u32; ++ let height = size.height.max(0) as u32; + if width == 0 || height == 0 { + return None; + } + -+ let device_pixels_per_page_pixel = -+ webview_renderer.device_pixels_per_page_pixel_not_including_pinch_zoom(); ++ // Embedded webviews apply their page zoom inside their own display list ++ // (page_zoom_for_rendering), so scale only by the device hidpi here -- the same as the ++ // chrome's dpppp does for the embedded pipeline in normal push_iframe compositing. ++ // Using the page-zoom-inclusive scale would double-count the zoom and leave a gray gap ++ // at zoom != 1. Top-level webviews apply zoom externally, so they use the full scale. ++ let render_scale = if self.embedded_webview_ids.contains(&webview_id) { ++ webview_renderer.hidpi_scale_factor().get() ++ } else { ++ webview_renderer ++ .device_pixels_per_page_pixel_not_including_pinch_zoom() ++ .get() ++ }; + + // Build a display list with only the target webview at origin + let root_pipeline = WebRenderPipelineId(0, 1); @@ -259,11 +276,7 @@ + let root_clip = builder.define_clip_rect(root_ref, viewport_rect); + let clip_chain = builder.define_clip_chain(None, [root_clip]); + -+ let transform = LayoutTransform::scale( -+ device_pixels_per_page_pixel.get(), -+ device_pixels_per_page_pixel.get(), -+ 1.0, -+ ); ++ let transform = LayoutTransform::scale(render_scale, render_scale, 1.0); + let webview_ref = builder.push_reference_frame( + LayoutPoint::zero(), + root_ref, @@ -277,7 +290,7 @@ + ); + + let scaled_size = -+ LayoutSize::from_untyped((rect.size() / device_pixels_per_page_pixel).to_untyped()); ++ LayoutSize::new(width as f32 / render_scale, height as f32 / render_scale); + let iframe_rect = LayoutRect::from_size(scaled_size); + builder.push_iframe( + iframe_rect, @@ -325,7 +338,7 @@ /// Update the given transaction with the scroll offsets of all active scroll nodes in /// the WebRender scene. This is necessary because WebRender does not preserve scroll /// offsets between scroll tree modifications. If a display list could potentially -@@ -805,6 +1032,26 @@ +@@ -805,6 +1044,26 @@ self.send_root_pipeline_display_list(); } @@ -352,7 +365,7 @@ pub(crate) fn set_throttled( &mut self, webview_id: WebViewId, -@@ -1204,15 +1451,23 @@ +@@ -1204,15 +1463,23 @@ webview: Box, viewport_details: ViewportDetails, ) { @@ -380,7 +393,7 @@ } pub(crate) fn remove_webview(&mut self, webview_id: WebViewId) { -@@ -1414,7 +1669,6 @@ +@@ -1414,7 +1681,6 @@ result: InputEventResult, ) { let Some(webview_renderer) = self.webview_renderers.get_mut(&webview_id) else { diff --git a/patches/components/paint/tracing.rs.patch b/patches/components/paint/tracing.rs.patch index 1192b42..ba4e4dc 100644 --- a/patches/components/paint/tracing.rs.patch +++ b/patches/components/paint/tracing.rs.patch @@ -1,12 +1,11 @@ --- original +++ modified -@@ -55,6 +55,14 @@ +@@ -55,6 +55,13 @@ Self::ScreenshotReadinessReponse(..) => target!("ScreenshotReadinessResponse"), Self::SendLCPCandidate(..) => target!("SendLCPCandidate"), Self::EnableLCPCalculation(..) => target!("EnableLCPCalculation"), -+ Self::UpdateEmbeddedWebViewRect { .. } => target!("UpdateEmbeddedWebViewRect"), ++ Self::RegisterEmbeddedWebView(..) => target!("RegisterEmbeddedWebView"), + Self::RemoveEmbeddedWebView(..) => target!("RemoveEmbeddedWebView"), -+ Self::SetEmbeddedWebViewHidden { .. } => target!("SetEmbeddedWebViewHidden"), + Self::TakeEncodedScreenshot(..) => target!("TakeEncodedScreenshot"), + Self::ForwardInputEventToEmbeddedWebView(..) => { + target!("ForwardInputEventToEmbeddedWebView") diff --git a/patches/components/paint/webview_renderer.rs.patch b/patches/components/paint/webview_renderer.rs.patch index f3a73dd..fc12e5b 100644 --- a/patches/components/paint/webview_renderer.rs.patch +++ b/patches/components/paint/webview_renderer.rs.patch @@ -195,11 +195,6 @@ + pinch_zoom_result, + pending_scroll: combined_scroll_event, + } -+ } -+ -+ /// Whether this renderer has a scroll tree for `pipeline_id`. -+ pub(crate) fn owns_pipeline(&self, pipeline_id: PipelineId) -> bool { -+ self.pipelines.contains_key(&pipeline_id) } - /// Perform a hit test at the given [`DevicePoint`] and apply the [`Scroll`] @@ -213,6 +208,11 @@ - scroll: Scroll, - ) -> Option { - let scroll_location = match scroll { ++ /// Whether this renderer has a scroll tree for `pipeline_id`. ++ pub(crate) fn owns_pipeline(&self, pipeline_id: PipelineId) -> bool { ++ self.pipelines.contains_key(&pipeline_id) ++ } ++ + /// Convert a [`Scroll`] into a [`ScrollLocation`] using this renderer's own scale. + fn scroll_location_for(&self, scroll: Scroll) -> ScrollLocation { + match scroll { @@ -258,7 +258,12 @@ + offset, + }) + } -+ + +- let hit_test_results: Vec<_> = self +- .touch_handler +- .get_hit_test_result_cache_value() +- .map(|result| vec![result]) +- .unwrap_or_else(|| self.hit_test(render_api, cursor)); + /// Scroll this renderer's root pipeline's root scroll node (the document viewport). Used + /// as a fallback when a scroll's cursor does not resolve to an owned scroll node, which + /// happens for an embedded webview: it receives wheel events in its own local @@ -301,23 +306,6 @@ + offset, + }) + } -+ -+ /// The hit-test result pinned while touch-panning, so a fling keeps scrolling the -+ /// same node across a touch sequence. -+ pub(crate) fn cached_scroll_hit_test(&self) -> Option { -+ self.touch_handler.get_hit_test_result_cache_value() -+ } - -- let hit_test_results: Vec<_> = self -- .touch_handler -- .get_hit_test_result_cache_value() -- .map(|result| vec![result]) -- .unwrap_or_else(|| self.hit_test(render_api, cursor)); -+ /// Pin the scrolled node's hit-test result for the current touch/pan sequence. -+ pub(crate) fn cache_scroll_hit_test(&mut self, hit_test_result: PaintHitTestResult) { -+ self.touch_handler -+ .set_hit_test_result_cache_value(hit_test_result, self.device_pixels_per_page_pixel()); -+ } - // 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 @@ -352,6 +340,18 @@ - } - } - None ++ /// The hit-test result pinned while touch-panning, so a fling keeps scrolling the ++ /// same node across a touch sequence. ++ pub(crate) fn cached_scroll_hit_test(&self) -> Option { ++ self.touch_handler.get_hit_test_result_cache_value() ++ } ++ ++ /// Pin the scrolled node's hit-test result for the current touch/pan sequence. ++ pub(crate) fn cache_scroll_hit_test(&mut self, hit_test_result: PaintHitTestResult) { ++ self.touch_handler ++ .set_hit_test_result_cache_value(hit_test_result, self.device_pixels_per_page_pixel()); ++ } ++ + /// Stop any in-progress fling (used when a scroll finds nothing to scroll). + pub(crate) fn stop_fling(&mut self) { + self.touch_handler.stop_fling_if_needed(); @@ -381,42 +381,48 @@ external_scroll_id, offset, }; -@@ -1046,13 +1095,24 @@ +@@ -1035,6 +1084,12 @@ + self.page_zoom * self.hidpi_scale_factor + } + ++ /// The device hidpi scale, excluding page zoom. Used for embedded-webview screenshots, ++ /// whose page zoom is applied inside the embedded pipeline's own display list. ++ pub(crate) fn hidpi_scale_factor(&self) -> Scale { ++ self.hidpi_scale_factor ++ } ++ + /// Adjust the pinch zoom of the [`WebView`] by the given zoom delta. + pub(crate) fn adjust_pinch_zoom(&mut self, magnification: f32, center: DevicePoint) { + if magnification == 1.0 { +@@ -1046,8 +1101,17 @@ } fn send_window_size_message(&self) { - // The device pixel ratio used by the style system should include the scale from page pixels - // to device pixels, but not including any pinch zoom. -+ // Both top-level and embedded webviews include page_zoom in hidpi_scale_factor -+ // to cause layout to reflow at the zoomed viewport size. -+ // -+ // The difference is in how the visual scaling is applied: -+ // - Top-level: zoom transform applied externally by the painter as a reference frame -+ // - Embedded: zoom transform applied inside the display list via page_zoom_for_rendering -+ // -+ // This matches Firefox/servoshell behavior where zoom causes layout reflow. ++ // Embedded webviews get their viewport (size and page zoom) from the parent's layout ++ // via the iframe-sizes path; this renderer's `rect` is not maintained for them, so ++ // sending here would push a stale (full-window) size that fights the layout-driven ++ // value. Skip embedded webviews entirely. ++ if self.is_embedded_webview { ++ return; ++ } ++ ++ // Page zoom is folded into hidpi_scale_factor so layout reflows at the zoomed viewport ++ // size; the visual zoom transform is applied externally by the painter as a reference ++ // frame. This matches Firefox/servoshell behavior where zoom causes layout reflow. let device_pixel_ratio = self.device_pixels_per_page_pixel_not_including_pinch_zoom(); // From : // This is the viewport you get after processing the viewport tag. - let layout_viewport = self.rect.size().to_f32() / - (device_pixel_ratio * Scale::new(self.viewport_description.initial_scale.get())); -+ let page_zoom_for_rendering = if self.is_embedded_webview { -+ Some(self.page_zoom.get()) -+ } else { -+ None -+ }; - let _ = self.embedder_to_constellation_sender.send( - EmbedderToConstellationMessage::ChangeViewportDetails( - self.id, -@@ -1060,6 +1120,7 @@ +@@ -1060,6 +1124,7 @@ hidpi_scale_factor: device_pixel_ratio, size: layout_viewport, device_size: self.screen_size, -+ page_zoom_for_rendering, ++ page_zoom_for_rendering: None, }, WindowSizeType::Resize, ), -@@ -1143,8 +1204,21 @@ +@@ -1143,8 +1208,21 @@ !result.contains(InputEventResult::DefaultPrevented) { // A scroll delta for a wheel event is the inverse of the wheel delta. diff --git a/patches/components/script/dom/document/document.rs.patch b/patches/components/script/dom/document/document.rs.patch index d0f70bf..a18e098 100644 --- a/patches/components/script/dom/document/document.rs.patch +++ b/patches/components/script/dom/document/document.rs.patch @@ -24,15 +24,7 @@ use servo_media::{ClientContextId, ServoMedia}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use style::attr::AttrValue; -@@ -168,6 +171,7 @@ - use crate::dom::html::htmlimageelement::HTMLImageElement; - use crate::dom::html::htmlscriptelement::{HTMLScriptElement, ScriptResult}; - use crate::dom::html::htmltitleelement::HTMLTitleElement; -+use crate::dom::html::htmlwebviewelement::HTMLWebViewElement; - use crate::dom::htmldetailselement::DetailsNameGroups; - use crate::dom::intersectionobserver::IntersectionObserver; - use crate::dom::iterators::ShadowIncluding; -@@ -218,6 +222,7 @@ +@@ -218,6 +221,7 @@ use crate::task_manager::TaskManager; use crate::task_source::TaskSourceName; use crate::timers::{OneshotTimerCallback, OneshotTimers}; @@ -40,7 +32,7 @@ use crate::xpath::parse_expression; #[derive(Clone, Copy, PartialEq)] -@@ -413,6 +418,8 @@ +@@ -413,6 +417,8 @@ applets: MutNullableDom, /// Information about the `` in this [`Document`]. iframes: RefCell, @@ -49,7 +41,7 @@ /// Shared locks used for style attributes, author-origin stylesheets, and user and /// user agent stylesheets in this document. Can be acquired once for accessing many /// objects. This is shared with the owning [`ScriptThread`]. -@@ -654,6 +661,9 @@ +@@ -654,6 +660,9 @@ #[no_trace] favicon: RefCell>, @@ -59,7 +51,7 @@ /// All websockets created that are associated with this document. websockets: DOMTracker, -@@ -707,6 +717,11 @@ +@@ -707,6 +716,11 @@ /// history: MutNullableDom, @@ -71,7 +63,7 @@ } impl Document { -@@ -956,6 +971,12 @@ +@@ -956,6 +970,12 @@ // Set the document's activity level, reflow if necessary, and suspend or resume timers. self.activity.set(activity); @@ -84,7 +76,7 @@ let media = ServoMedia::get(); let pipeline_id = self.window().pipeline_id(); let client_context_id = -@@ -969,6 +990,7 @@ +@@ -969,6 +989,7 @@ self.title_changed(); self.notify_embedder_favicon(); @@ -92,7 +84,7 @@ self.dirty_all_nodes(); self.window().resume(cx); media.resume(&client_context_id); -@@ -1356,6 +1378,9 @@ +@@ -1356,6 +1377,9 @@ LoadStatus::Started, )); self.send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); @@ -102,7 +94,7 @@ } }, DocumentReadyState::Complete => { -@@ -1364,6 +1389,9 @@ +@@ -1364,6 +1388,9 @@ self.webview_id(), LoadStatus::Complete, )); @@ -112,7 +104,7 @@ } update_with_current_instant(&self.navigation_timing.dom_complete); }, -@@ -1444,7 +1472,13 @@ +@@ -1444,7 +1471,13 @@ let window = self.window(); if window.is_top_level() { let title = self.title().map(String::from); @@ -127,7 +119,7 @@ } } -@@ -1453,6 +1487,18 @@ +@@ -1453,6 +1486,18 @@ window.send_to_embedder(msg); } @@ -146,7 +138,7 @@ pub(crate) fn dirty_all_nodes(&self) { let root = match self.GetDocumentElement() { Some(root) => root, -@@ -2902,6 +2948,17 @@ +@@ -2902,6 +2947,17 @@ self.iframes.borrow_mut() } @@ -164,65 +156,7 @@ pub(crate) fn set_navigation_start(&self, navigation_start: CrossProcessInstant) { self.interactive_time .borrow_mut() -@@ -3140,9 +3197,57 @@ - current_rendering_epoch, - ); - -+ // After reflow, update embedded webview rects for input event routing -+ self.update_embedded_webview_rects(); -+ - (phases, statistics) - } - -+ /// Update the rects of embedded webviews for input event routing. -+ /// This sends the current position/size of each embedded webview iframe -+ /// to the compositor so it can route input events to the correct webview. -+ /// Also updates visibility state when iframes have display:none. -+ fn update_embedded_webview_rects(&self) { -+ let parent_webview_id = self.webview_id(); -+ let paint_api = self.window().paint_api(); -+ let device_pixel_ratio = self.window().device_pixel_ratio().get(); -+ -+ // Collect embedded webviews first to avoid holding the borrow during border_box() -+ // calls, which can trigger reflow. -+ let embedded_webviews: Vec<(DomRoot, WebViewId)> = self -+ .webviews() -+ .iter() -+ .filter_map(|webview| webview.embedded_webview_id().map(|id| (webview, id))) -+ .collect(); -+ -+ for (webview, embedded_webview_id) in embedded_webviews { -+ // Get the webview's border box (viewport-relative, like getBoundingClientRect()). -+ // If it has display:none, border_box() returns None. -+ let Some(border_box) = webview.upcast::().border_box() else { -+ // Iframe is not visible (display:none), notify compositor to hide it -+ paint_api.set_embedded_webview_hidden(embedded_webview_id, parent_webview_id, true); -+ continue; -+ }; -+ -+ // Convert to device pixels -+ // Note: border_box coordinates are viewport-relative (like getBoundingClientRect) -+ let rect = webrender_api::units::DeviceRect::from_origin_and_size( -+ webrender_api::units::DevicePoint::new( -+ border_box.origin.x.to_f32_px() * device_pixel_ratio, -+ border_box.origin.y.to_f32_px() * device_pixel_ratio, -+ ), -+ webrender_api::units::DeviceSize::new( -+ border_box.size.width.to_f32_px() * device_pixel_ratio, -+ border_box.size.height.to_f32_px() * device_pixel_ratio, -+ ), -+ ); -+ -+ // Iframe is visible, notify compositor to show it and update its rect -+ paint_api.set_embedded_webview_hidden(embedded_webview_id, parent_webview_id, false); -+ paint_api.update_embedded_webview_rect(embedded_webview_id, parent_webview_id, rect); -+ } -+ } -+ - pub(crate) fn handle_no_longer_waiting_on_asynchronous_image_updates(&self) { - self.waiting_on_canvas_image_updates.set(false); - } -@@ -3765,6 +3870,7 @@ +@@ -3765,6 +3821,7 @@ anchors: Default::default(), applets: Default::default(), iframes: RefCell::new(IFrameCollection::new()), @@ -230,7 +164,7 @@ shared_style_locks, stylesheets: DomRefCell::new(DocumentStylesheetSet::new()), stylesheet_list: MutNullableDom::new(None), -@@ -3845,6 +3951,7 @@ +@@ -3845,6 +3902,7 @@ active_sandboxing_flag_set: Cell::new(creation_sandboxing_flag_set), creation_sandboxing_flag_set: Cell::new(creation_sandboxing_flag_set), favicon: RefCell::new(None), @@ -238,7 +172,7 @@ websockets: DOMTracker::new(), details_name_groups: Default::default(), protocol_handler_automation_mode: Default::default(), -@@ -3865,6 +3972,7 @@ +@@ -3865,6 +3923,7 @@ )), image_cache, history: Default::default(), @@ -246,7 +180,7 @@ } } -@@ -4906,6 +5014,36 @@ +@@ -4906,6 +4965,36 @@ pub(crate) fn notify_embedder_favicon(&self) { if let Some(ref image) = *self.favicon.borrow() { @@ -283,7 +217,7 @@ self.send_to_embedder(EmbedderMsg::NewFavicon(self.webview_id(), image.clone())); } } -@@ -4984,6 +5122,30 @@ +@@ -4984,6 +5073,30 @@ pub(crate) fn set_iframe_load_in_progress(&self, value: bool) { self.iframe_load_in_progress.set(value) } diff --git a/patches/components/script/dom/html/htmlwebviewelement.rs.patch b/patches/components/script/dom/html/htmlwebviewelement.rs.patch index 6b65b9c..fea7894 100644 --- a/patches/components/script/dom/html/htmlwebviewelement.rs.patch +++ b/patches/components/script/dom/html/htmlwebviewelement.rs.patch @@ -1,6 +1,6 @@ --- original +++ modified -@@ -0,0 +1,1715 @@ +@@ -0,0 +1,1736 @@ +/* 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/. */ @@ -269,6 +269,12 @@ + self.pipeline_id.set(Some(response.new_pipeline_id)); + self.webview_id.set(Some(response.new_webview_id)); + ++ // Dirty the node so the parent reflows and the iframe-sizes path sizes the ++ // embedded webview immediately. The pipeline id is only known now (after the ++ // blocking IPC), so any layout that ran on connection emitted no iframe ++ // fragment yet. ++ self.upcast::().dirty(NodeDamage::Other); ++ + // Register a dissimilar-origin WindowProxy so that contentWindow works + // for cross-event-loop embedded webviews (e.g. for postMessage). + if self @@ -341,6 +347,11 @@ + self.pipeline_id.set(Some(pipeline_id)); + self.webview_id.set(Some(webview_id)); + ++ // Dirty the node so the parent reflows and the iframe-sizes path sizes the embedded ++ // webview immediately. Otherwise its viewport keeps the creation default (often the ++ // full window) until an unrelated reflow corrects it. ++ self.upcast::().dirty(NodeDamage::Other); ++ + let element = self.upcast::(); + element.remove_attribute(cx, &ns!(), &LocalName::from("adopt-webview-id")); + element.remove_attribute(cx, &ns!(), &LocalName::from("adopt-browsing-context-id")); @@ -1018,6 +1029,11 @@ + let zoom = zoom.clamp(0.1, 10.0); + self.page_zoom.set(zoom); + ++ // Dirty the node so the parent reflows and the iframe-sizes path re-applies the new ++ // page zoom to the embedded webview's viewport. The embedded viewport comes from the ++ // parent's layout now, not from an out-of-band compositor message. ++ self.upcast::().dirty(NodeDamage::Other); ++ + let window = self.owner_window(); + window + .as_global_scope() @@ -1413,6 +1429,11 @@ + pub(crate) fn browsing_context_id(self) -> Option { + (self.unsafe_get()).browsing_context_id.get() + } ++ ++ #[inline] ++ pub(crate) fn page_zoom(self) -> f32 { ++ (self.unsafe_get()).page_zoom.get() as f32 ++ } +} + +impl HTMLWebViewElementMethods for HTMLWebViewElement { diff --git a/patches/components/script/dom/node/layout_dom.rs.patch b/patches/components/script/dom/node/layout_dom.rs.patch index 578e262..dcaf393 100644 --- a/patches/components/script/dom/node/layout_dom.rs.patch +++ b/patches/components/script/dom/node/layout_dom.rs.patch @@ -8,7 +8,7 @@ use crate::dom::html::input_element::HTMLInputElement; use crate::dom::shadowroot::ShadowRoot; use crate::dom::svg::svgsvgelement::SVGSVGElement; -@@ -314,13 +315,19 @@ +@@ -314,13 +315,27 @@ } pub(crate) fn iframe_browsing_context_id(self) -> Option { @@ -29,6 +29,14 @@ + } + self.downcast::() + .and_then(|webview| webview.pipeline_id()) ++ } ++ ++ /// The page zoom of an embedded ``, or None for a regular iframe / other node. ++ /// A regular iframe inherits page zoom from its parent, so it reports None; an embedded ++ /// webview is a separate top-level webview and carries its own zoom. ++ pub(crate) fn iframe_page_zoom(self) -> Option { ++ self.downcast::() ++ .map(|webview| webview.page_zoom()) } #[expect(unsafe_code)] diff --git a/patches/components/script/layout_dom/servo_layout_node.rs.patch b/patches/components/script/layout_dom/servo_layout_node.rs.patch new file mode 100644 index 0000000..2278159 --- /dev/null +++ b/patches/components/script/layout_dom/servo_layout_node.rs.patch @@ -0,0 +1,13 @@ +--- original ++++ modified +@@ -286,6 +286,10 @@ + self.node.iframe_pipeline_id() + } + ++ fn iframe_page_zoom(&self) -> Option { ++ self.node.iframe_page_zoom() ++ } ++ + fn table_span(&self) -> Option { + self.node + .downcast::() diff --git a/patches/components/shared/layout/layout_node.rs.patch b/patches/components/shared/layout/layout_node.rs.patch new file mode 100644 index 0000000..4cb7474 --- /dev/null +++ b/patches/components/shared/layout/layout_node.rs.patch @@ -0,0 +1,15 @@ +--- original ++++ modified +@@ -201,6 +201,12 @@ + /// not an iframe element, fails. Returns None if there is no nested browsing context. + fn iframe_pipeline_id(&self) -> Option; + ++ /// If this node is an embedded ``, returns its page zoom factor for rendering. ++ /// Returns None for a regular iframe (which inherits page zoom from its parent) or any ++ /// other node. Embedded webviews are separate top-level webviews, so their page zoom is ++ /// carried in the `ViewportDetails` reported to layout rather than inherited. ++ fn iframe_page_zoom(&self) -> Option; ++ + /// Return the table span property if it is an element that supports it. + fn table_span(&self) -> Option; + diff --git a/patches/components/shared/paint/lib.rs.patch b/patches/components/shared/paint/lib.rs.patch index f974f61..3923701 100644 --- a/patches/components/shared/paint/lib.rs.patch +++ b/patches/components/shared/paint/lib.rs.patch @@ -14,39 +14,29 @@ }; pub use webrender_api::ExternalImageSource; -use webrender_api::units::{DevicePixel, LayoutVector2D, TexelRect}; -+use webrender_api::units::{DevicePixel, DeviceRect, LayoutVector2D, TexelRect}; ++use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutVector2D, TexelRect}; use webrender_api::{ BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData, ExternalImageHandler, ExternalImageId, ExternalScrollId, FontInstanceFlags, FontInstanceKey, -@@ -189,6 +189,45 @@ +@@ -189,6 +189,35 @@ SendLCPCandidate(LCPCandidate, WebViewId, PipelineId, Epoch), /// Enable LCP calculation for the given WebView. EnableLCPCalculation(WebViewId), -+ /// Update the position and size of an embedded webview within its parent webview. -+ /// Used for routing input events to embedded webviews. -+ UpdateEmbeddedWebViewRect { -+ /// The embedded webview's ID. -+ embedded_webview_id: WebViewId, -+ /// The parent webview that contains this embedded webview. -+ parent_webview_id: WebViewId, -+ /// The rect of the embedded webview in device pixels, relative to the parent webview's origin. -+ rect: DeviceRect, -+ }, ++ /// Register a webview as embedded: it is composited in-tree via its parent's ++ /// `push_iframe`, so the painter must skip it in the root display list (and apply its page ++ /// zoom inside its own display list). Sent once by the constellation when the embedded ++ /// webview is created; its size, position and visibility all come from the parent's layout ++ /// of the `` element. ++ RegisterEmbeddedWebView(WebViewId), + /// Remove tracking for an embedded webview (called when the iframe is removed). + RemoveEmbeddedWebView(WebViewId), -+ /// Set whether an embedded webview is hidden (called when display:none is set on the iframe). -+ SetEmbeddedWebViewHidden { -+ /// The embedded webview's ID. -+ embedded_webview_id: WebViewId, -+ /// The parent webview that contains this embedded webview. -+ parent_webview_id: WebViewId, -+ /// Whether the embedded webview should be hidden. -+ hidden: bool, -+ }, + /// Take an encoded screenshot of an embedded webview. The screenshot is captured, encoded -+ /// to the specified format, and sent back via the provided IPC sender. ++ /// to the specified format, and sent back via the provided IPC sender. The size is the ++ /// device-pixel render size of the embedded webview (its `` box), supplied by the ++ /// constellation from the webview's viewport details. + TakeEncodedScreenshot( + WebViewId, ++ DeviceIntSize, + embedder_traits::EmbeddedWebViewScreenshotRequest, + GenericCallback< + Result< @@ -64,24 +54,18 @@ } impl Debug for PaintMessage { -@@ -536,6 +575,65 @@ +@@ -536,6 +565,47 @@ source, )); } + -+ /// Update the position and size of an embedded webview within its parent webview. -+ /// This is used for routing input events to embedded webviews. -+ pub fn update_embedded_webview_rect( -+ &self, -+ embedded_webview_id: WebViewId, -+ parent_webview_id: WebViewId, -+ rect: DeviceRect, -+ ) { -+ let _ = self.0.send(PaintMessage::UpdateEmbeddedWebViewRect { -+ embedded_webview_id, -+ parent_webview_id, -+ rect, -+ }); ++ /// Register a webview as embedded (composited in-tree via its parent's `push_iframe`), so ++ /// the painter skips it in the root display list. Sent once when the embedded webview is ++ /// created. ++ pub fn register_embedded_webview(&self, embedded_webview_id: WebViewId) { ++ let _ = self ++ .0 ++ .send(PaintMessage::RegisterEmbeddedWebView(embedded_webview_id)); + } + + /// Remove tracking for an embedded webview (called when the iframe is removed). @@ -91,26 +75,13 @@ + .send(PaintMessage::RemoveEmbeddedWebView(embedded_webview_id)); + } + -+ /// Set whether an embedded webview is hidden (called when display:none is set on the iframe). -+ pub fn set_embedded_webview_hidden( -+ &self, -+ embedded_webview_id: WebViewId, -+ parent_webview_id: WebViewId, -+ hidden: bool, -+ ) { -+ let _ = self.0.send(PaintMessage::SetEmbeddedWebViewHidden { -+ embedded_webview_id, -+ parent_webview_id, -+ hidden, -+ }); -+ } -+ + /// Take an encoded screenshot of an embedded webview. The screenshot is captured, + /// encoded to the specified format (PNG, JPEG, or WebP), and sent back via the + /// provided IPC callback. + pub fn request_encoded_screenshot( + &self, + webview_id: WebViewId, ++ size: DeviceIntSize, + request: embedder_traits::EmbeddedWebViewScreenshotRequest, + response_sender: GenericCallback< + Result< @@ -121,6 +92,7 @@ + ) { + if let Err(error) = self.0.send(PaintMessage::TakeEncodedScreenshot( + webview_id, ++ size, + request, + response_sender, + )) { diff --git a/ui/system/desktop/overview_card.css b/ui/system/desktop/overview_card.css index 31cd5e0..4535eef 100644 --- a/ui/system/desktop/overview_card.css +++ b/ui/system/desktop/overview_card.css @@ -46,7 +46,9 @@ } .thumb { - aspect-ratio: 4 / 3; + /* The view's real aspect ratio (from the screenshot's natural size), so the screenshot + fills it with no cropping. Falls back to 4:3 until the screenshot loads. */ + aspect-ratio: var(--thumb-aspect, 4 / 3); background: linear-gradient(135deg, oklch(75% 0.07 200), oklch(60% 0.1 240)); display: flex; align-items: center; diff --git a/ui/system/desktop/overview_items.js b/ui/system/desktop/overview_items.js index 843f9da..3ca7c03 100644 --- a/ui/system/desktop/overview_items.js +++ b/ui/system/desktop/overview_items.js @@ -51,6 +51,24 @@ class OverviewItemBase extends LitElement { if (changed.has("viewFav")) { this.style.setProperty("--card-fav", this.viewFav || ""); } + if (changed.has("screenshotUrl")) { + // Give the thumbnail the view's real aspect ratio so nothing is cropped. The + // screenshot is captured at the view's on-screen box, so its natural dimensions are + // that aspect. Loading is async; the CSS var updates the `.thumb` when it resolves. + this.style.removeProperty("--thumb-aspect"); + if (this.screenshotUrl) { + const img = new Image(); + img.addEventListener("load", () => { + if (img.naturalWidth > 0 && img.naturalHeight > 0) { + this.style.setProperty( + "--thumb-aspect", + `${img.naturalWidth} / ${img.naturalHeight}`, + ); + } + }); + img.src = this.screenshotUrl; + } + } } _thumbStyle() { diff --git a/ui/system/desktop/overview_slot.css b/ui/system/desktop/overview_slot.css index b267097..bec5f09 100644 --- a/ui/system/desktop/overview_slot.css +++ b/ui/system/desktop/overview_slot.css @@ -67,12 +67,20 @@ } .foot { + /* Overlaid as a caption at the bottom so the thumb fills the whole slot. The slot box + already mirrors the view's aspect (it's positioned from the desk's split tree), so a + full-height thumb + `cover` shows the view with no cropping; reserving footer height + in the flow would skew the thumb wider and crop top/bottom. */ + position: absolute; + left: 0; + right: 0; + bottom: 0; display: flex; align-items: center; gap: 6px; padding: 5px 9px; font-size: 10px; - background: var(--bg-icon); + background: color-mix(in oklab, var(--bg-icon), transparent 12%); border-top: 1px solid var(--color-border); } -- 2.51.2