diff --git a/patches/components/constellation/constellation.rs.patch b/patches/components/constellation/constellation.rs.patch index 4bdc0e1..57a4d6d 100644 --- a/patches/components/constellation/constellation.rs.patch +++ b/patches/components/constellation/constellation.rs.patch @@ -32,7 +32,7 @@ + KeyboardEvent, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, + MouseButton, MouseButtonAction, MouseButtonEvent, NewWebViewDetails, PaintHitTestResult, Theme, + ViewportDetails, WakeLockDelegate, WakeLockType, WebDriverCommandMsg, WebDriverLoadStatus, -+ WebDriverScriptCommand, ++ WebDriverScriptCommand, WebViewPoint, }; use euclid::Size2D; use euclid::default::Size2D as UntypedSize2D; @@ -491,21 +491,20 @@ ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => { // Unlikely at this point, but we may receive events coming from // different media sessions, so we set the active media session based -@@ -2025,8 +2262,13 @@ +@@ -2025,7 +2262,12 @@ }; self.active_media_session = Some(pipeline_id); self.constellation_to_embedder_proxy.send( - ConstellationToEmbedderMsg::MediaSessionEvent(webview_id, event), + ConstellationToEmbedderMsg::MediaSessionEvent(webview_id, event.clone()), - ); ++ ); + // Also route to embedded webview parent iframe. + self.handle_embedded_webview_notification( + webview_id, + EmbeddedWebViewEventType::MediaSessionEvent(event), -+ ); + ); }, #[cfg(feature = "webgpu")] - ScriptToConstellationMessage::RequestAdapter(response_sender, options, ids) => self @@ -2099,7 +2341,997 @@ } }, @@ -1470,7 +1469,7 @@ + } + } + return; - } ++ } + + // Handle peer disconnect: clean up remote channel state. + if let PairingEvent::PeerExpired { ref id } = event { @@ -1494,7 +1493,7 @@ + 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()) { @@ -1560,7 +1559,144 @@ } } -@@ -3325,6 +4606,40 @@ +@@ -3204,6 +4485,101 @@ + ); + } + ++ /// If the WebRender hit-test names a pipeline owned by an embedded webview (a ++ /// separate top-level webview composited in-tree via the parent's `push_iframe`), ++ /// return that webview's id along with the event rewritten to use the ++ /// WebRender-localized point in the embedded pipeline's CSS viewport. Otherwise the ++ /// inputs are returned unchanged. Only pointer events (which carry a coordinate) are ++ /// redirected; keyboard/IME follow focus. ++ fn redirect_pointer_event_to_embedded_webview( ++ &self, ++ webview_id: WebViewId, ++ mut event: InputEventAndId, ++ hit_test_result: &Option, ++ ) -> (WebViewId, InputEventAndId) { ++ // Only real mouse events take the compositor-direct path. Wheel is excluded: ++ // scrolling is handled by the compositor's own scroll path (notify_scroll_event), ++ // which hit-tests the shared WebRender scroll tree in shared-document coordinates. ++ // Touch is excluded too: it keeps the chrome DOM-forward path ++ // (forward_event_to_embedded_iframe_if_needed), which tracks the touch sequence ++ // (forwarded_touches) so a Down/Move/Up stays pinned to one webview and suppresses ++ // the parent's duplicate click synthesis. Redirecting per-event by hit-test loses ++ // that continuity and breaks taps (notably on the mobile UI, whose homescreen is ++ // itself an embedded webview). Compatibility mouse events synthesized from a touch ++ // tap are excluded for the same reason: they must follow the touch they mirror, not ++ // be re-hit-tested independently (else a keyboard-key tap's synthetic click lands on ++ // the input's webview and blurs it, closing the virtual keyboard). ++ let is_compatibility_event_for_touch = match &event.event { ++ InputEvent::MouseButton(inner) => inner.is_compatibility_event_for_touch, ++ InputEvent::MouseMove(inner) => inner.is_compatibility_event_for_touch, ++ _ => false, ++ }; ++ if is_compatibility_event_for_touch || ++ !matches!( ++ event.event, ++ InputEvent::MouseButton(_) | InputEvent::MouseMove(_) ++ ) ++ { ++ return (webview_id, event); ++ } ++ ++ let Some(hit_test_result) = hit_test_result.as_ref() else { ++ return (webview_id, event); ++ }; ++ ++ // The top-level webview that owns the hit pipeline. ++ let Some(target_webview_id) = self ++ .pipelines ++ .get(&hit_test_result.pipeline_id) ++ .map(|pipeline| pipeline.webview_id) ++ else { ++ return (webview_id, event); ++ }; ++ ++ // Only redirect to a different webview that is registered as embedded. ++ if target_webview_id == webview_id || ++ !self ++ .embedded_webview_to_iframe ++ .contains_key(&target_webview_id) ++ { ++ return (webview_id, event); ++ } ++ ++ // WebRender localizes the point into the hit pipeline's CSS viewport, but in the ++ // parent document's CSS scale. An embedded webview applies its page zoom inside ++ // its own display list, so its document's CSS viewport is scaled by `zoom` ++ // relative to the parent. Divide by that zoom to land in the embedded document's ++ // own coordinates (a no-op at 100% zoom). ++ let zoom = self ++ .browsing_contexts ++ .get(&BrowsingContextId::from(target_webview_id)) ++ .and_then(|browsing_context| browsing_context.viewport_details.page_zoom_for_rendering) ++ .unwrap_or(1.0); ++ let localized = (hit_test_result.point_in_viewport.to_vector() / zoom).to_point(); ++ let point = WebViewPoint::Page(localized); ++ event.event = match event.event { ++ InputEvent::MouseMove(mut inner) => { ++ inner.point = point; ++ InputEvent::MouseMove(inner) ++ }, ++ InputEvent::MouseButton(mut inner) => { ++ inner.point = point; ++ InputEvent::MouseButton(inner) ++ }, ++ InputEvent::Touch(mut inner) => { ++ inner.point = point; ++ InputEvent::Touch(inner) ++ }, ++ InputEvent::Wheel(mut inner) => { ++ inner.point = point; ++ InputEvent::Wheel(inner) ++ }, ++ other => other, ++ }; ++ ++ (target_webview_id, event) ++ } ++ + fn forward_input_event( + &mut self, + webview_id: WebViewId, +@@ -3223,6 +4599,34 @@ + let pressed_mouse_buttons = self.pressed_mouse_buttons; + let active_keyboard_modifiers = self.active_keyboard_modifiers; + ++ // Compositor-direct routing for embedded webviews. The WebRender hit-test now ++ // includes chrome overlays (every stacking context with pointer-events emits a ++ // hit item), so its topmost pipeline is trustworthy. If that pipeline belongs to ++ // an embedded webview (a separate top-level webview composited in-tree via the ++ // parent's push_iframe), deliver the pointer event straight to it using the ++ // WebRender-localized point. This supersedes the chrome-side ++ // forward_event_to_embedded_iframe_if_needed coordinate-transform path. ++ let (webview_id, event) = ++ self.redirect_pointer_event_to_embedded_webview(webview_id, event, &hit_test_result); ++ ++ // If a mouse Down landed on an embedded webview, re-fire the activation event on ++ // the chrome's element so it can take focus and update the topbar. ++ // Compositor-direct routing delivers the Down straight to the embedded webview, ++ // bypassing the chrome DOM hit-test (forward_event_to_embedded_iframe_if_needed) ++ // that used to fire this. Touch is intentionally excluded: on mobile the chrome's ++ // webview-focus handler churns state (pebble/carousel), which disrupts an in-progress ++ // tap; touch focus is handled separately. ++ let is_activation_down = matches!( ++ &event.event, ++ InputEvent::MouseButton(mouse_button) if mouse_button.action == MouseButtonAction::Down ++ ); ++ if is_activation_down && self.embedded_webview_to_iframe.contains_key(&webview_id) { ++ self.handle_embedded_webview_notification( ++ webview_id, ++ EmbeddedWebViewEventType::InputReceived, ++ ); ++ } ++ + 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 @@ /// fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { debug!("{webview_id}: Closing"); @@ -1601,7 +1737,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 +4916,27 @@ +@@ -3601,8 +5039,27 @@ opener_webview_id, opener_pipeline_id, response_sender, @@ -1629,7 +1765,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 +5035,392 @@ +@@ -3701,6 +5158,392 @@ }); } @@ -2022,7 +2158,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 +5970,7 @@ +@@ -4250,7 +6093,7 @@ }, }; @@ -2031,7 +2167,7 @@ match self.browsing_contexts.get_mut(&browsing_context_id) { Some(browsing_context) => { let old_pipeline_id = browsing_context.pipeline_id; -@@ -4259,6 +5979,7 @@ +@@ -4259,6 +6102,7 @@ old_pipeline_id, browsing_context.parent_pipeline_id, browsing_context.webview_id, @@ -2039,7 +2175,7 @@ ) }, None => { -@@ -4268,6 +5989,15 @@ +@@ -4268,6 +6112,15 @@ self.unload_document(old_pipeline_id); @@ -2055,7 +2191,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 +6566,7 @@ +@@ -4836,7 +6689,7 @@ } #[servo_tracing::instrument(skip_all)] @@ -2064,7 +2200,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 +6678,22 @@ +@@ -4948,9 +6801,22 @@ self.constellation_to_embedder_proxy .send(ConstellationToEmbedderMsg::HistoryChanged( webview_id, @@ -2088,7 +2224,7 @@ } #[servo_tracing::instrument(skip_all)] -@@ -4969,7 +6712,7 @@ +@@ -4969,7 +6835,7 @@ webview.focused_browsing_context_id = change.browsing_context_id; } @@ -2097,7 +2233,7 @@ match self.browsing_contexts.get_mut(&change.browsing_context_id) { Some(browsing_context) => { debug!("Adding pipeline to existing browsing context."); -@@ -4976,11 +6719,15 @@ +@@ -4976,11 +6842,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); @@ -2115,7 +2251,7 @@ }, }; -@@ -4988,6 +6735,18 @@ +@@ -4988,6 +6858,18 @@ self.unload_document(old_pipeline_id); } diff --git a/patches/components/layout/display_list/mod.rs.patch b/patches/components/layout/display_list/mod.rs.patch index 2531ee0..5ac5247 100644 --- a/patches/components/layout/display_list/mod.rs.patch +++ b/patches/components/layout/display_list/mod.rs.patch @@ -161,3 +161,38 @@ { return false; } +@@ -1528,6 +1597,34 @@ + .translate(self.containing_block_origin.to_vector()) + .to_webrender(), + ); ++ } else if (self ++ .fragment ++ .style() ++ .establishes_stacking_context(self.fragment.base.flags) || ++ self.fragment ++ .style() ++ .clone_position() ++ .is_absolutely_positioned()) && ++ self.fragment.style().get_inherited_ui().pointer_events != ++ style::computed_values::pointer_events::T::None ++ { ++ // Servo only emits WebRender hit-test items for pipeline roots and scroll ++ // containers, so a chrome overlay (menu, dialog, backdrop) that is either a ++ // stacking context or an out-of-flow positioned box produces no compositor hit ++ // item and is invisible to WebRender hit-testing. An embedded webview's ++ // full-viewport root hit item then captures every point over it, so such ++ // overlays cannot intercept input above an embedded webview. Emit a hit item ++ // for these so they rank above the embedded pipeline in the shared scene (they ++ // paint later), letting compositor-direct routing see them. Element targeting ++ // within a pipeline still uses layout's own hit_test (elementFromPoint). ++ self.build_hit_test( ++ builder, ++ state, ++ self.fragment ++ .border_rect() ++ .translate(self.containing_block_origin.to_vector()) ++ .to_webrender(), ++ ); + } + } + diff --git a/patches/components/paint/paint.rs.patch b/patches/components/paint/paint.rs.patch index ed1af43..7a03eae 100644 --- a/patches/components/paint/paint.rs.patch +++ b/patches/components/paint/paint.rs.patch @@ -211,96 +211,24 @@ /// 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,10 +922,37 @@ +@@ -766,6 +922,9 @@ #[cfg(feature = "webxr")] self.webxr_main_thread.borrow_mut().run_one_frame(); -+ // Collect all unconsumed scroll events from embedded webviews -+ let mut all_unconsumed_scrolls = Vec::new(); ++ // Each painter drains its queued scroll/zoom events and resolves scroll against ++ // its shared WebRender scene, chaining an embedded webview to its parent through ++ // the compositor hit-test. No cross-painter bubbling is required. for painter in &self.painters { -- painter.borrow_mut().perform_updates(); -+ let unconsumed = painter.borrow_mut().perform_updates(); -+ all_unconsumed_scrolls.extend(unconsumed); + painter.borrow_mut().perform_updates(); } - -+ // Forward unconsumed scroll events to parent webviews. -+ for (embedded_webview_id, scroll_event) in all_unconsumed_scrolls { -+ // Find the parent webview for this embedded webview -+ let parent_webview_id = self.embedded_webview_rects.borrow().iter().find_map( -+ |(parent_id, embedded_map)| { -+ if embedded_map.contains_key(&embedded_webview_id) { -+ Some(*parent_id) -+ } else { -+ None -+ } -+ }, -+ ); -+ -+ if let Some(parent_id) = parent_webview_id { -+ if let Some(mut painter) = self.maybe_painter_mut(parent_id.into()) { -+ // Try to scroll any scrollable node in the parent document. -+ // We skip hit-testing because the iframe_rect is in layout coordinates, -+ // not visual coordinates (post-scroll transform), so hit-testing -+ // at that position doesn't work correctly after the parent has scrolled. -+ painter.try_scroll_any_and_send_to_webrender(parent_id, scroll_event.scroll); -+ } -+ } -+ } -+ - self.shutdown_state() != ShutdownState::FinishedShuttingDown - } - -@@ -819,6 +1002,53 @@ +@@ -819,6 +978,10 @@ if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } + -+ // Check if this event should be forwarded to an embedded webview. -+ let scale = self.device_pixels_per_page_pixel(webview_id); -+ let device_point = point.as_device_point(scale); -+ -+ // First, find candidate embedded webviews that contain the point -+ let candidate_ids: Vec = self -+ .embedded_webview_rects -+ .borrow() -+ .get(&webview_id) -+ .map(|embedded_map| { -+ embedded_map -+ .iter() -+ .filter(|(_, rect)| rect.contains(device_point)) -+ .map(|(id, _)| *id) -+ .collect() -+ }) -+ .unwrap_or_default(); -+ -+ // Then check which one is visible (not hidden) -+ for embedded_id in candidate_ids { -+ let is_visible = self -+ .maybe_painter(webview_id.into()) -+ .map(|painter| { -+ painter -+ .webview_renderer(embedded_id) -+ .is_some_and(|renderer| !renderer.hidden()) -+ }) -+ .unwrap_or(false); -+ -+ if is_visible { -+ // Try to scroll the embedded webview first. If it consumes the scroll -+ // (i.e., has scrollable content that was scrolled), we're done. -+ // Otherwise, fall through to let the parent webview handle the scroll. -+ let consumed = self.painter_mut(embedded_id.into()).try_scroll_at_point( -+ embedded_id, -+ scroll, -+ point, -+ ); -+ -+ if consumed { -+ return; -+ } -+ // Fall through to parent if embedded didn't consume the scroll -+ } -+ } -+ ++ // Enqueue the scroll on the webview that received the event. Resolution against the ++ // shared WebRender scene, including chaining an embedded webview to its parent, ++ // happens later in perform_updates via Painter::scroll_at_point. self.painter_mut(webview_id.into()) .notify_scroll_event(webview_id, scroll, point); } diff --git a/patches/components/paint/painter.rs.patch b/patches/components/paint/painter.rs.patch index 3d918bd..8397b13 100644 --- a/patches/components/paint/painter.rs.patch +++ b/patches/components/paint/painter.rs.patch @@ -19,7 +19,7 @@ }; use webrender_api::{ self, BuiltDisplayList, BuiltDisplayListDescriptor, ColorF, DirtyRect, DisplayListPayload, -@@ -81,7 +82,14 @@ +@@ -81,8 +82,15 @@ pub(crate) painter_id: PainterId, /// Our [`WebViewRenderer`]s, one for every `WebView`. @@ -27,14 +27,15 @@ + /// 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, @@ -43,17 +44,14 @@ rendering_context, needs_repaint: Cell::default(), pending_frames: Default::default(), -@@ -290,20 +299,32 @@ +@@ -290,20 +299,37 @@ painter } -- pub(crate) fn perform_updates(&mut self) { -+ /// Process pending scroll and zoom events for all webview renderers. -+ /// Returns a list of (webview_id, unconsumed_scroll) tuples for scroll events -+ /// that were not consumed by embedded webviews and should be forwarded to parents. -+ pub(crate) fn perform_updates( -+ &mut self, -+ ) -> Vec<(WebViewId, crate::webview_renderer::ScrollEvent)> { ++ /// Process pending scroll and zoom events for all webview renderers, resolving each ++ /// scroll within the webview it arrived on and chaining an embedded webview to its ++ /// parent when it can't consume the scroll. + pub(crate) fn perform_updates(&mut self) { + // The WebXR thread may make a different context current + if let Err(err) = self.rendering_context.make_current() { + warn!("Failed to make the rendering context current: {:?}", err); @@ -70,35 +68,137 @@ - scroll_result - }) - .collect(); -+ let mut unconsumed_scrolls = Vec::new(); -+ let mut scroll_offset_updates = Vec::new(); - -- self.send_zoom_and_scroll_offset_updates(need_zoom, scroll_offset_updates); ++ // Pass 1: drain each renderer's queued events, applying pinch-zoom pan, and collect ++ // the resulting scrolls (delta + cursor) tagged with the renderer they arrived on. ++ let mut pending_scrolls = Vec::new(); + for (webview_id, webview_renderer) in self.webview_renderers.iter_mut() { -+ let result = -+ webview_renderer.process_pending_scroll_and_pinch_zoom_events(&self.webrender_api); ++ let result = webview_renderer.process_pending_scroll_and_pinch_zoom_events(); + need_zoom = need_zoom || (result.pinch_zoom_result == PinchZoomResult::DidPinchZoom); -+ if let Some(scroll_result) = result.scroll_result { -+ scroll_offset_updates.push(scroll_result); ++ if let Some(scroll_event) = result.pending_scroll { ++ pending_scrolls.push((*webview_id, scroll_event)); + } -+ if let Some(unconsumed_scroll) = result.unconsumed_scroll { -+ unconsumed_scrolls.push((*webview_id, unconsumed_scroll)); ++ } + +- self.send_zoom_and_scroll_offset_updates(need_zoom, scroll_offset_updates); ++ // Pass 2: resolve each scroll within the webview it arrived on, chaining an embedded ++ // webview to its parent when it can't consume the scroll. ++ let mut scroll_offset_updates = Vec::new(); ++ for (entry_webview_id, scroll_event) in pending_scrolls { ++ if let Some(scroll_result) = ++ self.scroll_at_point(entry_webview_id, scroll_event.scroll, scroll_event.point) ++ { ++ scroll_offset_updates.push(scroll_result); + } + } if let Some(colors) = self.web_content_animator.update(&self.webview_renderers) { let mut transaction = Transaction::new(); -@@ -316,6 +337,9 @@ +@@ -316,6 +342,104 @@ self.generate_frame(&mut transaction, RenderReasons::ANIMATED_PROPERTY); self.send_transaction(transaction); } + + self.send_zoom_and_scroll_offset_updates(need_zoom, scroll_offset_updates); -+ unconsumed_scrolls ++ } ++ ++ /// 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 ++ /// wheel over an embedded `` to that webview in the webview's OWN local ++ /// coordinates, and touch pans are dispatched to the embedded webview directly. So we ++ /// resolve within the entry renderer: scroll the node the cursor hits in one of the ++ /// entry's own pipelines, else fall back to the entry's root pipeline. Hit-testing the ++ /// shared world-space scene with these local coordinates would land elsewhere, so we ++ /// never scroll a pipeline the entry does not own here. ++ /// ++ /// If the entry is an embedded webview and cannot consume the scroll (its content is at ++ /// the limit), chain to its parent so scrolling past a page edge still moves the parent's ++ /// scrollable container (e.g. switches a horizontal panel). An embedded webview shares ++ /// its parent's Painter, so the parent is the non-embedded renderer here. ++ pub(crate) fn scroll_at_point( ++ &mut self, ++ entry: WebViewId, ++ scroll: Scroll, ++ cursor: DevicePoint, ++ ) -> Option { ++ if let Some(result) = self.scroll_within_entry(entry, scroll, cursor) { ++ return Some(result); ++ } ++ ++ if self.embedded_webview_ids.contains(&entry) { ++ if let Some(parent_id) = self ++ .webview_renderers ++ .keys() ++ .find(|id| !self.embedded_webview_ids.contains(*id)) ++ .copied() ++ { ++ if let Some(result) = self ++ .webview_renderers ++ .get_mut(&parent_id) ++ .and_then(|parent| parent.scroll_any(scroll)) ++ { ++ return Some(result); ++ } ++ } ++ } ++ ++ if let Some(entry_renderer) = self.webview_renderers.get_mut(&entry) { ++ entry_renderer.stop_fling(); ++ } ++ None ++ } ++ ++ /// Scroll the node the `cursor` hits in one of the `entry` renderer's own pipelines, ++ /// falling back to the entry's root pipeline. Returns None if the entry could not consume ++ /// the scroll. ++ fn scroll_within_entry( ++ &mut self, ++ entry: WebViewId, ++ scroll: Scroll, ++ cursor: DevicePoint, ++ ) -> Option { ++ // While touch-panning, the scrolled node is pinned on the entry renderer; reuse it. ++ // Otherwise hit-test the shared document at the cursor. ++ let cached = self ++ .webview_renderers ++ .get(&entry) ++ .and_then(|renderer| renderer.cached_scroll_hit_test()); ++ let hit_test_results = cached.map(|result| vec![result]).unwrap_or_else(|| { ++ Self::hit_test_at_point_with_api_and_document( ++ &self.webrender_api, ++ self.webrender_document, ++ cursor, ++ ) ++ }); ++ ++ // Walk results front-to-back, one node per pipeline, scrolling the first node OWNED ++ // BY THE ENTRY that consumes. ++ let mut previous_pipeline_id = None; ++ for hit_test_result in hit_test_results { ++ if previous_pipeline_id.replace(hit_test_result.pipeline_id) == ++ Some(hit_test_result.pipeline_id) ++ { ++ continue; ++ } ++ let entry_renderer = self.webview_renderers.get_mut(&entry)?; ++ if !entry_renderer.owns_pipeline(hit_test_result.pipeline_id) { ++ continue; ++ } ++ if let Some(result) = ++ entry_renderer.scroll_specific_node(hit_test_result.clone(), scroll) ++ { ++ return Some(result); ++ } ++ } ++ ++ // Fallback: scroll the entry's own root pipeline (document viewport). ++ self.webview_renderers ++ .get_mut(&entry) ++ .and_then(|entry_renderer| entry_renderer.scroll_root_pipeline(scroll)) } #[track_caller] -@@ -609,7 +633,16 @@ +@@ -609,7 +733,16 @@ let root_clip_id = builder.define_clip_rect(root_reference_frame, viewport_rect); let clip_chain_id = builder.define_clip_chain(None, [root_clip_id]); @@ -115,7 +215,7 @@ if webview_renderer.hidden() { continue; } -@@ -669,7 +702,7 @@ +@@ -669,7 +802,7 @@ /// Set the root pipeline for our WebRender scene to a display list that consists of an iframe /// for each visible top-level browsing context, applying a transformation on the root for /// pinch zoom, page zoom, and HiDPI scaling. @@ -124,7 +224,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 +709,100 @@ +@@ -676,6 +809,100 @@ self.send_transaction(transaction); } @@ -225,29 +325,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 -@@ -735,6 +862,21 @@ - self.send_transaction(transaction); - } - -+ /// Send a single scroll result to WebRender. This is used when forwarding -+ /// unconsumed scroll events from embedded webviews to their parent. -+ pub(crate) fn send_scroll_result_to_webrender(&mut self, scroll_result: ScrollResult) { -+ let mut transaction = Transaction::new(); -+ transaction.set_scroll_offsets( -+ scroll_result.external_scroll_id, -+ vec![SampledScrollOffset { -+ offset: scroll_result.offset, -+ generation: 0, -+ }], -+ ); -+ self.generate_frame(&mut transaction, RenderReasons::APZ); -+ self.send_transaction(transaction); -+ } -+ - pub(crate) fn toggle_webrender_debug(&mut self, option: WebRenderDebugOption) { - let Some(renderer) = self.webrender_renderer.as_mut() else { - return; -@@ -805,6 +947,26 @@ +@@ -805,6 +1032,26 @@ self.send_root_pipeline_display_list(); } @@ -274,7 +352,7 @@ pub(crate) fn set_throttled( &mut self, webview_id: WebViewId, -@@ -1204,15 +1366,23 @@ +@@ -1204,15 +1451,23 @@ webview: Box, viewport_details: ViewportDetails, ) { @@ -302,46 +380,7 @@ } pub(crate) fn remove_webview(&mut self, webview_id: WebViewId) { -@@ -1361,6 +1531,38 @@ - self.lcp_calculator.enabled_for_webview(webview_id) - } - -+ /// Attempt to scroll at the given point. Returns true if scroll was consumed. -+ /// This is used for embedded webviews to check if the scroll should bubble up to the parent. -+ pub(crate) fn try_scroll_at_point( -+ &mut self, -+ webview_id: WebViewId, -+ scroll: Scroll, -+ point: WebViewPoint, -+ ) -> bool { -+ let Some(webview_renderer) = self.webview_renderers.get_mut(&webview_id) else { -+ return false; -+ }; -+ let device_point = point.as_device_point(webview_renderer.device_pixels_per_page_pixel()); -+ webview_renderer -+ .scroll_node_at_device_point(&self.webrender_api, device_point, scroll) -+ .is_some() -+ } -+ -+ /// Try to scroll any scrollable node in the webview and send the result to WebRender. -+ /// This is used for bubbling scroll events from embedded iframes when hit-testing fails. -+ pub(crate) fn try_scroll_any_and_send_to_webrender( -+ &mut self, -+ webview_id: WebViewId, -+ scroll: Scroll, -+ ) { -+ let Some(webview_renderer) = self.webview_renderers.get_mut(&webview_id) else { -+ return; -+ }; -+ if let Some(scroll_result) = webview_renderer.try_scroll_any(scroll) { -+ self.send_scroll_result_to_webrender(scroll_result); -+ } -+ } -+ - pub(crate) fn adjust_pinch_zoom( - &mut self, - webview_id: WebViewId, -@@ -1414,7 +1616,6 @@ +@@ -1414,7 +1669,6 @@ result: InputEventResult, ) { let Some(webview_renderer) = self.webview_renderers.get_mut(&webview_id) else { diff --git a/patches/components/paint/webview_renderer.rs.patch b/patches/components/paint/webview_renderer.rs.patch index ad257f9..f3a73dd 100644 --- a/patches/components/paint/webview_renderer.rs.patch +++ b/patches/components/paint/webview_renderer.rs.patch @@ -9,7 +9,19 @@ pub(crate) struct ScrollEvent { /// Scroll by this offset, or to Start or End pub scroll: Scroll, -@@ -75,6 +75,18 @@ +@@ -60,11 +60,7 @@ + + #[derive(Clone, Debug)] + pub(crate) struct ScrollResult { +- pub hit_test_result: PaintHitTestResult, + /// The [`ExternalScrollId`] of the node that was actually scrolled. +- /// +- /// Note that this is an inclusive ancestor of `external_scroll_id` in +- /// [`Self::hit_test_result`]. + pub external_scroll_id: ExternalScrollId, + pub offset: LayoutVector2D, + } +@@ -75,6 +71,17 @@ DidNotPinchZoom, } @@ -18,17 +30,16 @@ +pub(crate) struct ScrollZoomProcessingResult { + /// Whether pinch zoom occurred. + pub pinch_zoom_result: PinchZoomResult, -+ /// The scroll result if scrolling was consumed. -+ pub scroll_result: Option, -+ /// The unconsumed scroll event if scrolling was not consumed. -+ /// This can be used to bubble the scroll to a parent webview. -+ pub unconsumed_scroll: Option, ++ /// The combined (post-pinch-zoom-pan) scroll to resolve against the shared scene, ++ /// if any. Cross-pipeline resolution (chaining an embedded webview to its parent) ++ /// is done by the Painter, which owns every renderer sharing the WebRender document. ++ pub pending_scroll: Option, +} + /// A renderer for a libservo `WebView`. This is essentially the [`ServoRenderer`]'s interface to a /// libservo `WebView`, but the code here cannot depend on libservo in order to prevent circular /// dependencies, which is why we store a `dyn WebViewTrait` here instead of the `WebView` itself. -@@ -115,9 +127,11 @@ +@@ -115,9 +122,11 @@ /// A [`ViewportDescription`] for this [`WebViewRenderer`], which contains the limitations /// and initial values for zoom derived from the `viewport` meta tag in web content. viewport_description: ViewportDescription, @@ -41,7 +52,7 @@ // // Data that is shared with the parent renderer. -@@ -158,6 +172,7 @@ +@@ -158,6 +167,7 @@ animating: false, viewport_description: Default::default(), screen_size: viewport_details.device_size, @@ -49,7 +60,7 @@ embedder_to_constellation_sender, refresh_driver, webrender_document, -@@ -193,6 +208,16 @@ +@@ -193,6 +203,16 @@ new_value != old_value } @@ -66,7 +77,7 @@ /// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed. pub(crate) fn ensure_pipeline_details( &mut self, -@@ -388,16 +413,14 @@ +@@ -388,16 +408,14 @@ .map(|point| point.as_device_point(self.device_pixels_per_page_pixel())); let hit_test_result = match event_point { Some(point) => { @@ -88,96 +99,25 @@ }, None => None, }; -@@ -745,6 +768,88 @@ - self.on_scroll_window_event(scroll, point); - } - -+ /// Try to scroll the root scroll node in the root pipeline without hit testing. -+ /// Only tries the root scroll node (document viewport) to allow proper scroll -+ /// bubbling to parent webviews when the embedded content can't scroll in the -+ /// requested direction. -+ /// Returns the scroll result without dispatching scroll events (caller should dispatch). -+ fn try_scroll_root_pipeline( -+ &mut self, -+ scroll_location: ScrollLocation, -+ ) -> Option { -+ let root_pipeline_id = self.root_pipeline_id?; -+ let root_pipeline = self.pipelines.get_mut(&root_pipeline_id)?; -+ -+ // Only try the root scroll node (ExternalScrollId(0, pipeline_id)), not all nodes. -+ // This ensures that if the document viewport can't scroll in the requested -+ // direction, the scroll event bubbles up to the parent webview instead of -+ // being captured by some random scrollable element elsewhere on the page. -+ let root_scroll_id = ExternalScrollId(0, root_pipeline_id.into()); -+ let (external_scroll_id, offset) = root_pipeline.scroll_tree.scroll_node_or_ancestor( -+ root_scroll_id, -+ scroll_location, -+ ScrollType::InputEvents, -+ )?; -+ -+ let hit_test_result = PaintHitTestResult { -+ pipeline_id: root_pipeline_id, -+ point_in_viewport: Default::default(), -+ external_scroll_id, -+ }; -+ -+ Some(ScrollResult { -+ hit_test_result, -+ external_scroll_id, -+ offset, -+ }) -+ } -+ -+ /// Try to scroll any scrollable node in the parent document. -+ /// This is used for bubbling scroll events from embedded iframes where -+ /// hit-testing in layout coordinates doesn't work because the visual -+ /// position has changed due to scrolling. -+ /// -+ /// Unlike `try_scroll_root_pipeline` which only tries the root scroll node, -+ /// this method tries ALL scroll nodes because the parent's scrollable element -+ /// (like a horizontal panel container) might not be the root scroll node. -+ pub(crate) fn try_scroll_any(&mut self, scroll: Scroll) -> Option { -+ let device_pixels_per_page_pixel = self.device_pixels_per_page_pixel(); -+ -+ let scroll_location = match scroll { -+ Scroll::Delta(delta) => { -+ let delta = delta.as_device_vector(device_pixels_per_page_pixel); -+ let delta_for_scroll = delta / device_pixels_per_page_pixel; -+ ScrollLocation::Delta(delta_for_scroll.cast_unit()) -+ }, -+ Scroll::Start => ScrollLocation::Start, -+ Scroll::End => ScrollLocation::End, -+ }; -+ -+ let root_pipeline_id = self.root_pipeline_id?; -+ let root_pipeline = self.pipelines.get_mut(&root_pipeline_id)?; -+ -+ // Try any scrollable node in the tree, not just the root. -+ // This is needed for parent bubbling because the scrollable element -+ // (like a horizontal panel container) might not be the root scroll node. -+ let (external_scroll_id, offset) = root_pipeline -+ .scroll_tree -+ .try_scroll_any_node(scroll_location, ScrollType::InputEvents)?; -+ -+ let hit_test_result = PaintHitTestResult { -+ pipeline_id: root_pipeline_id, -+ point_in_viewport: Default::default(), -+ external_scroll_id, -+ }; -+ -+ self.send_scroll_positions_to_layout_for_pipeline(root_pipeline_id, external_scroll_id); -+ -+ Some(ScrollResult { -+ hit_test_result, -+ external_scroll_id, -+ offset, -+ }) -+ } -+ - fn on_scroll_window_event(&mut self, scroll: Scroll, cursor: DevicePoint) { - self.pending_scroll_zoom_events - .push(ScrollZoomEvent::Scroll(ScrollEvent { -@@ -753,18 +858,25 @@ +@@ -722,7 +740,7 @@ + ); + self.dispatch_input_event_with_hit_testing( + render_api, +- InputEvent::MouseButton(MouseButtonEvent::new( ++ InputEvent::MouseButton(MouseButtonEvent::new_compatibility_for_touch( + MouseButtonAction::Down, + button, + point.into(), +@@ -731,7 +749,7 @@ + ); + self.dispatch_input_event_with_hit_testing( + render_api, +- InputEvent::MouseButton(MouseButtonEvent::new( ++ InputEvent::MouseButton(MouseButtonEvent::new_compatibility_for_touch( + MouseButtonAction::Up, + button, + point.into(), +@@ -753,18 +771,23 @@ })); } @@ -195,120 +135,253 @@ /// It is up to the caller to ensure that these events update the rendering appropriately. pub(crate) fn process_pending_scroll_and_pinch_zoom_events( &mut self, - render_api: &RenderApi, +- render_api: &RenderApi, - ) -> (PinchZoomResult, Option) { + ) -> ScrollZoomProcessingResult { if self.pending_scroll_zoom_events.is_empty() { - return (PinchZoomResult::DidNotPinchZoom, None); + return ScrollZoomProcessingResult { + pinch_zoom_result: PinchZoomResult::DidNotPinchZoom, -+ scroll_result: None, -+ unconsumed_scroll: None, ++ pending_scroll: None, + }; } // Batch up all scroll events and changes to pinch zoom into a single change, or -@@ -813,15 +925,24 @@ +@@ -813,101 +836,136 @@ } } -+ // Save the original scroll before pan() modifies it, so we can return it -+ // as unconsumed if neither pan nor scroll consumed the event. -+ let original_scroll_event = combined_scroll_event; -+ - // When zoomed in via pinch zoom, first try to move the center of the zoom and use the rest - // of the delta for scrolling. This allows moving the zoomed into viewport around in the - // unzoomed viewport before actually scrolling the underlying layers. -- if let Some(combined_scroll_event) = combined_scroll_event.as_mut() { -- new_pinch_zoom.pan( -- &mut combined_scroll_event.scroll, -- self.device_pixels_per_page_pixel(), +- // When zoomed in via pinch zoom, first try to move the center of the zoom and use the rest +- // of the delta for scrolling. This allows moving the zoomed into viewport around in the +- // unzoomed viewport before actually scrolling the underlying layers. ++ // When zoomed in via pinch zoom, first try to move the center of the zoom and use ++ // the rest of the delta for scrolling. This allows moving the zoomed-into viewport ++ // around before actually scrolling the underlying layers. Any remaining delta stays ++ // in `combined_scroll_event` for the Painter to resolve. + if let Some(combined_scroll_event) = combined_scroll_event.as_mut() { + new_pinch_zoom.pan( + &mut combined_scroll_event.scroll, + self.device_pixels_per_page_pixel(), - ) - } -+ let pan_consumed_scroll = -+ if let Some(combined_scroll_event) = combined_scroll_event.as_mut() { -+ let original_scroll = combined_scroll_event.scroll; -+ new_pinch_zoom.pan( -+ &mut combined_scroll_event.scroll, -+ self.device_pixels_per_page_pixel(), -+ ); -+ original_scroll != combined_scroll_event.scroll -+ } else { -+ false -+ }; +- +- let scroll_result = combined_scroll_event.and_then(|combined_event| { +- self.scroll_node_at_device_point( +- render_api, +- combined_event.point.to_f32(), +- combined_event.scroll, +- ) +- }); +- if let Some(ref scroll_result) = scroll_result { +- self.send_scroll_positions_to_layout_for_pipeline( +- scroll_result.hit_test_result.pipeline_id, +- scroll_result.external_scroll_id, + ); +- } else { +- self.touch_handler.stop_fling_if_needed(); + } - let scroll_result = combined_scroll_event.and_then(|combined_event| { - self.scroll_node_at_device_point( -@@ -830,6 +951,21 @@ - combined_event.scroll, - ) - }); -+ -+ // Determine if the scroll was consumed or not. -+ // If scroll failed and pan didn't consume anything, return the original scroll event -+ // so it can bubble up to the parent. If pan consumed some delta, return the remaining -+ // (post-pan) scroll as unconsumed. -+ let unconsumed_scroll = if scroll_result.is_some() { -+ None -+ } else if pan_consumed_scroll { -+ // Pan consumed some scroll, return the remaining delta (which might be zero) -+ combined_scroll_event -+ } else { -+ // Nothing consumed the scroll, return the original to bubble up -+ original_scroll_event -+ }; -+ - if let Some(ref scroll_result) = scroll_result { - self.send_scroll_positions_to_layout_for_pipeline( - scroll_result.hit_test_result.pipeline_id, -@@ -845,7 +981,11 @@ +- // Additionally notify pinch zoom update to the script. ++ // Notify pinch zoom update to the script. The scroll itself is resolved by the ++ // Painter (`scroll_at_point`), which can chain across the embedded/parent pipeline ++ // boundary since all top-level webviews share one WebRender document. + let pinch_zoom_result = self.set_pinch_zoom(new_pinch_zoom); + if pinch_zoom_result == PinchZoomResult::DidPinchZoom { self.send_pinch_zoom_infos_to_script(); } - (pinch_zoom_result, scroll_result) + ScrollZoomProcessingResult { + pinch_zoom_result, -+ scroll_result, -+ unconsumed_scroll, ++ 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`] -@@ -852,7 +992,7 @@ - /// scrolling to the applicable scroll node under that point. If a scroll was - /// performed, returns the hit test result contains [`PipelineId`] of the node - /// scrolled, the id, and the final scroll delta. +- /// Perform a hit test at the given [`DevicePoint`] and apply the [`Scroll`] +- /// scrolling to the applicable scroll node under that point. If a scroll was +- /// performed, returns the hit test result contains [`PipelineId`] of the node +- /// scrolled, the id, and the final scroll delta. - fn scroll_node_at_device_point( -+ pub(crate) fn scroll_node_at_device_point( - &mut self, - render_api: &RenderApi, - cursor: DevicePoint, -@@ -880,7 +1020,10 @@ - // its ancestor pipelines. - let mut previous_pipeline_id = None; - for hit_test_result in hit_test_results { +- &mut self, +- render_api: &RenderApi, +- cursor: DevicePoint, +- scroll: Scroll, +- ) -> Option { +- let scroll_location = match scroll { ++ /// Convert a [`Scroll`] into a [`ScrollLocation`] using this renderer's own scale. ++ fn scroll_location_for(&self, scroll: Scroll) -> ScrollLocation { ++ match scroll { + Scroll::Delta(delta) => { + let device_pixels_per_page = self.device_pixels_per_page_pixel(); +- let calculate_delta = +- delta.as_device_vector(device_pixels_per_page) / device_pixels_per_page; +- ScrollLocation::Delta(calculate_delta.cast_unit()) ++ let delta = delta.as_device_vector(device_pixels_per_page) / device_pixels_per_page; ++ ScrollLocation::Delta(delta.cast_unit()) + }, + Scroll::Start => ScrollLocation::Start, + Scroll::End => ScrollLocation::End, +- }; ++ } ++ } ++ ++ /// Scroll the node named by `hit_test_result` (or its nearest scrollable ancestor) in ++ /// this renderer, using this renderer's own scale, and notify layout. Returns the ++ /// result if a node actually scrolled. Used by the Painter's `scroll_at_point` for the ++ /// pipeline the cursor hits in the entry renderer. ++ pub(crate) fn scroll_specific_node( ++ &mut self, ++ hit_test_result: PaintHitTestResult, ++ scroll: Scroll, ++ ) -> Option { ++ let scroll_location = self.scroll_location_for(scroll); ++ let details = self.pipelines.get_mut(&hit_test_result.pipeline_id)?; ++ let (external_scroll_id, offset) = details.scroll_tree.scroll_node_or_ancestor( ++ hit_test_result.external_scroll_id, ++ scroll_location, ++ ScrollType::InputEvents, ++ )?; ++ self.send_scroll_positions_to_layout_for_pipeline( ++ hit_test_result.pipeline_id, ++ external_scroll_id, ++ ); ++ // Pin the scrolled node for the current touch/pan sequence (a no-op outside a touch ++ // sequence) so a fling keeps scrolling the same node. ++ self.cache_scroll_hit_test(hit_test_result); ++ Some(ScrollResult { ++ external_scroll_id, ++ offset, ++ }) ++ } ++ ++ /// 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 ++ /// coordinates, which do not hit-test against the shared world-space scene. Returns the ++ /// result if the root scrolled. ++ pub(crate) fn scroll_root_pipeline(&mut self, scroll: Scroll) -> Option { ++ let scroll_location = self.scroll_location_for(scroll); ++ let root_pipeline_id = self.root_pipeline_id?; ++ let root_pipeline = self.pipelines.get_mut(&root_pipeline_id)?; ++ // Only try the root scroll node (ExternalScrollId(0, pipeline_id)); if it can't ++ // scroll in the requested direction the caller chains to the parent instead of ++ // capturing the scroll in some unrelated scrollable element. ++ let root_scroll_id = ExternalScrollId(0, root_pipeline_id.into()); ++ let (external_scroll_id, offset) = root_pipeline.scroll_tree.scroll_node_or_ancestor( ++ root_scroll_id, ++ scroll_location, ++ ScrollType::InputEvents, ++ )?; ++ self.send_scroll_positions_to_layout_for_pipeline(root_pipeline_id, external_scroll_id); ++ Some(ScrollResult { ++ external_scroll_id, ++ offset, ++ }) ++ } ++ ++ /// Scroll any scrollable node in this renderer's root pipeline (depth-first). Used to ++ /// chain an embedded webview's unconsumed scroll to its parent, whose scrollable ++ /// container (e.g. a horizontal panel strip) is not necessarily the root scroll node and ++ /// whose on-screen position is in layout, not visual, coordinates. ++ pub(crate) fn scroll_any(&mut self, scroll: Scroll) -> Option { ++ let scroll_location = self.scroll_location_for(scroll); ++ let root_pipeline_id = self.root_pipeline_id?; ++ let root_pipeline = self.pipelines.get_mut(&root_pipeline_id)?; ++ let (external_scroll_id, offset) = root_pipeline ++ .scroll_tree ++ .try_scroll_any_node(scroll_location, ScrollType::InputEvents)?; ++ self.send_scroll_positions_to_layout_for_pipeline(root_pipeline_id, external_scroll_id); ++ Some(ScrollResult { ++ external_scroll_id, ++ 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 +- // its ancestor pipelines. +- let mut previous_pipeline_id = None; +- for hit_test_result in hit_test_results { - let pipeline_details = self.pipelines.get_mut(&hit_test_result.pipeline_id)?; -+ let Some(pipeline_details) = self.pipelines.get_mut(&hit_test_result.pipeline_id) -+ else { -+ continue; -+ }; - if previous_pipeline_id.replace(hit_test_result.pipeline_id) != - Some(hit_test_result.pipeline_id) - { -@@ -907,7 +1050,11 @@ - } - } - } +- if previous_pipeline_id.replace(hit_test_result.pipeline_id) != +- Some(hit_test_result.pipeline_id) +- { +- let scroll_result = pipeline_details.scroll_tree.scroll_node_or_ancestor( +- hit_test_result.external_scroll_id, +- scroll_location, +- ScrollType::InputEvents, +- ); +- if let Some((external_scroll_id, offset)) = scroll_result { +- // We would like to cache the hit test for the node that that actually scrolls +- // while panning, which we don't know until right now (as some nodes +- // might be at the end of their scroll area). In particular, directionality of +- // scroll matters. That's why this is done here and not as soon as the touch +- // starts. +- self.touch_handler.set_hit_test_result_cache_value( +- hit_test_result.clone(), +- self.device_pixels_per_page_pixel(), +- ); +- return Some(ScrollResult { +- hit_test_result, +- external_scroll_id, +- offset, +- }); +- } +- } +- } - None -+ -+ // If hit test returned no matching pipelines (e.g., for embedded webviews where -+ // coordinates are in embedded space but hit test uses parent's WebRender document), -+ // fall back to scrolling the root scroll node in our root pipeline. -+ self.try_scroll_root_pipeline(scroll_location) ++ /// 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(); } /// Scroll the viewport (root pipeline, root scroll node) of this WebView, but first -@@ -1046,13 +1193,24 @@ +@@ -949,14 +1007,6 @@ + return (pinch_zoom_result, vec![]); + }; + +- let hit_test_result = PaintHitTestResult { +- pipeline_id: root_pipeline_id, +- // It's difficult to get a good value for this as it needs to be piped +- // all the way through script and back here. +- point_in_viewport: Default::default(), +- external_scroll_id, +- }; +- + self.send_scroll_positions_to_layout_for_pipeline(root_pipeline_id, external_scroll_id); + + if pinch_zoom_result == PinchZoomResult::DidPinchZoom { +@@ -964,7 +1014,6 @@ + } + + let scroll_result = ScrollResult { +- hit_test_result, + external_scroll_id, + offset, + }; +@@ -1046,13 +1095,24 @@ } fn send_window_size_message(&self) { @@ -335,7 +408,7 @@ let _ = self.embedder_to_constellation_sender.send( EmbedderToConstellationMessage::ChangeViewportDetails( self.id, -@@ -1060,6 +1218,7 @@ +@@ -1060,6 +1120,7 @@ hidpi_scale_factor: device_pixel_ratio, size: layout_viewport, device_size: self.screen_size, @@ -343,7 +416,7 @@ }, WindowSizeType::Resize, ), -@@ -1143,8 +1302,21 @@ +@@ -1143,8 +1204,21 @@ !result.contains(InputEventResult::DefaultPrevented) { // A scroll delta for a wheel event is the inverse of the wheel delta. diff --git a/patches/components/shared/embedder/input_events.rs.patch b/patches/components/shared/embedder/input_events.rs.patch index 3b0d1f9..b216ee4 100644 --- a/patches/components/shared/embedder/input_events.rs.patch +++ b/patches/components/shared/embedder/input_events.rs.patch @@ -50,7 +50,43 @@ } #[derive(Clone, Debug, Default, Deserialize, Serialize)] -@@ -240,7 +276,7 @@ +@@ -149,6 +185,11 @@ + pub action: MouseButtonAction, + pub button: MouseButton, + pub point: WebViewPoint, ++ #[doc(hidden)] ++ // Set when this event was synthesized from a touch tap (see the touch->mouse ++ // compatibility path). Such events must not be re-routed independently of the touch ++ // they mirror; the embedded-webview redirect skips them. ++ pub is_compatibility_event_for_touch: bool, + } + + impl MouseButtonEvent { +@@ -157,8 +198,23 @@ + action, + button, + point, ++ is_compatibility_event_for_touch: false, + } + } ++ ++ #[doc(hidden)] ++ pub fn new_compatibility_for_touch( ++ action: MouseButtonAction, ++ button: MouseButton, ++ point: WebViewPoint, ++ ) -> Self { ++ Self { ++ action, ++ button, ++ point, ++ is_compatibility_event_for_touch: true, ++ } ++ } + } + + /// The types of mouse buttons. +@@ -240,7 +296,7 @@ } /// The type of input represented by a multi-touch event. diff --git a/patches/components/shared/paint/display_list.rs.patch b/patches/components/shared/paint/display_list.rs.patch index 29ea9e9..222794c 100644 --- a/patches/components/shared/paint/display_list.rs.patch +++ b/patches/components/shared/paint/display_list.rs.patch @@ -4,18 +4,18 @@ result } -+ /// Try to scroll any scroll node in the tree that can accept the scroll. -+ /// This is used for bubbling scroll events from embedded iframes where we -+ /// don't have a specific target scroll node from hit-testing. ++ /// Try to scroll any scroll node in the tree that can accept the scroll (depth-first ++ /// from the root). Used to chain an embedded webview's unconsumed scroll to its parent, ++ /// whose scrollable container is not necessarily the root scroll node and whose ++ /// on-screen position is in layout, not visual, coordinates (so it cannot be hit-tested ++ /// reliably). + pub fn try_scroll_any_node( + &mut self, + scroll_location: ScrollLocation, + context: ScrollType, + ) -> Option<(ExternalScrollId, LayoutVector2D)> { -+ // Try each scroll node in order (depth-first from root) + for i in 0..self.nodes.len() { + let node_id = ScrollTreeNodeId { index: i }; -+ + if let Some(result) = + self.scroll_node_or_ancestor_inner(node_id, scroll_location, context) + { diff --git a/ui/system/desktop/popover.js b/ui/system/desktop/popover.js index 38711ad..ac4154b 100644 --- a/ui/system/desktop/popover.js +++ b/ui/system/desktop/popover.js @@ -47,6 +47,7 @@ export class Popover { this.onClose = onClose; this._outsideHandler = null; this._anchor = null; + this._backdrop = null; } /** @@ -74,24 +75,26 @@ export class Popover { } this.el.hidden = false; document.body.dataset[this.dataAttr] = "true"; - // Neutralize webviews (CSS: body.popover-open web-view) so an outside-click - // over a desk view falls through to the document handler below rather than - // being swallowed by the webview iframe. document.body.classList.add("popover-open"); this._anchor = anchor; - // Defer the outside-click hookup by one frame so the click that - // opened the popover doesn't immediately close it via bubbling. - requestAnimationFrame(() => { - this._outsideHandler = (e) => { - if ( - !this.el.contains(e.target) && - !(this._anchor && this._anchor.contains(e.target)) - ) { - this.close(); - } - }; - document.addEventListener("mousedown", this._outsideHandler, true); - }); + // Dismiss via a full-viewport backdrop instead of a document-level + // outside-click listener. With embedded webviews composited in-tree, + // compositor hit-testing routes a click over a desk view straight to the + // embedded pipeline, so a document listener never sees it. The backdrop is + // inserted just before the popover with the popover's own (modal) z-index: + // the popover still paints above it (equal z, later in DOM), while it stacks + // above the embedded content (which sits below chrome-layer modal content, so + // an `auto` z-index is not enough). A `click` handler (not `mousedown`) closes + // on any outside-click without the closing click falling through to the anchor + // and immediately reopening. + const zIndex = getComputedStyle(this.el).zIndex; + this._backdrop = document.createElement("div"); + this._backdrop.className = "popover-backdrop"; + this._backdrop.style.cssText = `position:fixed;inset:0;background:transparent;z-index:${ + zIndex === "auto" ? "var(--z-modal)" : zIndex + };`; + this._backdrop.addEventListener("click", () => this.close()); + this.el.parentNode.insertBefore(this._backdrop, this.el); } close() { @@ -99,6 +102,10 @@ export class Popover { this.el.hidden = true; delete document.body.dataset[this.dataAttr]; document.body.classList.remove("popover-open"); + if (this._backdrop) { + this._backdrop.remove(); + this._backdrop = null; + } if (this._outsideHandler) { document.removeEventListener("mousedown", this._outsideHandler, true); this._outsideHandler = null;