mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 00:00:44 +00:00
Simplify iframe input and scroll routing
This commit is contained in:
@@ -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)),
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user