From 3f89e0109cbd39b4379fefbd1e91c0905a92b241 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 18 Aug 2026 17:31:38 +0800 Subject: [PATCH] Simplify iframe input and scroll routing --- .../src/native_bridge/context_host/layout.rs | 43 +-- .../context_host/layout_state.rs | 19 +- .../native_bridge/element/geometry/metrics.rs | 251 +++++++++--------- .../element/geometry/provider.rs | 229 ++++++++-------- .../src/native_bridge/element/styles.rs | 2 +- moli-renderer-v8/src/script_vm.rs | 11 +- .../src/script_vm/input_dispatch.rs | 22 +- .../src/script_vm/input_helpers.rs | 2 +- 8 files changed, 272 insertions(+), 307 deletions(-) diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout.rs b/moli-renderer-v8/src/native_bridge/context_host/layout.rs index 07f9978dab..599a17409a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout.rs @@ -98,14 +98,11 @@ impl JsContextHost { /// resolution can run while the document layout state is already lent to /// a recursive paint pass, in which case the caller uses its ordinary /// authored-style fallback. - pub(crate) fn retained_iframe_layout_viewport( - &self, - frame: DomHandle, - ) -> Option { + pub(crate) fn frame_viewport(&self, frame: DomHandle) -> Option { self.document_layout_state .try_borrow() .ok()? - .embedded_frame_viewport(frame) + .frame_viewport(frame) } pub(crate) fn with_fresh_layout_pass_for_document( @@ -129,9 +126,7 @@ impl JsContextHost { self.child_browsing_context_host_for_document_handle(candidate) .is_some() }); - state.retain_live_embedded_frame_viewports(|frame| { - self.child_browsing_context_is_live(frame) - }); + state.retain_live_frame_viewports(|frame| self.child_browsing_context_is_live(frame)); state.with_services_for_document( document, self.document_handle(), @@ -156,7 +151,7 @@ impl JsContextHost { pass.validate_retention_budget()?; let consumed = consume(&mut pass)?; let metrics = pass.metrics; - let embedded_frame_viewports = self + let frame_viewports = self .child_browsing_context_handles_in_document_order() .into_iter() .filter(|frame| self.dom_host().owner_document_handle(*frame) == Some(document)) @@ -178,7 +173,7 @@ impl JsContextHost { { let mut state = self.document_layout_state.borrow_mut(); state.publish_latest_layout(document, tree); - state.publish_embedded_frame_viewports(embedded_frame_viewports); + state.update_frame_viewports(frame_viewports); } self.last_layout_pass_metrics.set(Some(metrics)); self.layout_snapshot_cache_publishes @@ -193,7 +188,7 @@ impl JsContextHost { queries: &LayoutQueryBatch, ) -> Result, LayoutError> { let viewport = self.layout_viewport_for_document(document); - self.answer_layout_for_document_with_viewport(document, reason, viewport, queries) + self.answer_layout(document, reason, viewport, queries, false) } pub(crate) fn can_answer_layout_from_snapshot(&self, document: DomHandle) -> bool { @@ -222,37 +217,23 @@ impl JsContextHost { state.latest_layout(document).map(inspect) } - fn answer_layout_for_document_with_viewport( + pub(crate) fn answer_layout_at_viewport( &self, document: DomHandle, reason: LayoutFlushReason, viewport: LayoutViewport, queries: &LayoutQueryBatch, ) -> Result, LayoutError> { - self.answer_layout_for_document_with_viewport_policy( - document, reason, viewport, queries, false, - ) + self.answer_layout(document, reason, viewport, queries, true) } - pub(crate) fn answer_layout_for_document_with_exact_viewport( + fn answer_layout( &self, document: DomHandle, reason: LayoutFlushReason, viewport: LayoutViewport, queries: &LayoutQueryBatch, - ) -> Result, LayoutError> { - self.answer_layout_for_document_with_viewport_policy( - document, reason, viewport, queries, true, - ) - } - - fn answer_layout_for_document_with_viewport_policy( - &self, - document: DomHandle, - reason: LayoutFlushReason, - viewport: LayoutViewport, - queries: &LayoutQueryBatch, - require_exact_viewport: bool, + exact_viewport: bool, ) -> Result, LayoutError> { #[cfg(test)] let reuse_latest = !self.force_fresh_layout_reads_for_test; @@ -262,7 +243,7 @@ impl JsContextHost { let state = self.document_layout_state.borrow(); state .latest_layout(document) - .filter(|tree| !require_exact_viewport || tree.viewport == viewport) + .filter(|tree| !exact_viewport || tree.viewport == viewport) .and_then(|tree| { self.last_layout_pass_metrics .get() @@ -453,7 +434,7 @@ impl GeometryProvider for JsContextHost { queries: &LayoutQueryBatch, ) -> Result, LayoutError> { let document = self.document_handle(); - self.answer_layout_for_document_with_viewport(document, reason, viewport, queries) + self.answer_layout(document, reason, viewport, queries, false) } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs index 76a1b40ad1..f7fb38487c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs @@ -29,7 +29,7 @@ pub(super) struct DocumentLayoutState { /// parent layout. Blink keeps the equivalent size on LocalFrameView; it is /// separate from the single latest-tree slot because parent and child /// layout queries replace that slot while the frame view remains live. - embedded_frame_viewports: HashMap, + frame_viewports: HashMap, } impl Default for DocumentLayoutState { @@ -40,7 +40,7 @@ impl Default for DocumentLayoutState { web_fonts: DocumentWebFontState::default(), web_font_sources_dirty: true, latest_layout: LatestLayoutTreeCache::default(), - embedded_frame_viewports: HashMap::new(), + frame_viewports: HashMap::new(), } } } @@ -106,32 +106,31 @@ impl DocumentLayoutState { self.latest_layout.clear(); } - pub(super) fn embedded_frame_viewport(&self, frame: DomHandle) -> Option { - self.embedded_frame_viewports.get(&frame).copied() + pub(super) fn frame_viewport(&self, frame: DomHandle) -> Option { + self.frame_viewports.get(&frame).copied() } - pub(super) fn publish_embedded_frame_viewports( + pub(super) fn update_frame_viewports( &mut self, updates: impl IntoIterator)>, ) { for (frame, viewport) in updates { match viewport { Some(viewport) => { - self.embedded_frame_viewports.insert(frame, viewport); + self.frame_viewports.insert(frame, viewport); } None => { - self.embedded_frame_viewports.remove(&frame); + self.frame_viewports.remove(&frame); } } } } - pub(super) fn retain_live_embedded_frame_viewports( + pub(super) fn retain_live_frame_viewports( &mut self, mut is_live: impl FnMut(DomHandle) -> bool, ) { - self.embedded_frame_viewports - .retain(|frame, _| is_live(*frame)); + self.frame_viewports.retain(|frame, _| is_live(*frame)); } #[cfg(test)] diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs b/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs index 6a39466694..dfbfec8d37 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs @@ -300,6 +300,23 @@ enum ScrollIntoViewAlignment { Nearest, } +#[derive(Clone, Copy)] +struct ScrollIntoViewParams { + horizontal: ScrollIntoViewAlignment, + vertical: ScrollIntoViewAlignment, + center_if_fully_hidden: bool, +} + +const SCROLL_INTO_VIEW_FRAME_DEPTH_LIMIT: usize = 16; + +fn resolve_scroll_target(runtime: &JsContextHost, handle: DomHandle) -> Option { + let node = runtime.dom_host().node(handle)?; + if node.is_document() { + return runtime.dom_host().document_element_handle(); + } + runtime.dom_host().is_connected(handle).then_some(handle) +} + fn aligned_scroll_position( target_start: f64, target_end: f64, @@ -381,18 +398,7 @@ pub(crate) fn perform_wheel_scroll_default_action( } let runtime = unsafe { &*runtime_ptr }; - let target = if runtime - .dom_host() - .node(handle) - .is_some_and(Node::is_document) - { - let Some(root) = runtime.dom_host().document_element_handle() else { - return Ok(false); - }; - root - } else if runtime.dom_host().is_connected(handle) { - handle - } else { + let Some(target) = resolve_scroll_target(runtime, handle) else { return Ok(false); }; let Some(mut geometry) = observable_scroll_into_view_geometry( @@ -494,80 +500,28 @@ pub(crate) fn scroll_node_into_view_if_needed( handle: DomHandle, relative_rect: Option, ) -> Result, moli_layout::LayoutError> { - scroll_node_into_view_with_geometry( + scroll_node_into_view( scope, runtime_ptr, handle, relative_rect, - None, - ScrollIntoViewAlignment::Nearest, - ScrollIntoViewAlignment::Nearest, - true, - 0, + ScrollIntoViewParams { + horizontal: ScrollIntoViewAlignment::Nearest, + vertical: ScrollIntoViewAlignment::Nearest, + center_if_fully_hidden: true, + }, ) } fn scroll_node_into_view( - scope: &mut v8::PinScope<'_, '_>, - runtime_ptr: *mut JsContextHost, - handle: DomHandle, - horizontal: ScrollIntoViewAlignment, - vertical: ScrollIntoViewAlignment, -) -> Result, moli_layout::LayoutError> { - scroll_node_into_view_with_geometry( - scope, - runtime_ptr, - handle, - None, - None, - horizontal, - vertical, - false, - 0, - ) -} - -pub(crate) fn scroll_node_into_view_at_start( - scope: &mut v8::PinScope<'_, '_>, - runtime_ptr: *mut JsContextHost, - handle: DomHandle, -) -> Result, moli_layout::LayoutError> { - scroll_node_into_view( - scope, - runtime_ptr, - handle, - ScrollIntoViewAlignment::Nearest, - ScrollIntoViewAlignment::Start, - ) -} - -fn scroll_node_into_view_with_geometry( scope: &mut v8::PinScope<'_, '_>, runtime_ptr: *mut JsContextHost, handle: DomHandle, relative_rect: Option, - target_rects_override: Option>, - horizontal: ScrollIntoViewAlignment, - vertical: ScrollIntoViewAlignment, - center_if_fully_hidden: bool, - frame_depth: usize, + params: ScrollIntoViewParams, ) -> Result, moli_layout::LayoutError> { let runtime = unsafe { &*runtime_ptr }; - let target = if runtime - .dom_host() - .node(handle) - .is_some_and(Node::is_document) - { - let Some(root) = runtime.dom_host().document_element_handle() else { - return Ok(None); - }; - root - } else if runtime.dom_host().is_connected(handle) && runtime.dom_host().node(handle).is_some() { - // CDP also accepts connected rendered Text nodes. Whether a concrete - // node owns fragments is a layout-output decision; this DOM boundary - // only rejects detached or stale handles. - handle - } else { + let Some(target) = resolve_scroll_target(runtime, handle) else { return Ok(None); }; let Some(mut geometry) = observable_scroll_into_view_geometry( @@ -592,17 +546,44 @@ fn scroll_node_into_view_with_geometry( )), ]; } - if let Some(target_rects) = target_rects_override { - if target_rects.is_empty() { - return Ok(None); - } - geometry.target_rects = target_rects; - } if geometry.target_rects.is_empty() { return Ok(None); } + perform_bubbling_scroll_into_view(scope, runtime_ptr, target, geometry, params, 0).map(Some) +} - let target_document = runtime.dom_host().owner_document_handle(target); +pub(crate) fn scroll_node_into_view_at_start( + scope: &mut v8::PinScope<'_, '_>, + runtime_ptr: *mut JsContextHost, + handle: DomHandle, +) -> Result, moli_layout::LayoutError> { + scroll_node_into_view( + scope, + runtime_ptr, + handle, + None, + ScrollIntoViewParams { + horizontal: ScrollIntoViewAlignment::Nearest, + vertical: ScrollIntoViewAlignment::Start, + center_if_fully_hidden: false, + }, + ) +} + +/// Blink's `PerformBubblingScrollIntoView` follows the same shape: scroll the +/// local containers, convert the resulting target geometry into the parent +/// frame, then continue with the frame owner. +fn perform_bubbling_scroll_into_view( + scope: &mut v8::PinScope<'_, '_>, + runtime_ptr: *mut JsContextHost, + target: DomHandle, + mut geometry: moli_layout::LayoutScrollIntoViewGeometry, + params: ScrollIntoViewParams, + frame_depth: usize, +) -> Result { + let target_document = unsafe { &*runtime_ptr } + .dom_host() + .owner_document_handle(target); let mut changed = false; for container in geometry.scroll_containers { let metrics = &container.metrics; @@ -610,7 +591,7 @@ fn scroll_node_into_view_with_geometry( else { continue; }; - let desired_x = if center_if_fully_hidden { + let desired_x = if params.center_if_fully_hidden { scroll_axis_to_expose( f64::from(target_bounds.x), f64::from(target_bounds.right()), @@ -623,10 +604,10 @@ fn scroll_node_into_view_with_geometry( f64::from(target_bounds.right()), f64::from(metrics.scroll_offset.x), f64::from(metrics.client_size.width), - horizontal, + params.horizontal, ) }; - let desired_y = if center_if_fully_hidden { + let desired_y = if params.center_if_fully_hidden { scroll_axis_to_expose( f64::from(target_bounds.y), f64::from(target_bounds.bottom()), @@ -639,7 +620,7 @@ fn scroll_node_into_view_with_geometry( f64::from(target_bounds.bottom()), f64::from(metrics.scroll_offset.y), f64::from(metrics.client_size.height), - vertical, + params.vertical, ) }; let target_x = desired_x.clamp( @@ -694,47 +675,64 @@ fn scroll_node_into_view_with_geometry( delta_y, ); } - if frame_depth < 16 - && let Some((document, frame)) = target_document.and_then(|document| { - let runtime = unsafe { &*runtime_ptr }; - (document != runtime.document_handle()) - .then(|| runtime.child_browsing_context_host_for_document_handle(document)) - .flatten() - .map(|frame| (document, frame)) - }) - && let Some(frame_content) = observable_box_model( - unsafe { &*runtime_ptr }, - frame, - moli_layout::LayoutFlushReason::SynchronousGeometry, - )? - .map(|model| model.content) - && let Some(parent_target_rects) = map_child_viewport_quads_to_parent( - &geometry.target_rects, - unsafe { &*runtime_ptr }.layout_viewport_for_document(document), - frame_content, - ) - && let Some(parent_changed) = scroll_node_into_view_with_geometry( - scope, - runtime_ptr, - frame, - None, - Some(parent_target_rects), - ScrollIntoViewAlignment::Nearest, - ScrollIntoViewAlignment::Nearest, - center_if_fully_hidden, - frame_depth + 1, - )? - { - changed |= parent_changed; + if frame_depth >= SCROLL_INTO_VIEW_FRAME_DEPTH_LIMIT { + return Ok(changed); } - Ok(Some(changed)) + let Some(document) = target_document else { + return Ok(changed); + }; + let runtime = unsafe { &*runtime_ptr }; + if document == runtime.document_handle() { + return Ok(changed); + } + let Some(frame) = runtime.child_browsing_context_host_for_document_handle(document) else { + return Ok(changed); + }; + let Some(frame_content) = observable_box_model( + runtime, + frame, + moli_layout::LayoutFlushReason::SynchronousGeometry, + )? + .map(|model| model.content) else { + return Ok(changed); + }; + let Some(parent_target_rects) = convert_quads_to_parent_frame( + &geometry.target_rects, + runtime.layout_viewport_for_document(document), + frame_content, + ) else { + return Ok(changed); + }; + let Some(mut parent_geometry) = observable_scroll_into_view_geometry( + runtime, + frame, + moli_layout::LayoutFlushReason::SynchronousGeometry, + )? + else { + return Ok(changed); + }; + parent_geometry.target_rects = parent_target_rects; + let parent_params = ScrollIntoViewParams { + horizontal: ScrollIntoViewAlignment::Nearest, + vertical: ScrollIntoViewAlignment::Nearest, + ..params + }; + changed |= perform_bubbling_scroll_into_view( + scope, + runtime_ptr, + frame, + parent_geometry, + parent_params, + frame_depth + 1, + )?; + Ok(changed) } /// Convert a rectangle carried by a child frame into the parent document's /// viewport coordinates. This mirrors Blink's cross-frame scroll bubbling: /// the target rectangle is converted through the frame owner's content box, /// rather than replacing it with the bounds of the whole frame. -fn map_child_viewport_quads_to_parent( +fn convert_quads_to_parent_frame( child_quads: &[moli_layout::LayoutQuad], child_viewport: moli_layout::LayoutViewport, parent_content: moli_layout::LayoutQuad, @@ -1329,8 +1327,17 @@ pub(in crate::native_bridge) fn node_scroll_into_view_callback( ) { if let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object(scope, args.this()) { let (horizontal, vertical) = element_scroll_into_view_alignments(scope, &args); - if let Err(error) = scroll_node_into_view(scope, runtime_ptr, handle, horizontal, vertical) - { + if let Err(error) = scroll_node_into_view( + scope, + runtime_ptr, + handle, + None, + ScrollIntoViewParams { + horizontal, + vertical, + center_if_fully_hidden: false, + }, + ) { throw_scroll_layout_error(scope, "scrollIntoView", error); } } @@ -1368,7 +1375,7 @@ fn reveal_lazy_images_for_scroll( #[cfg(test)] mod scroll_alignment_tests { - use super::{map_child_viewport_quads_to_parent, scroll_axis_to_expose}; + use super::{convert_quads_to_parent_frame, scroll_axis_to_expose}; #[test] fn center_if_needed_returns_the_unclamped_chromium_alignment_position() { @@ -1398,7 +1405,7 @@ mod scroll_alignment_tests { ], }; - let mapped = map_child_viewport_quads_to_parent( + let mapped = convert_quads_to_parent_frame( &[child_quad], moli_layout::LayoutViewport::new(200, 100, 1.0), parent_content, @@ -1424,7 +1431,7 @@ mod scroll_alignment_tests { .map_rect(moli_layout::LayoutRect::new(0.0, 0.0, 10.0, 10.0)); assert!( - map_child_viewport_quads_to_parent( + convert_quads_to_parent_frame( &[child_quad], moli_layout::LayoutViewport::new(0, 100, 1.0), parent_content, diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs b/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs index ddb2380c68..7441ca7f7b 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/provider.rs @@ -18,13 +18,58 @@ use crate::{document_runtime::DomHandle, dom::native::Node, native_bridge::JsCon const HIT_TEST_CHILD_FRAME_DEPTH_LIMIT: usize = 16; #[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) struct ObservableInputHit { +pub(crate) struct InputHit { pub(crate) handle: DomHandle, - /// Converts a root-frame input position to viewport-relative CSS - /// coordinates in `handle`'s owner Document. Keeping the affine map lets - /// boundary and capture events convert a new root position into a - /// previously targeted child Document. - pub(crate) root_to_client: LayoutTransform2D, + /// Converts a root-frame input position into the viewport of `handle`'s + /// owner frame. Keeping the affine map lets boundary and capture events + /// convert a new root position into a previously targeted child frame. + pub(crate) root_to_frame: LayoutTransform2D, +} + +#[derive(Clone, Copy)] +struct FrameHitTest { + document: DomHandle, + viewport: LayoutViewport, + point: LayoutPoint, + root_to_frame: LayoutTransform2D, +} + +impl FrameHitTest { + fn root(runtime: &JsContextHost, document: DomHandle, point: LayoutPoint) -> Self { + Self { + document, + viewport: runtime.layout_viewport_for_document(document), + point, + root_to_frame: LayoutTransform2D::IDENTITY, + } + } + + /// Blink performs the equivalent conversion through LocalFrameView when + /// an input hit crosses an embedded-content boundary. + fn child( + self, + runtime: &JsContextHost, + frame: DomHandle, + hit: LayoutHit, + ) -> Option { + let document = runtime.child_browsing_context_document_handle(frame)?; + let content_box = hit.local_content_box?; + if !content_box.contains(hit.local_point) { + return None; + } + let frame_to_child = LayoutTransform2D::translation(-content_box.x, -content_box.y) + .concatenate(hit.viewport_to_local); + Some(Self { + document, + viewport: LayoutViewport::new( + css_viewport_dimension(content_box.width), + css_viewport_dimension(content_box.height), + self.viewport.device_pixel_ratio, + ), + point: frame_to_child.map_point(self.point), + root_to_frame: frame_to_child.concatenate(self.root_to_frame), + }) + } } pub(crate) fn observable_geometry_batch( @@ -294,20 +339,25 @@ fn observable_hit_test( } } -fn observable_hit_test_with_viewport( +fn observable_hit_test_in_viewport( runtime: &JsContextHost, document: DomHandle, viewport: LayoutViewport, point: LayoutPoint, ignore_pointer_events_none: bool, - reason: LayoutFlushReason, ) -> Result>, LayoutError> { if !runtime.layout_policy().uses_real_layout() { - return observable_hit_test(runtime, document, point, ignore_pointer_events_none, reason); + return observable_hit_test( + runtime, + document, + point, + ignore_pointer_events_none, + LayoutFlushReason::HitTest, + ); } - let answers = runtime.answer_layout_for_document_with_exact_viewport( + let answers = runtime.answer_layout_at_viewport( document, - reason, + LayoutFlushReason::HitTest, viewport, &LayoutQueryBatch::new(vec![LayoutQuery::HitTest { point, @@ -355,8 +405,8 @@ pub(crate) fn observable_input_hit_test( runtime: &JsContextHost, document: DomHandle, point: LayoutPoint, -) -> Result, LayoutError> { - observable_deep_input_hit_test(runtime, document, point, false) +) -> Result, LayoutError> { + input_hit_test(runtime, document, point, false) } pub(crate) fn observable_scrollbar_hit_test( @@ -367,14 +417,7 @@ pub(crate) fn observable_scrollbar_hit_test( if !runtime.layout_policy().uses_real_layout() { return Ok(None); } - observable_deep_scrollbar_hit_test_inner( - runtime, - document, - runtime.layout_viewport_for_document(document), - point, - LayoutTransform2D::IDENTITY, - 0, - ) + scrollbar_hit_test_in_frame(runtime, FrameHitTest::root(runtime, document, point), 0) } pub(crate) fn observable_deep_hit_test( @@ -383,153 +426,91 @@ pub(crate) fn observable_deep_hit_test( point: LayoutPoint, ignore_pointer_events_none: bool, ) -> Result, LayoutError> { - Ok( - observable_deep_input_hit_test(runtime, document, point, ignore_pointer_events_none)? - .map(|hit| hit.handle), - ) + Ok(input_hit_test(runtime, document, point, ignore_pointer_events_none)?.map(|hit| hit.handle)) } -fn observable_deep_input_hit_test( +fn input_hit_test( runtime: &JsContextHost, document: DomHandle, point: LayoutPoint, ignore_pointer_events_none: bool, -) -> Result, LayoutError> { - observable_deep_input_hit_test_inner( +) -> Result, LayoutError> { + input_hit_test_in_frame( runtime, - document, - runtime.layout_viewport_for_document(document), - point, - LayoutTransform2D::IDENTITY, + FrameHitTest::root(runtime, document, point), ignore_pointer_events_none, 0, ) } -fn observable_deep_input_hit_test_inner( +fn input_hit_test_in_frame( runtime: &JsContextHost, - document: DomHandle, - viewport: LayoutViewport, - point: LayoutPoint, - root_to_viewport: LayoutTransform2D, + frame: FrameHitTest, ignore_pointer_events_none: bool, depth: usize, -) -> Result, LayoutError> { - let Some((hit, target)) = observable_live_hit_with_viewport( - runtime, - document, - viewport, - point, - ignore_pointer_events_none, - )? +) -> Result, LayoutError> { + let Some((layout_hit, target)) = live_hit_in_frame(runtime, frame, ignore_pointer_events_none)? else { return Ok(None); }; - let target_hit = ObservableInputHit { + let target_hit = InputHit { handle: target, - root_to_client: root_to_viewport, + root_to_frame: frame.root_to_frame, }; if depth >= HIT_TEST_CHILD_FRAME_DEPTH_LIMIT { return Ok(Some(target_hit)); } - let Some(child_document) = runtime.child_browsing_context_document_handle(target) else { + let Some(child) = frame.child(runtime, target, layout_hit) else { return Ok(Some(target_hit)); }; - let Some(content_box) = hit.local_content_box else { - return Ok(Some(target_hit)); - }; - if !content_box.contains(hit.local_point) { - return Ok(Some(target_hit)); - } - let viewport_to_child = LayoutTransform2D::translation(-content_box.x, -content_box.y) - .concatenate(hit.viewport_to_local); - let child_point = viewport_to_child.map_point(point); - let root_to_child = viewport_to_child.concatenate(root_to_viewport); - let child_viewport = LayoutViewport::new( - css_viewport_dimension(content_box.width), - css_viewport_dimension(content_box.height), - viewport.device_pixel_ratio, - ); - Ok(observable_deep_input_hit_test_inner( - runtime, - child_document, - child_viewport, - child_point, - root_to_child, - ignore_pointer_events_none, - depth + 1, - )? - .or(Some(target_hit))) + Ok( + input_hit_test_in_frame(runtime, child, ignore_pointer_events_none, depth + 1)? + .or(Some(target_hit)), + ) } -fn observable_deep_scrollbar_hit_test_inner( +fn scrollbar_hit_test_in_frame( runtime: &JsContextHost, - document: DomHandle, - viewport: LayoutViewport, - point: LayoutPoint, - root_to_viewport: LayoutTransform2D, + frame: FrameHitTest, depth: usize, ) -> Result>, LayoutError> { // Publish a current tree through the same exact-viewport hit-test // boundary used for DOM input. Scrollbar geometry intentionally remains a // frozen-tree control query because it is user-agent chrome, not a DOM // target. - let live_hit = observable_live_hit_with_viewport(runtime, document, viewport, point, false)?; + let live_hit = live_hit_in_frame(runtime, frame, false)?; if let Some(mut hit) = runtime - .with_latest_layout_tree_for_document(document, |tree| tree.scrollbar_hit_test(point)) + .with_latest_layout_tree_for_document(frame.document, |tree| { + tree.scrollbar_hit_test(frame.point) + }) .flatten() { - hit.viewport_to_local = hit.viewport_to_local.concatenate(root_to_viewport); + hit.viewport_to_local = hit.viewport_to_local.concatenate(frame.root_to_frame); return Ok(Some(hit)); } if depth >= HIT_TEST_CHILD_FRAME_DEPTH_LIMIT { return Ok(None); } - let Some((frame_hit, frame)) = live_hit else { + let Some((layout_hit, target)) = live_hit else { return Ok(None); }; - let Some(child_document) = runtime.child_browsing_context_document_handle(frame) else { + let Some(child) = frame.child(runtime, target, layout_hit) else { return Ok(None); }; - let Some(content_box) = frame_hit.local_content_box else { - return Ok(None); - }; - if !content_box.contains(frame_hit.local_point) { - return Ok(None); - } - let viewport_to_child = LayoutTransform2D::translation(-content_box.x, -content_box.y) - .concatenate(frame_hit.viewport_to_local); - let root_to_child = viewport_to_child.concatenate(root_to_viewport); - let child_point = viewport_to_child.map_point(point); - let child_viewport = LayoutViewport::new( - css_viewport_dimension(content_box.width), - css_viewport_dimension(content_box.height), - viewport.device_pixel_ratio, - ); - observable_deep_scrollbar_hit_test_inner( - runtime, - child_document, - child_viewport, - child_point, - root_to_child, - depth + 1, - ) + scrollbar_hit_test_in_frame(runtime, child, depth + 1) } -fn observable_live_hit_with_viewport( +fn live_hit_in_frame( runtime: &JsContextHost, - document: DomHandle, - viewport: LayoutViewport, - point: LayoutPoint, + frame: FrameHitTest, ignore_pointer_events_none: bool, ) -> Result, DomHandle)>, LayoutError> { - let first_hit = observable_hit_test_with_viewport( + let first_hit = observable_hit_test_in_viewport( runtime, - document, - viewport, - point, + frame.document, + frame.viewport, + frame.point, ignore_pointer_events_none, - LayoutFlushReason::HitTest, )?; let live_first_hit = first_hit.and_then(|hit| { let target = element_for_hit_source(runtime, hit.source)?; @@ -546,13 +527,12 @@ fn observable_live_hit_with_viewport( if live_first_hit.is_some() || first_hit.is_none() { return Ok(live_first_hit); } - let hits = observable_hit_test_all_with_viewport( + let hits = observable_hit_test_all_in_viewport( runtime, - document, - viewport, - point, + frame.document, + frame.viewport, + frame.point, ignore_pointer_events_none, - LayoutFlushReason::HitTest, )?; Ok(hits.into_iter().find_map(|hit| { let target = element_for_hit_source(runtime, hit.source)?; @@ -606,13 +586,12 @@ pub(crate) fn observable_hit_test_all( } } -fn observable_hit_test_all_with_viewport( +fn observable_hit_test_all_in_viewport( runtime: &JsContextHost, document: DomHandle, viewport: LayoutViewport, point: LayoutPoint, ignore_pointer_events_none: bool, - reason: LayoutFlushReason, ) -> Result>, LayoutError> { if !runtime.layout_policy().uses_real_layout() { return observable_hit_test_all( @@ -620,13 +599,13 @@ fn observable_hit_test_all_with_viewport( document, point, ignore_pointer_events_none, - reason, + LayoutFlushReason::HitTest, ) .map(|(_, hits)| hits); } - let answers = runtime.answer_layout_for_document_with_exact_viewport( + let answers = runtime.answer_layout_at_viewport( document, - reason, + LayoutFlushReason::HitTest, viewport, &LayoutQueryBatch::new(vec![LayoutQuery::HitTestAll { point, diff --git a/moli-renderer-v8/src/native_bridge/element/styles.rs b/moli-renderer-v8/src/native_bridge/element/styles.rs index 654c66c035..876a431fba 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles.rs @@ -692,7 +692,7 @@ fn iframe_handle_viewport_with_depth( .filter(|parent_frame| *parent_frame != frame_handle) .and_then(|parent_frame| iframe_handle_viewport_with_depth(host, parent_frame, depth + 1)); let parent_viewport = parent_viewport.unwrap_or_else(|| host.style_viewport()); - if let Some(viewport) = host.retained_iframe_layout_viewport(frame_handle) { + if let Some(viewport) = host.frame_viewport(frame_handle) { return Some( StyleViewport::new( Some(f64::from(viewport.css_width)), diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 64a9e14e39..b22b5a9a39 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -924,11 +924,10 @@ pub(super) struct ScriptVm { pressed_mouse_buttons: i32, pending_mouse_press: Option, hovered_mouse_handle: Option, - /// Root-frame to client-coordinate transform for the Document that owns - /// `hovered_mouse_handle`. Blink keeps this conversion on LocalFrameView; - /// retaining the last affine map lets exit events use the old frame even - /// after the new hit has entered another frame. - hovered_mouse_root_to_client: moli_layout::LayoutTransform2D, + /// Root-frame to local-frame transform for `hovered_mouse_handle`. Blink + /// keeps this conversion on LocalFrameView; retaining the last affine map + /// lets exit events use the old frame after a new hit enters another one. + hovered_mouse_root_to_frame: moli_layout::LayoutTransform2D, active_touch_pointer_handle: Option, active_touch_pointer_handles: BTreeMap, active_touch_event_handle: Option, @@ -2314,7 +2313,7 @@ impl ScriptVmDefaultWorldBootstrap { pressed_mouse_buttons: 0, pending_mouse_press: None, hovered_mouse_handle: None, - hovered_mouse_root_to_client: moli_layout::LayoutTransform2D::IDENTITY, + hovered_mouse_root_to_frame: moli_layout::LayoutTransform2D::IDENTITY, active_touch_pointer_handle: None, active_touch_pointer_handles: BTreeMap::new(), active_touch_event_handle: None, diff --git a/moli-renderer-v8/src/script_vm/input_dispatch.rs b/moli-renderer-v8/src/script_vm/input_dispatch.rs index ceb41c18f6..993647df62 100644 --- a/moli-renderer-v8/src/script_vm/input_dispatch.rs +++ b/moli-renderer-v8/src/script_vm/input_dispatch.rs @@ -541,22 +541,22 @@ impl ScriptVm { let Some(handle) = capture_handle.or(hit_handle).or(wheel_fallback_handle) else { return Ok(input_dispatch_outcome(false)); }; - let root_to_client = hit + let root_to_frame = hit .filter(|hit| hit.handle == handle) - .map(|hit| hit.root_to_client) + .map(|hit| hit.root_to_frame) .unwrap_or(moli_layout::LayoutTransform2D::IDENTITY); - let client_point = root_to_client.map_point(root_point); + let client_point = root_to_frame.map_point(root_point); let client_x = f64::from(client_point.x); let client_y = f64::from(client_point.y); let hover_transition = if tracks_mouse_hover_for_event(event_name) { let previous = self.hovered_mouse_handle; if previous != Some(handle) { - let previous_root_to_client = self.hovered_mouse_root_to_client; + let previous_root_to_frame = self.hovered_mouse_root_to_frame; self.hovered_mouse_handle = Some(handle); - self.hovered_mouse_root_to_client = root_to_client; - Some((previous, previous_root_to_client)) + self.hovered_mouse_root_to_frame = root_to_frame; + Some((previous, previous_root_to_frame)) } else { - self.hovered_mouse_root_to_client = root_to_client; + self.hovered_mouse_root_to_frame = root_to_frame; None } } else { @@ -600,8 +600,8 @@ impl ScriptVm { let result = self.with_default_context_scope(|scope, runtime_ptr| { let mut pointer_dispatch_handle = handle; - if let Some((Some(previous_handle), previous_root_to_client)) = hover_transition { - let previous_client_point = previous_root_to_client.map_point(root_point); + if let Some((Some(previous_handle), previous_root_to_frame)) = hover_transition { + let previous_client_point = previous_root_to_frame.map_point(root_point); let previous_client_x = f64::from(previous_client_point.x); let previous_client_y = f64::from(previous_client_point.y); let related_target = related_target_value(scope, Some(handle)); @@ -699,8 +699,8 @@ impl ScriptVm { .unwrap_or(pointer_dispatch_handle); } } - if let Some((Some(previous_handle), previous_root_to_client)) = hover_transition { - let previous_client_point = previous_root_to_client.map_point(root_point); + if let Some((Some(previous_handle), previous_root_to_frame)) = hover_transition { + let previous_client_point = previous_root_to_frame.map_point(root_point); let previous_client_x = f64::from(previous_client_point.x); let previous_client_y = f64::from(previous_client_point.y); let related_target = related_target_value(scope, Some(handle)); diff --git a/moli-renderer-v8/src/script_vm/input_helpers.rs b/moli-renderer-v8/src/script_vm/input_helpers.rs index e8f3c66575..def624b8e2 100644 --- a/moli-renderer-v8/src/script_vm/input_helpers.rs +++ b/moli-renderer-v8/src/script_vm/input_helpers.rs @@ -50,7 +50,7 @@ pub(super) fn clear_input_dispatch_state(vm: &mut ScriptVm) { vm.pressed_mouse_buttons = 0; vm.pending_mouse_press = None; vm.hovered_mouse_handle = None; - vm.hovered_mouse_root_to_client = moli_layout::LayoutTransform2D::IDENTITY; + vm.hovered_mouse_root_to_frame = moli_layout::LayoutTransform2D::IDENTITY; vm._context_host .borrow() .dom_host()