Fix transformed iframe input routing

This commit is contained in:
ldm0
2026-08-22 19:44:54 +08:00
committed by Donough Liu
parent 3a8e52ba99
commit f481bfa192
11 changed files with 625 additions and 161 deletions
+22
View File
@@ -34,6 +34,18 @@ pub struct LayoutHit<N> {
pub fragment: Option<LayoutFragmentId>,
pub local_point: LayoutPoint,
pub is_text: bool,
/// The exact physical content box in the hit fragment's own coordinate
/// space.
///
/// Unlike `box_model`, this rectangle has not been projected through CSS
/// transforms. Embedded-content consumers use it to enter the child frame
/// without reconstructing the iframe's used size from authored CSS.
pub local_content_box: Option<LayoutRect>,
/// Converts a point from this tree's viewport into the hit fragment's
/// coordinate space. This is retained on the short-lived hit result so
/// frame and native-scrollbar routing can compose the same inverse
/// transforms that produced `local_point`.
pub viewport_to_local: LayoutTransform2D,
/// Box geometry copied from the same frozen tree when the hit source owns a
/// CSS box. Consumers use it for source-dependent follow-up work such as
/// descending through a transformed child-frame content box without
@@ -363,11 +375,21 @@ where
.local_to_viewport
.inverse()?;
let local_point = inverse.map_point(viewport_point);
let local_content_box = self
.fragment(entry.fragment)
.and_then(|fragment| fragment.box_model.map(|model| model.content))
.or_else(|| {
let box_id = self.fragment_box_id(entry.fragment)?;
let geometry = self.box_geometry(box_id)?;
Some(geometry.content_box)
});
entry.local_rect.contains(local_point).then_some(LayoutHit {
source: entry.source,
fragment: Some(entry.fragment),
local_point,
is_text: entry.is_text,
local_content_box,
viewport_to_local: inverse,
box_model: self.box_model_for_source(entry.source),
})
}
@@ -53,6 +53,19 @@ where
self.element_metrics_for_source_with_offset_parent_filter(source, |_| true)
}
/// Returns the exact unprojected content box produced for a source's
/// principal layout box.
///
/// Embedded browsing contexts use this as their LocalFrameView size. The
/// value deliberately comes from used layout geometry rather than
/// authored width/height CSS, and therefore remains correct for
/// percentages, `calc()`, box sizing, borders, padding, and transforms.
pub fn local_content_box_for_source(&self, source: N) -> Option<LayoutRect> {
let box_id = self.source_output(source)?.principal_box?;
self.box_geometry(box_id)
.map(|geometry| geometry.content_box)
}
/// Resolves CSSOM View element metrics while allowing the renderer to hide
/// flat-tree ancestors that do not belong to the queried element's
/// ancestor tree scopes.
@@ -93,6 +93,21 @@ impl JsContextHost {
)
}
/// Returns the last exact used content viewport published by the iframe's
/// parent layout. A non-blocking borrow is intentional: child style
/// 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> {
self.document_layout_state
.try_borrow()
.ok()?
.embedded_frame_viewport(frame)
}
pub(crate) fn with_fresh_layout_pass_for_document<T>(
&self,
document: DomHandle,
@@ -106,6 +121,7 @@ impl JsContextHost {
else {
return Ok(None);
};
let pass_viewport = request.viewport;
let _active = ActiveLayoutPass::enter(&self.layout_pass_active)?;
let mut pass = {
let mut state = self.document_layout_state.borrow_mut();
@@ -113,6 +129,9 @@ 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.with_services_for_document(
document,
self.document_handle(),
@@ -137,10 +156,30 @@ impl JsContextHost {
pass.validate_retention_budget()?;
let consumed = consume(&mut pass)?;
let metrics = pass.metrics;
let embedded_frame_viewports = self
.child_browsing_context_handles_in_document_order()
.into_iter()
.filter(|frame| self.dom_host().owner_document_handle(*frame) == Some(document))
.map(|frame| {
let viewport = pass
.tree
.local_content_box_for_source(frame)
.map(|content| {
LayoutViewport::new(
css_viewport_dimension(f64::from(content.width)),
css_viewport_dimension(f64::from(content.height)),
pass_viewport.device_pixel_ratio,
)
});
(frame, viewport)
})
.collect::<Vec<_>>();
let tree = pass.into_tree();
self.document_layout_state
.borrow_mut()
.publish_latest_layout(document, tree);
{
let mut state = self.document_layout_state.borrow_mut();
state.publish_latest_layout(document, tree);
state.publish_embedded_frame_viewports(embedded_frame_viewports);
}
self.last_layout_pass_metrics.set(Some(metrics));
self.layout_snapshot_cache_publishes
.set(self.layout_snapshot_cache_publishes.get().saturating_add(1));
@@ -189,6 +228,31 @@ impl JsContextHost {
reason: LayoutFlushReason,
viewport: LayoutViewport,
queries: &LayoutQueryBatch<DomHandle>,
) -> Result<LayoutAnswers<DomHandle>, LayoutError> {
self.answer_layout_for_document_with_viewport_policy(
document, reason, viewport, queries, false,
)
}
pub(crate) fn answer_layout_for_document_with_exact_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, true,
)
}
fn answer_layout_for_document_with_viewport_policy(
&self,
document: DomHandle,
reason: LayoutFlushReason,
viewport: LayoutViewport,
queries: &LayoutQueryBatch<DomHandle>,
require_exact_viewport: bool,
) -> Result<LayoutAnswers<DomHandle>, LayoutError> {
#[cfg(test)]
let reuse_latest = !self.force_fresh_layout_reads_for_test;
@@ -196,11 +260,14 @@ impl JsContextHost {
let reuse_latest = true;
let cached = if reuse_latest {
let state = self.document_layout_state.borrow();
state.latest_layout(document).and_then(|tree| {
self.last_layout_pass_metrics
.get()
.map(|metrics| self.answer_layout_queries(tree, metrics, viewport, queries))
})
state
.latest_layout(document)
.filter(|tree| !require_exact_viewport || tree.viewport == viewport)
.and_then(|tree| {
self.last_layout_pass_metrics
.get()
.map(|metrics| self.answer_layout_queries(tree, metrics, viewport, queries))
})
} else {
None
};
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use moli_layout::{DocumentLayoutServices, FrozenLayoutTree};
use moli_layout::{DocumentLayoutServices, FrozenLayoutTree, LayoutViewport};
use super::layout_snapshot::LatestLayoutTreeCache;
use crate::{
@@ -25,6 +25,11 @@ pub(super) struct DocumentLayoutState {
web_fonts: DocumentWebFontState,
web_font_sources_dirty: bool,
latest_layout: LatestLayoutTreeCache,
/// Last used content viewport published by each live iframe owner's
/// 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>,
}
impl Default for DocumentLayoutState {
@@ -35,6 +40,7 @@ impl Default for DocumentLayoutState {
web_fonts: DocumentWebFontState::default(),
web_font_sources_dirty: true,
latest_layout: LatestLayoutTreeCache::default(),
embedded_frame_viewports: HashMap::new(),
}
}
}
@@ -100,6 +106,34 @@ 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 publish_embedded_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);
}
None => {
self.embedded_frame_viewports.remove(&frame);
}
}
}
}
pub(super) fn retain_live_embedded_frame_viewports(
&mut self,
mut is_live: impl FnMut(DomHandle) -> bool,
) {
self.embedded_frame_viewports
.retain(|frame, _| is_live(*frame));
}
#[cfg(test)]
pub(super) fn latest_layout_observability(
&self,
@@ -25,10 +25,10 @@ pub(in crate::native_bridge) use metrics::{
node_scroll_width_getter_function,
};
pub(crate) use provider::{
observable_bounding_client_rect, observable_bounding_client_rects, observable_caret_position,
observable_client_rects, observable_deep_hit_test, observable_element_metrics,
observable_event_offset, observable_geometry_batch, observable_hit_test_all,
observable_input_hit_test, observable_scroll_adjusted_client_rect,
observable_bounding_client_rect, observable_bounding_client_rects, observable_box_model,
observable_caret_position, observable_client_rects, observable_deep_hit_test,
observable_element_metrics, observable_event_offset, observable_geometry_batch,
observable_hit_test_all, observable_input_hit_test, observable_scroll_adjusted_client_rect,
observable_scroll_into_view_geometry, observable_scrollbar_hit_test,
observable_sources_with_fragments,
};
@@ -10,7 +10,7 @@ use super::super::super::{
use super::super::styles::raw_inline_style_property_value;
use super::super::{queue_revealed_lazy_image_loads, queue_revealed_lazy_media_loads};
use super::{
ClientRect, compute_mock_client_rect, observable_element_metrics,
ClientRect, compute_mock_client_rect, observable_box_model, observable_element_metrics,
observable_scroll_into_view_geometry,
};
@@ -499,6 +499,7 @@ pub(crate) fn scroll_node_into_view_if_needed(
runtime_ptr,
handle,
relative_rect,
None,
ScrollIntoViewAlignment::Nearest,
ScrollIntoViewAlignment::Nearest,
true,
@@ -518,6 +519,7 @@ fn scroll_node_into_view(
runtime_ptr,
handle,
None,
None,
horizontal,
vertical,
false,
@@ -544,6 +546,7 @@ fn scroll_node_into_view_with_geometry(
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,
@@ -575,9 +578,6 @@ fn scroll_node_into_view_with_geometry(
else {
return Ok(None);
};
if geometry.target_rects.is_empty() {
return Ok(None);
}
if let Some(relative) = relative_rect {
let Some(bounds) = quads_bounding_rect(&geometry.target_rects) else {
return Ok(None);
@@ -592,6 +592,15 @@ 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);
}
let target_document = runtime.dom_host().owner_document_handle(target);
let mut changed = false;
@@ -686,17 +695,30 @@ fn scroll_node_into_view_with_geometry(
);
}
if frame_depth < 16
&& let Some(frame) = target_document.and_then(|document| {
&& 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,
@@ -708,6 +730,40 @@ fn scroll_node_into_view_with_geometry(
Ok(Some(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(
child_quads: &[moli_layout::LayoutQuad],
child_viewport: moli_layout::LayoutViewport,
parent_content: moli_layout::LayoutQuad,
) -> Option<Vec<moli_layout::LayoutQuad>> {
let width = child_viewport.css_width as f32;
let height = child_viewport.css_height as f32;
if width <= 0.0 || height <= 0.0 {
return None;
}
let [origin, x_corner, _, y_corner] = parent_content.points;
let x_basis = moli_layout::LayoutPoint::new(x_corner.x - origin.x, x_corner.y - origin.y);
let y_basis = moli_layout::LayoutPoint::new(y_corner.x - origin.x, y_corner.y - origin.y);
Some(
child_quads
.iter()
.map(|quad| moli_layout::LayoutQuad {
points: quad.points.map(|point| {
let u = point.x / width;
let v = point.y / height;
moli_layout::LayoutPoint::new(
origin.x + x_basis.x * u + y_basis.x * v,
origin.y + x_basis.y * u + y_basis.y * v,
)
}),
})
.collect(),
)
}
fn quads_bounding_rect(quads: &[moli_layout::LayoutQuad]) -> Option<moli_layout::LayoutRect> {
quads
.iter()
@@ -1312,7 +1368,7 @@ fn reveal_lazy_images_for_scroll(
#[cfg(test)]
mod scroll_alignment_tests {
use super::scroll_axis_to_expose;
use super::{map_child_viewport_quads_to_parent, scroll_axis_to_expose};
#[test]
fn center_if_needed_returns_the_unclamped_chromium_alignment_position() {
@@ -1328,4 +1384,52 @@ mod scroll_alignment_tests {
assert_eq!(scroll_axis_to_expose(200.0, 220.0, 0.0, viewport), 160.0);
assert_eq!(scroll_axis_to_expose(0.0, 20.0, 200.0, viewport), -40.0);
}
#[test]
fn child_viewport_rect_maps_through_the_parent_content_quad() {
let child_quad = moli_layout::LayoutTransform2D::IDENTITY
.map_rect(moli_layout::LayoutRect::new(50.0, 25.0, 100.0, 50.0));
let parent_content = moli_layout::LayoutQuad {
points: [
moli_layout::LayoutPoint::new(10.0, 20.0),
moli_layout::LayoutPoint::new(110.0, 40.0),
moli_layout::LayoutPoint::new(90.0, 100.0),
moli_layout::LayoutPoint::new(-10.0, 80.0),
],
};
let mapped = map_child_viewport_quads_to_parent(
&[child_quad],
moli_layout::LayoutViewport::new(200, 100, 1.0),
parent_content,
)
.expect("non-empty child viewport should map");
assert_eq!(
mapped[0].points,
[
moli_layout::LayoutPoint::new(30.0, 40.0),
moli_layout::LayoutPoint::new(80.0, 50.0),
moli_layout::LayoutPoint::new(70.0, 80.0),
moli_layout::LayoutPoint::new(20.0, 70.0),
]
);
}
#[test]
fn empty_child_viewport_cannot_produce_parent_geometry() {
let child_quad = moli_layout::LayoutTransform2D::IDENTITY
.map_rect(moli_layout::LayoutRect::new(0.0, 0.0, 10.0, 10.0));
let parent_content = moli_layout::LayoutTransform2D::IDENTITY
.map_rect(moli_layout::LayoutRect::new(0.0, 0.0, 10.0, 10.0));
assert!(
map_child_viewport_quads_to_parent(
&[child_quad],
moli_layout::LayoutViewport::new(0, 100, 1.0),
parent_content,
)
.is_none()
);
}
}
@@ -5,6 +5,7 @@ use moli_layout::{
LayoutElementMetrics, LayoutError, LayoutFlushReason, LayoutHit, LayoutIntersectionGeometry,
LayoutPassMetrics, LayoutPoint, LayoutQuad, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch,
LayoutScrollContainerMetrics, LayoutScrollIntoViewGeometry, LayoutScrollbarHit, LayoutSize,
LayoutTransform2D, LayoutViewport,
};
use super::layout::{
@@ -16,6 +17,16 @@ 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) 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,
}
pub(crate) fn observable_geometry_batch(
runtime: &JsContextHost,
document: DomHandle,
@@ -149,6 +160,29 @@ pub(crate) fn observable_bounding_client_rects(
.collect()
}
pub(crate) fn observable_box_model(
runtime: &JsContextHost,
source: DomHandle,
reason: LayoutFlushReason,
) -> Result<Option<LayoutBoxModel>, LayoutError> {
if !runtime.dom_host().is_connected(source) {
return Ok(None);
}
let Some(document) = runtime.layout_document_for_source(source) else {
return Ok(None);
};
let answers = observable_geometry_batch(
runtime,
document,
reason,
&LayoutQueryBatch::new(vec![LayoutQuery::BoxModel { source }]),
)?;
match answers.answers.into_iter().next() {
Some(LayoutQueryAnswer::BoxModel(model)) => Ok(model),
_ => Err(provider_contract_error("box model")),
}
}
/// Resolve one viewport-relative box for scroll anchoring. Real layout already
/// projects root scrolling into viewport coordinates; the explicit adjustment
/// is retained only inside the legacy Mock provider.
@@ -260,6 +294,32 @@ fn observable_hit_test(
}
}
fn observable_hit_test_with_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);
}
let answers = runtime.answer_layout_for_document_with_exact_viewport(
document,
reason,
viewport,
&LayoutQueryBatch::new(vec![LayoutQuery::HitTest {
point,
ignore_pointer_events_none,
}]),
)?;
match answers.answers.into_iter().next() {
Some(LayoutQueryAnswer::HitTest(hit)) => Ok(hit),
_ => Err(provider_contract_error("viewport-scoped hit test")),
}
}
pub(crate) fn observable_caret_position(
runtime: &JsContextHost,
document: DomHandle,
@@ -295,8 +355,8 @@ pub(crate) fn observable_input_hit_test(
runtime: &JsContextHost,
document: DomHandle,
point: LayoutPoint,
) -> Result<Option<DomHandle>, LayoutError> {
observable_deep_hit_test(runtime, document, point, false)
) -> Result<Option<ObservableInputHit>, LayoutError> {
observable_deep_input_hit_test(runtime, document, point, false)
}
pub(crate) fn observable_scrollbar_hit_test(
@@ -307,17 +367,14 @@ pub(crate) fn observable_scrollbar_hit_test(
if !runtime.layout_policy().uses_real_layout() {
return Ok(None);
}
// Publish a current tree through the normal on-demand layout boundary;
// the control hit itself deliberately does not become another query model.
let _ = observable_geometry_batch(
observable_deep_scrollbar_hit_test_inner(
runtime,
document,
LayoutFlushReason::HitTest,
&LayoutQueryBatch::new(vec![LayoutQuery::DocumentMetrics]),
)?;
Ok(runtime
.with_latest_layout_tree_for_document(document, |tree| tree.scrollbar_hit_test(point))
.flatten())
runtime.layout_viewport_for_document(document),
point,
LayoutTransform2D::IDENTITY,
0,
)
}
pub(crate) fn observable_deep_hit_test(
@@ -326,19 +383,150 @@ pub(crate) fn observable_deep_hit_test(
point: LayoutPoint,
ignore_pointer_events_none: bool,
) -> Result<Option<DomHandle>, LayoutError> {
observable_deep_hit_test_inner(runtime, document, point, ignore_pointer_events_none, 0)
Ok(
observable_deep_input_hit_test(runtime, document, point, ignore_pointer_events_none)?
.map(|hit| hit.handle),
)
}
fn observable_deep_hit_test_inner(
fn observable_deep_input_hit_test(
runtime: &JsContextHost,
document: DomHandle,
point: LayoutPoint,
ignore_pointer_events_none: bool,
depth: usize,
) -> Result<Option<DomHandle>, LayoutError> {
let first_hit = observable_hit_test(
) -> Result<Option<ObservableInputHit>, LayoutError> {
observable_deep_input_hit_test_inner(
runtime,
document,
runtime.layout_viewport_for_document(document),
point,
LayoutTransform2D::IDENTITY,
ignore_pointer_events_none,
0,
)
}
fn observable_deep_input_hit_test_inner(
runtime: &JsContextHost,
document: DomHandle,
viewport: LayoutViewport,
point: LayoutPoint,
root_to_viewport: LayoutTransform2D,
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,
)?
else {
return Ok(None);
};
let target_hit = ObservableInputHit {
handle: target,
root_to_client: root_to_viewport,
};
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 {
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)))
}
fn observable_deep_scrollbar_hit_test_inner(
runtime: &JsContextHost,
document: DomHandle,
viewport: LayoutViewport,
point: LayoutPoint,
root_to_viewport: LayoutTransform2D,
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)?;
if let Some(mut hit) = runtime
.with_latest_layout_tree_for_document(document, |tree| tree.scrollbar_hit_test(point))
.flatten()
{
hit.viewport_to_local = hit.viewport_to_local.concatenate(root_to_viewport);
return Ok(Some(hit));
}
if depth >= HIT_TEST_CHILD_FRAME_DEPTH_LIMIT {
return Ok(None);
}
let Some((frame_hit, frame)) = live_hit else {
return Ok(None);
};
let Some(child_document) = runtime.child_browsing_context_document_handle(frame) 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,
)
}
fn observable_live_hit_with_viewport(
runtime: &JsContextHost,
document: DomHandle,
viewport: LayoutViewport,
point: LayoutPoint,
ignore_pointer_events_none: bool,
) -> Result<Option<(LayoutHit<DomHandle>, DomHandle)>, LayoutError> {
let first_hit = observable_hit_test_with_viewport(
runtime,
document,
viewport,
point,
ignore_pointer_events_none,
LayoutFlushReason::HitTest,
@@ -355,55 +543,24 @@ fn observable_deep_hit_test_inner(
// paint stack only on that stale-source path until it identifies a
// currently connected element. This live DOM check neither refreshes nor
// invalidates layout; the common case retains the single-hit fast path.
let fallback_hit = if live_first_hit.is_none() && first_hit.is_some() {
let (_, hits) = observable_hit_test_all(
runtime,
document,
point,
ignore_pointer_events_none,
LayoutFlushReason::HitTest,
)?;
hits.into_iter().find_map(|hit| {
let target = element_for_hit_source(runtime, hit.source)?;
runtime
.dom_host()
.is_connected(target)
.then_some((hit, target))
})
} else {
None
};
let Some((hit, target)) = live_first_hit.or(fallback_hit) else {
return Ok(None);
};
if depth >= HIT_TEST_CHILD_FRAME_DEPTH_LIMIT {
return Ok(Some(target));
if live_first_hit.is_some() || first_hit.is_none() {
return Ok(live_first_hit);
}
let Some(child_document) = runtime.child_browsing_context_document_handle(target) else {
return Ok(Some(target));
};
let Some(content_quad) = hit.box_model.map(|model| model.content) else {
return Ok(Some(target));
};
let child_viewport = runtime.layout_viewport_for_document(child_document);
let Some(child_point) = map_point_into_quad(
point,
content_quad,
LayoutSize::new(
child_viewport.css_width as f32,
child_viewport.css_height as f32,
),
) else {
return Ok(Some(target));
};
Ok(observable_deep_hit_test_inner(
let hits = observable_hit_test_all_with_viewport(
runtime,
child_document,
child_point,
document,
viewport,
point,
ignore_pointer_events_none,
depth + 1,
)?
.or(Some(target)))
LayoutFlushReason::HitTest,
)?;
Ok(hits.into_iter().find_map(|hit| {
let target = element_for_hit_source(runtime, hit.source)?;
runtime
.dom_host()
.is_connected(target)
.then_some((hit, target))
}))
}
fn element_for_hit_source(runtime: &JsContextHost, mut source: DomHandle) -> Option<DomHandle> {
@@ -420,36 +577,6 @@ fn element_for_hit_source(runtime: &JsContextHost, mut source: DomHandle) -> Opt
}
}
fn map_point_into_quad(
point: LayoutPoint,
quad: LayoutQuad,
target_size: LayoutSize,
) -> Option<LayoutPoint> {
let [origin, x_corner, _, y_corner] = quad.points;
let basis_x = (
f64::from(x_corner.x - origin.x),
f64::from(x_corner.y - origin.y),
);
let basis_y = (
f64::from(y_corner.x - origin.x),
f64::from(y_corner.y - origin.y),
);
let relative = (f64::from(point.x - origin.x), f64::from(point.y - origin.y));
let determinant = basis_x.0 * basis_y.1 - basis_x.1 * basis_y.0;
if !determinant.is_finite() || determinant.abs() <= f64::EPSILON {
return None;
}
let u = (relative.0 * basis_y.1 - relative.1 * basis_y.0) / determinant;
let v = (basis_x.0 * relative.1 - basis_x.1 * relative.0) / determinant;
if !(0.0..=1.0).contains(&u) || !(0.0..=1.0).contains(&v) {
return None;
}
Some(LayoutPoint::new(
(u * f64::from(target_size.width)) as f32,
(v * f64::from(target_size.height)) as f32,
))
}
pub(crate) fn observable_hit_test_all(
runtime: &JsContextHost,
document: DomHandle,
@@ -479,6 +606,46 @@ pub(crate) fn observable_hit_test_all(
}
}
fn observable_hit_test_all_with_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(
runtime,
document,
point,
ignore_pointer_events_none,
reason,
)
.map(|(_, hits)| hits);
}
let answers = runtime.answer_layout_for_document_with_exact_viewport(
document,
reason,
viewport,
&LayoutQueryBatch::new(vec![LayoutQuery::HitTestAll {
point,
ignore_pointer_events_none,
}]),
)?;
match answers.answers.into_iter().next() {
Some(LayoutQueryAnswer::HitTestAll(hits)) => Ok(hits),
_ => Err(provider_contract_error("viewport-scoped complete hit test")),
}
}
fn css_viewport_dimension(value: f32) -> u32 {
if !value.is_finite() {
return 0;
}
value.round().clamp(0.0, u32::MAX as f32) as u32
}
fn answer_mock_queries(
runtime: &JsContextHost,
document: DomHandle,
@@ -554,6 +721,8 @@ fn answer_mock_queries(
fragment: None,
local_point: *point,
is_text: false,
local_content_box: None,
viewport_to_local: moli_layout::LayoutTransform2D::IDENTITY,
box_model: mock_box_model(runtime, source),
}),
),
@@ -568,6 +737,8 @@ fn answer_mock_queries(
fragment: None,
local_point: *point,
is_text: false,
local_content_box: None,
viewport_to_local: moli_layout::LayoutTransform2D::IDENTITY,
box_model: mock_box_model(runtime, source),
}]
})
@@ -692,6 +692,15 @@ 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) {
return Some(
StyleViewport::new(
Some(f64::from(viewport.css_width)),
Some(f64::from(viewport.css_height)),
)
.with_screen_size(parent_viewport.screen_width, parent_viewport.screen_height),
);
}
let computed_width = style_property_value_with_context(
host,
frame_handle,
+6
View File
@@ -924,6 +924,11 @@ 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,
active_touch_pointer_handle: Option<DomHandle>,
active_touch_pointer_handles: BTreeMap<i32, DomHandle>,
active_touch_event_handle: Option<DomHandle>,
@@ -2309,6 +2314,7 @@ impl ScriptVmDefaultWorldBootstrap {
pressed_mouse_buttons: 0,
pending_mouse_press: None,
hovered_mouse_handle: None,
hovered_mouse_root_to_client: moli_layout::LayoutTransform2D::IDENTITY,
active_touch_pointer_handle: None,
active_touch_pointer_handles: BTreeMap::new(),
active_touch_event_handle: None,
@@ -480,11 +480,13 @@ impl ScriptVm {
return Ok(outcome);
}
let hit_handle = observable_input_hit_test(
let root_point = moli_layout::LayoutPoint::new(x as f32, y as f32);
let hit = observable_input_hit_test(
&self._context_host.borrow(),
self.document_runtime.document_handle(),
moli_layout::LayoutPoint::new(x as f32, y as f32),
root_point,
)?;
let hit_handle = hit.map(|hit| hit.handle);
let pointer_event_name = pointer_event_name_for_mouse_event(event_name);
let mut pending_pointer_capture_events = Vec::new();
if pointer_event_name.is_some() {
@@ -539,12 +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
.filter(|hit| hit.handle == handle)
.map(|hit| hit.root_to_client)
.unwrap_or(moli_layout::LayoutTransform2D::IDENTITY);
let client_point = root_to_client.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;
self.hovered_mouse_handle = Some(handle);
Some(previous)
self.hovered_mouse_root_to_client = root_to_client;
Some((previous, previous_root_to_client))
} else {
self.hovered_mouse_root_to_client = root_to_client;
None
}
} else {
@@ -588,13 +600,16 @@ impl ScriptVm {
let result = self.with_default_context_scope(|scope, runtime_ptr| {
let mut pointer_dispatch_handle = handle;
if let Some(Some(previous_handle)) = hover_transition {
if let Some((Some(previous_handle), previous_root_to_client)) = hover_transition {
let previous_client_point = previous_root_to_client.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));
if let Some(event) = construct_pointer_event_with_related_target_and_modifiers(
scope,
"pointerout",
x,
y,
previous_client_x,
previous_client_y,
button,
buttons,
&pointer,
@@ -607,8 +622,8 @@ impl ScriptVm {
if let Some(event) = construct_pointer_event_with_related_target_and_modifiers(
scope,
"pointerleave",
x,
y,
previous_client_x,
previous_client_y,
button,
buttons,
&pointer,
@@ -618,13 +633,13 @@ impl ScriptVm {
let _ = dispatch_public_event(scope, runtime_ptr, previous_handle, event);
}
}
if let Some(previous_handle) = hover_transition {
if let Some((previous_handle, _)) = hover_transition {
let related_target = related_target_value(scope, previous_handle);
if let Some(event) = construct_pointer_event_with_related_target_and_modifiers(
scope,
"pointerover",
x,
y,
client_x,
client_y,
button,
buttons,
&pointer,
@@ -637,8 +652,8 @@ impl ScriptVm {
if let Some(event) = construct_pointer_event_with_related_target_and_modifiers(
scope,
"pointerenter",
x,
y,
client_x,
client_y,
button,
buttons,
&pointer,
@@ -652,8 +667,8 @@ impl ScriptVm {
&& let Some(event) = construct_pointer_event_with_modifiers(
scope,
"pointerrawupdate",
x,
y,
client_x,
client_y,
button,
buttons,
&pointer,
@@ -670,8 +685,8 @@ impl ScriptVm {
runtime_ptr,
MOUSE_POINTER_ID,
&post_raw_update_capture_events,
x,
y,
client_x,
client_y,
button,
buttons,
&pointer,
@@ -684,13 +699,16 @@ impl ScriptVm {
.unwrap_or(pointer_dispatch_handle);
}
}
if let Some(Some(previous_handle)) = hover_transition {
if let Some((Some(previous_handle), previous_root_to_client)) = hover_transition {
let previous_client_point = previous_root_to_client.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));
if let Some(event) = construct_mouse_event_with_related_target_and_modifiers(
scope,
"mouseout",
x,
y,
previous_client_x,
previous_client_y,
button,
buttons,
related_target,
@@ -702,8 +720,8 @@ impl ScriptVm {
if let Some(event) = construct_mouse_event_with_related_target_and_modifiers(
scope,
"mouseleave",
x,
y,
previous_client_x,
previous_client_y,
button,
buttons,
related_target,
@@ -712,13 +730,13 @@ impl ScriptVm {
let _ = dispatch_public_event(scope, runtime_ptr, previous_handle, event);
}
}
if let Some(previous_handle) = hover_transition {
if let Some((previous_handle, _)) = hover_transition {
let related_target = related_target_value(scope, previous_handle);
if let Some(event) = construct_mouse_event_with_related_target_and_modifiers(
scope,
"mouseover",
x,
y,
client_x,
client_y,
button,
buttons,
related_target,
@@ -730,8 +748,8 @@ impl ScriptVm {
if let Some(event) = construct_mouse_event_with_related_target_and_modifiers(
scope,
"mouseenter",
x,
y,
client_x,
client_y,
button,
buttons,
related_target,
@@ -744,8 +762,8 @@ impl ScriptVm {
&& let Some(event) = construct_pointer_event_with_modifiers(
scope,
pointer_event_name,
x,
y,
client_x,
client_y,
button,
buttons,
&pointer,
@@ -763,8 +781,8 @@ impl ScriptVm {
scope,
runtime_ptr,
MOUSE_POINTER_ID,
x,
y,
client_x,
client_y,
button,
buttons,
&pointer,
@@ -776,11 +794,12 @@ impl ScriptVm {
if !suppress_current_mouse_event {
let event = if event_name == "wheel" {
construct_wheel_event(
scope, event_name, x, y, delta_x, delta_y, button, buttons, modifiers,
scope, event_name, client_x, client_y, delta_x, delta_y, button, buttons,
modifiers,
)
} else {
construct_mouse_event_with_modifiers(
scope, event_name, x, y, button, buttons, modifiers,
scope, event_name, client_x, client_y, button, buttons, modifiers,
)
};
if let Some(event) = event {
@@ -824,8 +843,14 @@ impl ScriptVm {
};
if let Some(data_transfer) =
crate::context_bootstrap::build_data_transfer_object(scope, &empty_drag_data)
&& let Some(event) =
construct_drag_event(scope, "dragstart", x, y, data_transfer.into(), 0)
&& let Some(event) = construct_drag_event(
scope,
"dragstart",
client_x,
client_y,
data_transfer.into(),
0,
)
{
if dispatch_public_event(scope, runtime_ptr, drag_start_handle, event)
.allows_default()
@@ -843,9 +868,14 @@ impl ScriptVm {
let active_drag_session = unsafe { &mut *active_drag_session };
if let Some(session) = active_drag_session.as_mut() {
let data_transfer = v8::Local::new(scope, &session.data_transfer);
if let Some(event) =
construct_drag_event(scope, "dragover", x, y, data_transfer.into(), 0)
{
if let Some(event) = construct_drag_event(
scope,
"dragover",
client_x,
client_y,
data_transfer.into(),
0,
) {
session.drop_allowed =
!dispatch_public_event(scope, runtime_ptr, handle, event)
.allows_default();
@@ -859,9 +889,14 @@ impl ScriptVm {
if let Some(session) = active_drag_session.as_mut() {
if session.drop_allowed {
let data_transfer = v8::Local::new(scope, &session.data_transfer);
let allows_default = if let Some(event) =
construct_drag_event(scope, "drop", x, y, data_transfer.into(), 0)
{
let allows_default = if let Some(event) = construct_drag_event(
scope,
"drop",
client_x,
client_y,
data_transfer.into(),
0,
) {
dispatch_public_event(scope, runtime_ptr, handle, event)
.allows_default()
} else {
@@ -888,8 +923,8 @@ impl ScriptVm {
scope,
runtime_ptr,
handle,
x,
y,
client_x,
client_y,
button,
buttons,
click_count.max(1),
@@ -899,8 +934,8 @@ impl ScriptVm {
&& let Some(event) = construct_mouse_event_with_detail_and_modifiers(
scope,
"dblclick",
x,
y,
client_x,
client_y,
click_count,
button,
buttons,
@@ -918,8 +953,8 @@ impl ScriptVm {
if let Some(event) = construct_mouse_event_with_modifiers(
scope,
follow_up_event_name,
x,
y,
client_x,
client_y,
button,
buttons,
modifiers,
@@ -1047,7 +1082,8 @@ impl ScriptVm {
&self._context_host.borrow(),
self.document_runtime.document_handle(),
moli_layout::LayoutPoint::new(x as f32, y as f32),
)?;
)?
.map(|hit| hit.handle);
if event_name == "touchstart" && hit_handle.is_none() {
return Ok(input_dispatch_outcome(false));
}
@@ -1278,7 +1314,8 @@ impl ScriptVm {
&self._context_host.borrow(),
self.document_runtime.document_handle(),
moli_layout::LayoutPoint::new(point.x as f32, point.y as f32),
)?;
)?
.map(|hit| hit.handle);
let target = match event_name {
"touchstart" => hit_handle,
"touchmove" | "touchend" | "touchcancel" => self
@@ -1505,7 +1542,7 @@ impl ScriptVm {
self.document_runtime.document_handle(),
moli_layout::LayoutPoint::new(x as f32, y as f32),
)?
else {
.map(|hit| hit.handle) else {
return Ok(input_dispatch_outcome(false));
};
@@ -50,6 +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._context_host
.borrow()
.dom_host()