Simplify iframe input and scroll routing

This commit is contained in:
ldm0
2026-08-22 19:44:54 +08:00
committed by Donough Liu
parent 4f39a0f0d9
commit 3f89e0109c
8 changed files with 272 additions and 307 deletions
@@ -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<LayoutViewport> {
pub(crate) fn frame_viewport(&self, frame: DomHandle) -> Option<LayoutViewport> {
self.document_layout_state
.try_borrow()
.ok()?
.embedded_frame_viewport(frame)
.frame_viewport(frame)
}
pub(crate) fn with_fresh_layout_pass_for_document<T>(
@@ -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<DomHandle>,
) -> Result<LayoutAnswers<DomHandle>, 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<DomHandle>,
) -> Result<LayoutAnswers<DomHandle>, 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<DomHandle>,
) -> Result<LayoutAnswers<DomHandle>, 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<DomHandle>,
require_exact_viewport: bool,
exact_viewport: bool,
) -> Result<LayoutAnswers<DomHandle>, 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<Self::NodeId>,
) -> Result<LayoutAnswers<Self::NodeId>, 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)
}
}
@@ -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<DomHandle, LayoutViewport>,
frame_viewports: HashMap<DomHandle, LayoutViewport>,
}
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<LayoutViewport> {
self.embedded_frame_viewports.get(&frame).copied()
pub(super) fn frame_viewport(&self, frame: DomHandle) -> Option<LayoutViewport> {
self.frame_viewports.get(&frame).copied()
}
pub(super) fn publish_embedded_frame_viewports(
pub(super) fn update_frame_viewports(
&mut self,
updates: impl IntoIterator<Item = (DomHandle, Option<LayoutViewport>)>,
) {
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)]
@@ -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<DomHandle> {
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<DomScrollIntoViewRect>,
) -> Result<Option<bool>, 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<Option<bool>, 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<Option<bool>, 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<DomScrollIntoViewRect>,
target_rects_override: Option<Vec<moli_layout::LayoutQuad>>,
horizontal: ScrollIntoViewAlignment,
vertical: ScrollIntoViewAlignment,
center_if_fully_hidden: bool,
frame_depth: usize,
params: ScrollIntoViewParams,
) -> Result<Option<bool>, 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<Option<bool>, 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<DomHandle>,
params: ScrollIntoViewParams,
frame_depth: usize,
) -> Result<bool, moli_layout::LayoutError> {
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,
@@ -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<DomHandle>,
) -> Option<Self> {
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<Option<LayoutHit<DomHandle>>, 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<Option<ObservableInputHit>, LayoutError> {
observable_deep_input_hit_test(runtime, document, point, false)
) -> Result<Option<InputHit>, 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<Option<DomHandle>, 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<Option<ObservableInputHit>, LayoutError> {
observable_deep_input_hit_test_inner(
) -> Result<Option<InputHit>, 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<Option<ObservableInputHit>, LayoutError> {
let Some((hit, target)) = observable_live_hit_with_viewport(
runtime,
document,
viewport,
point,
ignore_pointer_events_none,
)?
) -> Result<Option<InputHit>, 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<Option<LayoutScrollbarHit<DomHandle>>, 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<Option<(LayoutHit<DomHandle>, 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<Vec<LayoutHit<DomHandle>>, 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,
@@ -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)),
+5 -6
View File
@@ -924,11 +924,10 @@ pub(super) struct ScriptVm {
pressed_mouse_buttons: i32,
pending_mouse_press: Option<PendingMousePress>,
hovered_mouse_handle: Option<DomHandle>,
/// 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<DomHandle>,
active_touch_pointer_handles: BTreeMap<i32, DomHandle>,
active_touch_event_handle: Option<DomHandle>,
@@ -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,
@@ -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));
@@ -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()