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