mirror of
https://github.com/lexmount/moli.git
synced 2026-10-01 00:00:48 +00:00
Reuse composite layout snapshots for iframe input
This commit is contained in:
@@ -83,6 +83,13 @@ where
|
||||
self.tree
|
||||
}
|
||||
|
||||
/// Consumes one pass into the sole retainable tree plus optional one-shot
|
||||
/// paint input. Embedded-frame composition uses this to nest the child tree
|
||||
/// into its parent while separately consuming paint into the parent frame.
|
||||
pub fn into_tree_and_paint_snapshot(self) -> (FrozenLayoutTree<N>, Option<PaintSnapshot>) {
|
||||
(self.tree, self.paint_snapshot)
|
||||
}
|
||||
|
||||
pub fn retention_metrics(&self) -> LayoutTreeRetentionMetrics {
|
||||
self.tree.retention_metrics()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,18 @@ pub struct FrozenLayoutBox<N> {
|
||||
pub hit_source: Option<N>,
|
||||
}
|
||||
|
||||
/// One child browsing context owned by the same latest frozen snapshot as its
|
||||
/// parent. It is deliberately recursive rather than an independently keyed
|
||||
/// cache entry, so capturing input geometry does not publish a child over the
|
||||
/// top-level tree.
|
||||
pub struct FrozenEmbeddedFrame<N>
|
||||
where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
pub source: N,
|
||||
pub tree: Box<FrozenLayoutTree<N>>,
|
||||
}
|
||||
|
||||
impl<N> Deref for FrozenLayoutBox<N> {
|
||||
type Target = LayoutBoxGeometry;
|
||||
|
||||
@@ -65,6 +77,7 @@ pub struct FrozenLayoutTree<N>
|
||||
where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
source_root: N,
|
||||
pub viewport: LayoutViewport,
|
||||
pub viewport_scroll: LayoutPoint,
|
||||
pub content_size: LayoutSize,
|
||||
@@ -76,12 +89,31 @@ where
|
||||
pub scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
|
||||
viewport_coordinate_space: FrozenCoordinateSpace,
|
||||
pub clip_chain: Vec<LayoutClipNode>,
|
||||
embedded_frames: Vec<FrozenEmbeddedFrame<N>>,
|
||||
embedded_frames_complete: bool,
|
||||
}
|
||||
|
||||
impl<N> FrozenLayoutTree<N>
|
||||
where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
/// Source root whose complete layout projection this member tree owns.
|
||||
pub const fn source_root(&self) -> N {
|
||||
self.source_root
|
||||
}
|
||||
|
||||
/// Finds an exact Document/source-root member without a per-Document
|
||||
/// index. Browsing-context depth is bounded, so this walks only the small
|
||||
/// recursive frame projection rather than the member trees' boxes.
|
||||
pub fn tree_for_root(&self, source_root: N) -> Option<&FrozenLayoutTree<N>> {
|
||||
if self.source_root == source_root {
|
||||
return Some(self);
|
||||
}
|
||||
self.embedded_frames
|
||||
.iter()
|
||||
.find_map(|frame| frame.tree.tree_for_root(source_root))
|
||||
}
|
||||
|
||||
pub fn retention_metrics(&self) -> LayoutTreeRetentionMetrics {
|
||||
fn allocation<T>(capacity: usize) -> usize {
|
||||
capacity.saturating_mul(std::mem::size_of::<T>())
|
||||
@@ -92,18 +124,35 @@ where
|
||||
layout_box.fragments.capacity(),
|
||||
))
|
||||
});
|
||||
let estimated_geometry_bytes = std::mem::size_of::<Self>()
|
||||
let own_estimated_geometry_bytes = std::mem::size_of::<Self>()
|
||||
.saturating_add(allocation::<FrozenLayoutBox<N>>(self.boxes.capacity()))
|
||||
.saturating_add(allocation::<LayoutFragment>(self.fragments.capacity()))
|
||||
.saturating_add(allocation::<(N, LayoutOutputBoxId)>(
|
||||
self.scroll_proxy_links.capacity(),
|
||||
))
|
||||
.saturating_add(allocation::<LayoutClipNode>(self.clip_chain.capacity()))
|
||||
.saturating_add(allocation::<FrozenEmbeddedFrame<N>>(
|
||||
self.embedded_frames.capacity(),
|
||||
))
|
||||
.saturating_add(box_allocations);
|
||||
let embedded = self.embedded_frames.iter().fold(
|
||||
LayoutTreeRetentionMetrics::default(),
|
||||
|total, frame| {
|
||||
let child = frame.tree.retention_metrics();
|
||||
LayoutTreeRetentionMetrics {
|
||||
box_count: total.box_count.saturating_add(child.box_count),
|
||||
fragment_count: total.fragment_count.saturating_add(child.fragment_count),
|
||||
estimated_geometry_bytes: total
|
||||
.estimated_geometry_bytes
|
||||
.saturating_add(child.estimated_geometry_bytes),
|
||||
}
|
||||
},
|
||||
);
|
||||
LayoutTreeRetentionMetrics {
|
||||
box_count: self.boxes.len(),
|
||||
fragment_count: self.fragments.len(),
|
||||
estimated_geometry_bytes,
|
||||
box_count: self.boxes.len().saturating_add(embedded.box_count),
|
||||
fragment_count: self.fragments.len().saturating_add(embedded.fragment_count),
|
||||
estimated_geometry_bytes: own_estimated_geometry_bytes
|
||||
.saturating_add(embedded.estimated_geometry_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +187,25 @@ where
|
||||
.get(id.index())
|
||||
.map(|layout_box| &layout_box.scroll_extent)
|
||||
}
|
||||
|
||||
/// Returns the embedded child tree frozen into this same retained snapshot.
|
||||
pub fn embedded_frame_tree(&self, source: N) -> Option<&FrozenLayoutTree<N>> {
|
||||
self.embedded_frames
|
||||
.iter()
|
||||
.find(|frame| frame.source == source)
|
||||
.map(|frame| frame.tree.as_ref())
|
||||
}
|
||||
|
||||
/// Whether every live child frame encountered by this pass was projected.
|
||||
/// Hit-test demands require this bit before reusing a tree produced by a
|
||||
/// geometry-only pass.
|
||||
pub const fn embedded_frames_complete(&self) -> bool {
|
||||
self.embedded_frames_complete
|
||||
}
|
||||
|
||||
pub fn embedded_frames(&self) -> impl ExactSizeIterator<Item = &FrozenEmbeddedFrame<N>> {
|
||||
self.embedded_frames.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> FrozenLayoutTree<N>
|
||||
@@ -145,6 +213,7 @@ where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
source_root: N,
|
||||
viewport: LayoutViewport,
|
||||
viewport_scroll: LayoutPoint,
|
||||
content_size: LayoutSize,
|
||||
@@ -154,8 +223,11 @@ where
|
||||
scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
|
||||
viewport_coordinate_space: FrozenCoordinateSpace,
|
||||
clip_chain: Vec<LayoutClipNode>,
|
||||
embedded_frames: Vec<FrozenEmbeddedFrame<N>>,
|
||||
embedded_frames_complete: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
source_root,
|
||||
viewport,
|
||||
viewport_scroll,
|
||||
content_size,
|
||||
@@ -165,6 +237,8 @@ where
|
||||
scroll_proxy_links,
|
||||
viewport_coordinate_space,
|
||||
clip_chain,
|
||||
embedded_frames,
|
||||
embedded_frames_complete,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -45,23 +45,24 @@ pub use capture::{
|
||||
};
|
||||
pub use error::LayoutError;
|
||||
pub use layout_tree::{
|
||||
FrozenCoordinateSpace, FrozenLayoutBox, FrozenLayoutTree, GeometryProvider, LayoutAnswers,
|
||||
LayoutBoxGeometry, LayoutBoxModel, LayoutCaretPosition, LayoutClipChainId, LayoutClipNode,
|
||||
LayoutCoordinateSpaceId, LayoutDocumentMetrics, LayoutElementMetrics, LayoutFlushReason,
|
||||
LayoutFragment, LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHit,
|
||||
LayoutIntersectionGeometry, LayoutNodeOutput, LayoutOutputBoxId, LayoutPassMetrics,
|
||||
LayoutPassResult, LayoutPoint, LayoutQuad, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch,
|
||||
LayoutRect, LayoutScrollContainerMetrics, LayoutScrollExtent, LayoutScrollIntoViewGeometry,
|
||||
LayoutSize, LayoutTransform2D, LayoutTreeRetentionMetrics, LayoutViewport,
|
||||
MAX_RETAINED_LAYOUT_BOXES, MAX_RETAINED_LAYOUT_FRAGMENTS, MAX_RETAINED_LAYOUT_TREE_BYTES,
|
||||
FrozenCoordinateSpace, FrozenEmbeddedFrame, FrozenLayoutBox, FrozenLayoutTree,
|
||||
GeometryProvider, LayoutAnswers, LayoutBoxGeometry, LayoutBoxModel, LayoutCaretPosition,
|
||||
LayoutClipChainId, LayoutClipNode, LayoutCoordinateSpaceId, LayoutDocumentMetrics,
|
||||
LayoutElementMetrics, LayoutFlushReason, LayoutFragment, LayoutFragmentBoxModel,
|
||||
LayoutFragmentId, LayoutFragmentKind, LayoutHit, LayoutIntersectionGeometry, LayoutNodeOutput,
|
||||
LayoutOutputBoxId, LayoutPassMetrics, LayoutPassResult, LayoutPoint, LayoutQuad, LayoutQuery,
|
||||
LayoutQueryAnswer, LayoutQueryBatch, LayoutRect, LayoutScrollContainerMetrics,
|
||||
LayoutScrollExtent, LayoutScrollIntoViewGeometry, LayoutSize, LayoutTransform2D,
|
||||
LayoutTreeRetentionMetrics, LayoutViewport, MAX_RETAINED_LAYOUT_BOXES,
|
||||
MAX_RETAINED_LAYOUT_FRAGMENTS, MAX_RETAINED_LAYOUT_TREE_BYTES,
|
||||
};
|
||||
pub use normalize::{NormalizedBoxNode, NormalizedBoxTree, NormalizedFormattingContext};
|
||||
pub use normalize_source::{
|
||||
NormalizedLayoutSourceNode, NormalizedLayoutSourceTree, normalize_layout_source,
|
||||
};
|
||||
pub use pass::{
|
||||
EmbeddedFrameRenderer, LayoutPassRequest, ScreenshotLayoutRequest, build_layout_pass,
|
||||
build_layout_pass_with_embedded_frames, build_screenshot_snapshot,
|
||||
EmbeddedFrameRenderer, EmbeddedFrameSnapshot, LayoutPassRequest, ScreenshotLayoutRequest,
|
||||
build_layout_pass, build_layout_pass_with_embedded_frames, build_screenshot_snapshot,
|
||||
};
|
||||
pub use scrollbar::{
|
||||
LayoutScrollbarAxis, LayoutScrollbarColors, LayoutScrollbarGeometry, LayoutScrollbarGutter,
|
||||
|
||||
+50
-14
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
DocumentLayoutServices, LayoutError, LayoutFlushReason, LayoutPassResult, LayoutScrollbarAxis,
|
||||
LayoutSource, LayoutStyleResolver, LayoutViewport, PaintCaptureRequest, PaintSnapshot,
|
||||
PaintViewport, build_layout_world,
|
||||
DocumentLayoutServices, FrozenLayoutTree, LayoutError, LayoutFlushReason, LayoutPassResult,
|
||||
LayoutScrollbarAxis, LayoutSource, LayoutStyleResolver, LayoutViewport, PaintCaptureRequest,
|
||||
PaintSnapshot, PaintViewport, build_layout_world,
|
||||
form::prepare_form_controls,
|
||||
inline::prepare_inline_contexts,
|
||||
list::prepare_list_markers,
|
||||
@@ -11,6 +11,26 @@ use crate::{
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Owned products for one embedded browsing context in the same synchronous
|
||||
/// layout demand as its parent.
|
||||
pub struct EmbeddedFrameSnapshot<N>
|
||||
where
|
||||
N: Copy + std::fmt::Debug + Eq + std::hash::Hash,
|
||||
{
|
||||
pub tree: FrozenLayoutTree<N>,
|
||||
pub paint: Option<PaintSnapshot>,
|
||||
}
|
||||
|
||||
impl<N> EmbeddedFrameSnapshot<N>
|
||||
where
|
||||
N: Copy + std::fmt::Debug + Eq + std::hash::Hash,
|
||||
{
|
||||
#[must_use]
|
||||
pub const fn new(tree: FrozenLayoutTree<N>, paint: Option<PaintSnapshot>) -> Self {
|
||||
Self { tree, paint }
|
||||
}
|
||||
}
|
||||
|
||||
/// Renderer-owned bridge for one live embedded browsing context.
|
||||
///
|
||||
/// The parent numeric layout has already completed when this callback runs, so
|
||||
@@ -18,23 +38,30 @@ use std::time::Instant;
|
||||
/// computed-style estimate. Implementations must return an owned, source-free
|
||||
/// snapshot and must not run JavaScript, lifecycle work, or an event-loop turn.
|
||||
/// Recursive child layout is allowed because every nested world remains local
|
||||
/// to the same synchronous demand.
|
||||
pub trait EmbeddedFrameRenderer<N> {
|
||||
/// to the same synchronous demand. The child tree is consumed into the single
|
||||
/// parent-owned frozen snapshot; it is not a separately retained cache entry.
|
||||
pub trait EmbeddedFrameRenderer<N>
|
||||
where
|
||||
N: Copy + std::fmt::Debug + Eq + std::hash::Hash,
|
||||
{
|
||||
fn render_embedded_frame(
|
||||
&mut self,
|
||||
frame: N,
|
||||
viewport: LayoutViewport,
|
||||
) -> Result<Option<PaintSnapshot>, LayoutError>;
|
||||
) -> Result<Option<EmbeddedFrameSnapshot<N>>, LayoutError>;
|
||||
}
|
||||
|
||||
struct NoEmbeddedFrames;
|
||||
|
||||
impl<N> EmbeddedFrameRenderer<N> for NoEmbeddedFrames {
|
||||
impl<N> EmbeddedFrameRenderer<N> for NoEmbeddedFrames
|
||||
where
|
||||
N: Copy + std::fmt::Debug + Eq + std::hash::Hash,
|
||||
{
|
||||
fn render_embedded_frame(
|
||||
&mut self,
|
||||
_frame: N,
|
||||
_viewport: LayoutViewport,
|
||||
) -> Result<Option<PaintSnapshot>, LayoutError> {
|
||||
) -> Result<Option<EmbeddedFrameSnapshot<N>>, LayoutError> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -79,6 +106,13 @@ impl LayoutPassRequest {
|
||||
self.paint_capture.is_some()
|
||||
}
|
||||
|
||||
/// Whether this demand needs a complete embedded-frame input projection.
|
||||
/// Paint always composes child frames, while coordinate input needs their
|
||||
/// source-bearing geometry without retaining independent child snapshots.
|
||||
pub const fn requests_embedded_frames(self) -> bool {
|
||||
self.requests_paint() || matches!(self.reason, LayoutFlushReason::HitTest)
|
||||
}
|
||||
|
||||
/// Whether paint snapshots for this demand should include CSS backgrounds.
|
||||
/// Layout-only demands return `true` so recursive renderers retain their
|
||||
/// normal paint defaults when no capture policy exists.
|
||||
@@ -152,9 +186,9 @@ where
|
||||
/// Builds one complete layout result and resolves embedded frame pixels after
|
||||
/// the parent numeric layout has established their exact content viewports.
|
||||
///
|
||||
/// This is a one-shot composition seam, not a retained subframe tree. Child
|
||||
/// snapshots are consumed into the parent snapshot before all layout worlds
|
||||
/// are dropped.
|
||||
/// This is a one-shot composition seam, not a per-Document cache. Child trees
|
||||
/// are recursively owned by the one parent snapshot, child paint is consumed
|
||||
/// into parent paint, and every working layout world is then dropped.
|
||||
pub fn build_layout_pass_with_embedded_frames<S, R, F>(
|
||||
source: &S,
|
||||
styles: &mut R,
|
||||
@@ -174,7 +208,7 @@ where
|
||||
prepare_inline_contexts(&mut world, services);
|
||||
compute_world_layout_with_scrollbars(&mut world, request.viewport);
|
||||
let mut embedded_frames = HashMap::new();
|
||||
if request.requests_paint() {
|
||||
if request.requests_embedded_frames() {
|
||||
for index in 0..world.boxes.len() {
|
||||
let layout_box = &world.boxes[index];
|
||||
if !layout_box.element_semantics().is_some_and(|semantics| {
|
||||
@@ -207,17 +241,19 @@ where
|
||||
request.viewport.device_pixel_ratio,
|
||||
);
|
||||
if let Some(snapshot) = frames.render_embedded_frame(source, viewport)? {
|
||||
embedded_frames.insert(crate::LayoutBoxId::from_index(index), snapshot);
|
||||
embedded_frames.insert(crate::LayoutBoxId::from_index(index), (source, snapshot));
|
||||
}
|
||||
}
|
||||
}
|
||||
finish_layout_pass(
|
||||
&world,
|
||||
source.root(),
|
||||
request.viewport,
|
||||
request.reason,
|
||||
started,
|
||||
request.paint_capture,
|
||||
&mut embedded_frames,
|
||||
embedded_frames,
|
||||
request.requests_embedded_frames(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,13 @@ use crate::{
|
||||
|
||||
pub(crate) fn finish_layout_pass<N>(
|
||||
world: &LayoutWorld<N>,
|
||||
source_root: N,
|
||||
viewport: LayoutViewport,
|
||||
reason: LayoutFlushReason,
|
||||
started: Instant,
|
||||
paint_capture: Option<PaintCaptureRequest>,
|
||||
embedded_frames: &mut HashMap<LayoutBoxId, crate::PaintSnapshot>,
|
||||
mut embedded_frames: HashMap<LayoutBoxId, (N, crate::EmbeddedFrameSnapshot<N>)>,
|
||||
embedded_frames_complete: bool,
|
||||
) -> Result<LayoutPassResult<N>, LayoutError>
|
||||
where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
@@ -34,12 +36,16 @@ where
|
||||
projection.assign_clip_and_paint_order();
|
||||
|
||||
let content_size = projection.document_content_size();
|
||||
let mut embedded_paint = embedded_frames
|
||||
.iter_mut()
|
||||
.filter_map(|(id, (_, snapshot))| snapshot.paint.take().map(|paint| (*id, paint)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let mut paint_snapshot = paint_capture
|
||||
.map(|request| {
|
||||
request
|
||||
.resolve(viewport, projection.viewport_scroll, content_size)
|
||||
.map(|capture| {
|
||||
crate::paint::project_paint_snapshot(&projection, capture, embedded_frames)
|
||||
crate::paint::project_paint_snapshot(&projection, capture, &mut embedded_paint)
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
@@ -73,7 +79,19 @@ where
|
||||
.filter(|diagnostic| diagnostic.severity == PaintDiagnosticSeverity::Warning)
|
||||
.count(),
|
||||
};
|
||||
let tree = projection.into_frozen_tree(content_size);
|
||||
let embedded_frames = embedded_frames
|
||||
.into_values()
|
||||
.map(|(source, snapshot)| crate::FrozenEmbeddedFrame {
|
||||
source,
|
||||
tree: Box::new(snapshot.tree),
|
||||
})
|
||||
.collect();
|
||||
let tree = projection.into_frozen_tree(
|
||||
source_root,
|
||||
content_size,
|
||||
embedded_frames,
|
||||
embedded_frames_complete,
|
||||
);
|
||||
Ok(LayoutPassResult::new(
|
||||
tree,
|
||||
diagnostics,
|
||||
@@ -907,7 +925,13 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn into_frozen_tree(self, content_size: LayoutSize) -> FrozenLayoutTree<N> {
|
||||
fn into_frozen_tree(
|
||||
self,
|
||||
source_root: N,
|
||||
content_size: LayoutSize,
|
||||
embedded_frames: Vec<crate::FrozenEmbeddedFrame<N>>,
|
||||
embedded_frames_complete: bool,
|
||||
) -> FrozenLayoutTree<N> {
|
||||
let root_box = LayoutOutputBoxId::from_index(self.world.root.index());
|
||||
let mut coordinate_spaces = self
|
||||
.coordinate_spaces
|
||||
@@ -941,6 +965,7 @@ where
|
||||
)
|
||||
.collect();
|
||||
FrozenLayoutTree::new(
|
||||
source_root,
|
||||
self.viewport,
|
||||
self.viewport_scroll,
|
||||
content_size,
|
||||
@@ -950,6 +975,8 @@ where
|
||||
self.scroll_proxy_links,
|
||||
viewport_coordinate_space,
|
||||
self.clip_chain,
|
||||
embedded_frames,
|
||||
embedded_frames_complete,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::HashMap;
|
||||
|
||||
use moli_layout::{
|
||||
DocumentLayoutServices, EmbeddedFrameRenderer, LayoutPassRequest, LayoutPassResult,
|
||||
LayoutSource, LayoutViewport, PaintSnapshot, build_layout_pass_with_embedded_frames,
|
||||
LayoutSource, LayoutViewport, build_layout_pass_with_embedded_frames,
|
||||
};
|
||||
use style::values::generics::image::GenericImage;
|
||||
|
||||
@@ -148,6 +148,7 @@ fn build_native_layout_pass_recursive(
|
||||
let mut frames = NativeEmbeddedFrameRenderer {
|
||||
runtime,
|
||||
reason: request.reason,
|
||||
capture_paint: request.requests_paint(),
|
||||
include_backgrounds: request.includes_backgrounds(),
|
||||
document_stack,
|
||||
embedded_document_services,
|
||||
@@ -161,6 +162,7 @@ fn build_native_layout_pass_recursive(
|
||||
struct NativeEmbeddedFrameRenderer<'a> {
|
||||
runtime: &'a JsContextHost,
|
||||
reason: moli_layout::LayoutFlushReason,
|
||||
capture_paint: bool,
|
||||
include_backgrounds: bool,
|
||||
document_stack: &'a mut Vec<DomHandle>,
|
||||
embedded_document_services: &'a mut HashMap<DomHandle, DocumentLayoutServices>,
|
||||
@@ -171,7 +173,8 @@ impl EmbeddedFrameRenderer<DomHandle> for NativeEmbeddedFrameRenderer<'_> {
|
||||
&mut self,
|
||||
frame: DomHandle,
|
||||
viewport: LayoutViewport,
|
||||
) -> Result<Option<PaintSnapshot>, moli_layout::LayoutError> {
|
||||
) -> Result<Option<moli_layout::EmbeddedFrameSnapshot<DomHandle>>, moli_layout::LayoutError>
|
||||
{
|
||||
const MAX_EMBEDDED_DOCUMENT_DEPTH: usize = 32;
|
||||
|
||||
let Some(document) = self.runtime.child_browsing_context_document_handle(frame) else {
|
||||
@@ -194,19 +197,25 @@ impl EmbeddedFrameRenderer<DomHandle> for NativeEmbeddedFrameRenderer<'_> {
|
||||
.embedded_document_services
|
||||
.remove(&document)
|
||||
.unwrap_or_default();
|
||||
let mut capture = moli_layout::PaintCaptureRequest::viewport();
|
||||
capture.include_backgrounds = self.include_backgrounds;
|
||||
let request = if self.capture_paint {
|
||||
let mut capture = moli_layout::PaintCaptureRequest::viewport();
|
||||
capture.include_backgrounds = self.include_backgrounds;
|
||||
LayoutPassRequest::with_capture(viewport, self.reason, capture)
|
||||
} else {
|
||||
LayoutPassRequest::new(viewport, self.reason)
|
||||
};
|
||||
let result = build_native_layout_pass_recursive(
|
||||
self.runtime,
|
||||
root,
|
||||
&mut services,
|
||||
self.embedded_document_services,
|
||||
LayoutPassRequest::with_capture(viewport, self.reason, capture),
|
||||
request,
|
||||
self.document_stack,
|
||||
);
|
||||
self.embedded_document_services.insert(document, services);
|
||||
let output = result?;
|
||||
output.into_paint_snapshot().map(Some)
|
||||
let (tree, paint) = output.into_tree_and_paint_snapshot();
|
||||
Ok(Some(moli_layout::EmbeddedFrameSnapshot::new(tree, paint)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -196,13 +196,12 @@ impl JsContextHost {
|
||||
if self.force_fresh_layout_reads_for_test {
|
||||
return false;
|
||||
}
|
||||
self.document_layout_state
|
||||
.borrow()
|
||||
.latest_layout(document)
|
||||
self.with_latest_layout_tree_for_document(document, |_| ())
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// Inspects the latest frozen layout tree for one exact Document.
|
||||
/// Inspects the member tree for one exact Document in the single latest
|
||||
/// recursively frozen snapshot.
|
||||
///
|
||||
/// The callback cannot retain the tree or force a refresh. Consumers
|
||||
/// such as lazy-image admission may combine this sampled geometry with
|
||||
@@ -213,8 +212,15 @@ impl JsContextHost {
|
||||
document: DomHandle,
|
||||
inspect: impl FnOnce(&FrozenLayoutTree<DomHandle>) -> T,
|
||||
) -> Option<T> {
|
||||
let root = self
|
||||
.dom_host()
|
||||
.dom()
|
||||
.document_element_handle_for_document(document);
|
||||
let state = self.document_layout_state.borrow();
|
||||
state.latest_layout(document).map(inspect)
|
||||
state
|
||||
.latest_layout(document)
|
||||
.or_else(|| root.and_then(|root| state.latest_layout_for_root(root)))
|
||||
.map(inspect)
|
||||
}
|
||||
|
||||
pub(crate) fn answer_layout_at_viewport(
|
||||
@@ -227,6 +233,41 @@ impl JsContextHost {
|
||||
self.answer_layout(document, reason, viewport, queries, true)
|
||||
}
|
||||
|
||||
/// Ensures one exact-viewport tree exists without manufacturing a geometry
|
||||
/// query. Hit-test demands additionally require the complete embedded-frame
|
||||
/// projection owned by that same frozen tree.
|
||||
pub(crate) fn ensure_layout_at_viewport(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
reason: LayoutFlushReason,
|
||||
viewport: LayoutViewport,
|
||||
) -> Result<(), LayoutError> {
|
||||
#[cfg(test)]
|
||||
let reuse_latest = !self.force_fresh_layout_reads_for_test;
|
||||
#[cfg(not(test))]
|
||||
let reuse_latest = true;
|
||||
let cached = reuse_latest
|
||||
&& self
|
||||
.with_latest_layout_tree_for_document(document, |tree| {
|
||||
layout_tree_satisfies_request(tree, reason, viewport, true)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if cached {
|
||||
self.layout_snapshot_cache_hits
|
||||
.set(self.layout_snapshot_cache_hits.get().saturating_add(1));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.layout_snapshot_cache_misses
|
||||
.set(self.layout_snapshot_cache_misses.get().saturating_add(1));
|
||||
self.with_fresh_layout_pass_for_document(
|
||||
document,
|
||||
LayoutPassRequest::new(viewport, reason),
|
||||
|_| Ok(()),
|
||||
)?
|
||||
.ok_or(LayoutError::NoLayoutRoot)
|
||||
}
|
||||
|
||||
fn answer_layout(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
@@ -240,15 +281,15 @@ impl JsContextHost {
|
||||
#[cfg(not(test))]
|
||||
let reuse_latest = true;
|
||||
let cached = if reuse_latest {
|
||||
let state = self.document_layout_state.borrow();
|
||||
state
|
||||
.latest_layout(document)
|
||||
.filter(|tree| !exact_viewport || tree.viewport == viewport)
|
||||
.and_then(|tree| {
|
||||
self.last_layout_pass_metrics
|
||||
.get()
|
||||
.map(|metrics| self.answer_layout_queries(tree, metrics, viewport, queries))
|
||||
})
|
||||
self.with_latest_layout_tree_for_document(document, |tree| {
|
||||
if !layout_tree_satisfies_request(tree, reason, viewport, exact_viewport) {
|
||||
return None;
|
||||
}
|
||||
self.last_layout_pass_metrics
|
||||
.get()
|
||||
.map(|metrics| self.answer_layout_queries(tree, metrics, viewport, queries))
|
||||
})
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -424,6 +465,16 @@ impl JsContextHost {
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_tree_satisfies_request(
|
||||
tree: &FrozenLayoutTree<DomHandle>,
|
||||
reason: LayoutFlushReason,
|
||||
viewport: LayoutViewport,
|
||||
exact_viewport: bool,
|
||||
) -> bool {
|
||||
(!exact_viewport || tree.viewport == viewport)
|
||||
&& (!matches!(reason, LayoutFlushReason::HitTest) || tree.embedded_frames_complete())
|
||||
}
|
||||
|
||||
impl GeometryProvider for JsContextHost {
|
||||
type NodeId = DomHandle;
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ struct LatestFrozenLayout {
|
||||
|
||||
/// Single-slot storage for the latest successful frozen layout tree.
|
||||
///
|
||||
/// This owns exactly one frozen layout tree. It has no working layout world,
|
||||
/// source index, hit-test index, Taffy cache, style borrow, pass diagnostics,
|
||||
/// paint snapshot, timer, freshness stamp, or invalidation policy.
|
||||
/// This owns exactly one recursively frozen snapshot. It has no separately
|
||||
/// keyed child tree, working layout world, source index, hit-test index, Taffy
|
||||
/// cache, style borrow, pass diagnostics, paint snapshot, timer, freshness
|
||||
/// stamp, or invalidation policy.
|
||||
#[derive(Default)]
|
||||
pub(super) struct LatestLayoutTreeCache {
|
||||
latest: Option<LatestFrozenLayout>,
|
||||
@@ -25,6 +26,10 @@ impl LatestLayoutTreeCache {
|
||||
.map(|snapshot| snapshot.tree.as_ref())
|
||||
}
|
||||
|
||||
pub(super) fn get_for_root(&self, root: DomHandle) -> Option<&FrozenLayoutTree<DomHandle>> {
|
||||
self.latest.as_ref()?.tree.tree_for_root(root)
|
||||
}
|
||||
|
||||
pub(super) fn publish(&mut self, document: DomHandle, tree: FrozenLayoutTree<DomHandle>) {
|
||||
self.latest = Some(LatestFrozenLayout {
|
||||
document,
|
||||
|
||||
@@ -17,8 +17,9 @@ use crate::{
|
||||
/// layout tree and document-owned font/text sidecars survive a pass.
|
||||
/// Embedded documents receive separate Parley services so a main-document
|
||||
/// `@font-face` registration cannot leak across the browsing-context boundary.
|
||||
/// The single snapshot slot may describe any exact Document in the current
|
||||
/// document tree; its identity is stored alongside the tree.
|
||||
/// The single snapshot slot records the exact root Document it was built for;
|
||||
/// embedded Document member trees remain recursively owned by that same
|
||||
/// snapshot instead of becoming separately keyed cache entries.
|
||||
pub(super) struct DocumentLayoutState {
|
||||
services: DocumentLayoutServices,
|
||||
embedded_document_services: HashMap<DomHandle, DocumentLayoutServices>,
|
||||
@@ -27,8 +28,8 @@ pub(super) struct DocumentLayoutState {
|
||||
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.
|
||||
/// separate from the single latest-tree slot because a later fresh layout
|
||||
/// may replace that slot while the frame view remains live.
|
||||
frame_viewports: HashMap<DomHandle, LayoutViewport>,
|
||||
}
|
||||
|
||||
@@ -94,6 +95,13 @@ impl DocumentLayoutState {
|
||||
self.latest_layout.get(document)
|
||||
}
|
||||
|
||||
pub(super) fn latest_layout_for_root(
|
||||
&self,
|
||||
root: DomHandle,
|
||||
) -> Option<&FrozenLayoutTree<DomHandle>> {
|
||||
self.latest_layout.get_for_root(root)
|
||||
}
|
||||
|
||||
pub(super) fn publish_latest_layout(
|
||||
&mut self,
|
||||
document: DomHandle,
|
||||
|
||||
@@ -385,11 +385,13 @@ pub(crate) fn install_html_select_element_prototype_bindings(
|
||||
}
|
||||
|
||||
pub use geometry::ClientRect;
|
||||
#[cfg(test)]
|
||||
pub(crate) use geometry::observable_scrollbar_hit_test;
|
||||
pub(crate) use geometry::{
|
||||
apply_scroll_observable_effects, observable_bounding_client_rect, observable_caret_position,
|
||||
observable_deep_hit_test, observable_event_offset, observable_geometry_batch,
|
||||
observable_hit_test_all, observable_input_hit_test, observable_scroll_adjusted_client_rect,
|
||||
observable_scrollbar_hit_test, observable_sources_with_fragments,
|
||||
observable_hit_test_all, observable_input_hit_test, observable_input_surface_hit_test,
|
||||
observable_scroll_adjusted_client_rect, observable_sources_with_fragments,
|
||||
perform_scrollbar_scroll_default_action, perform_wheel_scroll_default_action,
|
||||
queue_scroll_observable_effects, scroll_node_into_view_at_start,
|
||||
};
|
||||
|
||||
@@ -8,8 +8,10 @@ mod scroll;
|
||||
mod scroll_into_view;
|
||||
|
||||
pub use client_rect::ClientRect;
|
||||
#[cfg(test)]
|
||||
pub(crate) use hit_test::observable_scrollbar_hit_test;
|
||||
pub(crate) use hit_test::{
|
||||
observable_deep_hit_test, observable_input_hit_test, observable_scrollbar_hit_test,
|
||||
observable_deep_hit_test, observable_input_hit_test, observable_input_surface_hit_test,
|
||||
};
|
||||
pub(in crate::native_bridge) use metrics::{
|
||||
node_client_height_getter_function, node_client_left_getter_function,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use moli_layout::{
|
||||
LayoutError, LayoutFlushReason, LayoutHit, LayoutPoint, LayoutQuery, LayoutQueryAnswer,
|
||||
LayoutQueryBatch, LayoutScrollbarHit, LayoutTransform2D, LayoutViewport,
|
||||
FrozenLayoutTree, LayoutError, LayoutFlushReason, LayoutHit, LayoutPoint, LayoutQuery,
|
||||
LayoutQueryAnswer, LayoutQueryBatch, LayoutScrollbarHit, LayoutTransform2D, LayoutViewport,
|
||||
};
|
||||
|
||||
use super::provider::{
|
||||
@@ -19,6 +19,12 @@ pub(crate) struct InputHit {
|
||||
pub(crate) root_to_frame: LayoutTransform2D,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub(crate) struct InputSurfaceHit {
|
||||
pub(crate) input: Option<InputHit>,
|
||||
pub(crate) scrollbar: Option<LayoutScrollbarHit<DomHandle>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FrameHitTest {
|
||||
document: DomHandle,
|
||||
@@ -70,18 +76,49 @@ pub(crate) fn observable_input_hit_test(
|
||||
document: DomHandle,
|
||||
point: LayoutPoint,
|
||||
) -> Result<Option<InputHit>, LayoutError> {
|
||||
input_hit_test(runtime, document, point, false)
|
||||
observable_input_surface_hit_test(runtime, document, point, false, false).map(|hit| hit.input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn observable_scrollbar_hit_test(
|
||||
runtime: &JsContextHost,
|
||||
document: DomHandle,
|
||||
point: LayoutPoint,
|
||||
) -> Result<Option<LayoutScrollbarHit<DomHandle>>, LayoutError> {
|
||||
observable_input_surface_hit_test(runtime, document, point, false, true)
|
||||
.map(|hit| hit.scrollbar)
|
||||
}
|
||||
|
||||
pub(crate) fn observable_input_surface_hit_test(
|
||||
runtime: &JsContextHost,
|
||||
document: DomHandle,
|
||||
point: LayoutPoint,
|
||||
ignore_pointer_events_none: bool,
|
||||
include_scrollbars: bool,
|
||||
) -> Result<InputSurfaceHit, LayoutError> {
|
||||
if !runtime.layout_policy().uses_real_layout() {
|
||||
return Ok(None);
|
||||
return input_hit_test_via_documents(runtime, document, point, ignore_pointer_events_none)
|
||||
.map(|input| InputSurfaceHit {
|
||||
input,
|
||||
scrollbar: None,
|
||||
});
|
||||
}
|
||||
scrollbar_hit_test_in_frame(runtime, FrameHitTest::root(runtime, document, point), 0)
|
||||
|
||||
let viewport = runtime.layout_viewport_for_document(document);
|
||||
runtime.ensure_layout_at_viewport(document, LayoutFlushReason::HitTest, viewport)?;
|
||||
runtime
|
||||
.with_latest_layout_tree_for_document(document, |tree| {
|
||||
input_surface_hit_test_in_tree(
|
||||
runtime,
|
||||
tree,
|
||||
point,
|
||||
LayoutTransform2D::IDENTITY,
|
||||
ignore_pointer_events_none,
|
||||
include_scrollbars,
|
||||
0,
|
||||
)
|
||||
})
|
||||
.ok_or(LayoutError::NoLayoutRoot)
|
||||
}
|
||||
|
||||
pub(crate) fn observable_deep_hit_test(
|
||||
@@ -90,10 +127,18 @@ pub(crate) fn observable_deep_hit_test(
|
||||
point: LayoutPoint,
|
||||
ignore_pointer_events_none: bool,
|
||||
) -> Result<Option<DomHandle>, LayoutError> {
|
||||
Ok(input_hit_test(runtime, document, point, ignore_pointer_events_none)?.map(|hit| hit.handle))
|
||||
Ok(observable_input_surface_hit_test(
|
||||
runtime,
|
||||
document,
|
||||
point,
|
||||
ignore_pointer_events_none,
|
||||
false,
|
||||
)?
|
||||
.input
|
||||
.map(|hit| hit.handle))
|
||||
}
|
||||
|
||||
fn input_hit_test(
|
||||
fn input_hit_test_via_documents(
|
||||
runtime: &JsContextHost,
|
||||
document: DomHandle,
|
||||
point: LayoutPoint,
|
||||
@@ -133,35 +178,112 @@ fn input_hit_test_in_frame(
|
||||
)
|
||||
}
|
||||
|
||||
fn scrollbar_hit_test_in_frame(
|
||||
fn input_surface_hit_test_in_tree(
|
||||
runtime: &JsContextHost,
|
||||
frame: FrameHitTest,
|
||||
tree: &FrozenLayoutTree<DomHandle>,
|
||||
point: LayoutPoint,
|
||||
root_to_frame: LayoutTransform2D,
|
||||
ignore_pointer_events_none: bool,
|
||||
include_scrollbars: bool,
|
||||
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 = live_hit_in_frame(runtime, frame, false)?;
|
||||
if let Some(mut hit) = runtime
|
||||
.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(frame.root_to_frame);
|
||||
return Ok(Some(hit));
|
||||
) -> InputSurfaceHit {
|
||||
if include_scrollbars && let Some(mut scrollbar) = tree.scrollbar_hit_test(point) {
|
||||
scrollbar.viewport_to_local = scrollbar.viewport_to_local.concatenate(root_to_frame);
|
||||
return InputSurfaceHit {
|
||||
input: None,
|
||||
scrollbar: Some(scrollbar),
|
||||
};
|
||||
}
|
||||
|
||||
let Some((layout_hit, target)) =
|
||||
live_hit_in_tree(runtime, tree, point, ignore_pointer_events_none)
|
||||
else {
|
||||
return InputSurfaceHit::default();
|
||||
};
|
||||
let target_hit = InputHit {
|
||||
handle: target,
|
||||
root_to_frame,
|
||||
};
|
||||
if depth >= CHILD_FRAME_DEPTH_LIMIT {
|
||||
return Ok(None);
|
||||
return InputSurfaceHit {
|
||||
input: Some(target_hit),
|
||||
scrollbar: None,
|
||||
};
|
||||
}
|
||||
let Some((layout_hit, target)) = live_hit else {
|
||||
return Ok(None);
|
||||
let Some(child_tree) = tree.embedded_frame_tree(target) else {
|
||||
return InputSurfaceHit {
|
||||
input: Some(target_hit),
|
||||
scrollbar: None,
|
||||
};
|
||||
};
|
||||
let Some(child) = frame.child(runtime, target, layout_hit) else {
|
||||
return Ok(None);
|
||||
if runtime
|
||||
.child_browsing_context_document_handle(target)
|
||||
.is_none()
|
||||
{
|
||||
return InputSurfaceHit {
|
||||
input: Some(target_hit),
|
||||
scrollbar: None,
|
||||
};
|
||||
}
|
||||
let Some(content_box) = layout_hit.local_content_box else {
|
||||
return InputSurfaceHit {
|
||||
input: Some(target_hit),
|
||||
scrollbar: None,
|
||||
};
|
||||
};
|
||||
scrollbar_hit_test_in_frame(runtime, child, depth + 1)
|
||||
if !content_box.contains(layout_hit.local_point) {
|
||||
return InputSurfaceHit {
|
||||
input: Some(target_hit),
|
||||
scrollbar: None,
|
||||
};
|
||||
}
|
||||
let frame_to_child = LayoutTransform2D::translation(-content_box.x, -content_box.y)
|
||||
.concatenate(layout_hit.viewport_to_local);
|
||||
let child_hit = input_surface_hit_test_in_tree(
|
||||
runtime,
|
||||
child_tree,
|
||||
frame_to_child.map_point(point),
|
||||
frame_to_child.concatenate(root_to_frame),
|
||||
ignore_pointer_events_none,
|
||||
include_scrollbars,
|
||||
depth + 1,
|
||||
);
|
||||
if child_hit.input.is_some() || child_hit.scrollbar.is_some() {
|
||||
child_hit
|
||||
} else {
|
||||
InputSurfaceHit {
|
||||
input: Some(target_hit),
|
||||
scrollbar: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn live_hit_in_tree(
|
||||
runtime: &JsContextHost,
|
||||
tree: &FrozenLayoutTree<DomHandle>,
|
||||
point: LayoutPoint,
|
||||
ignore_pointer_events_none: bool,
|
||||
) -> Option<(LayoutHit<DomHandle>, DomHandle)> {
|
||||
let first_hit = tree.hit_test(point, ignore_pointer_events_none);
|
||||
let live_first_hit = first_hit.and_then(|hit| {
|
||||
let target = element_for_hit_source(runtime, hit.source)?;
|
||||
runtime
|
||||
.dom_host()
|
||||
.is_connected(target)
|
||||
.then_some((hit, target))
|
||||
});
|
||||
if live_first_hit.is_some() || first_hit.is_none() {
|
||||
return live_first_hit;
|
||||
}
|
||||
tree.hit_test_all(point, ignore_pointer_events_none)
|
||||
.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 live_hit_in_frame(
|
||||
|
||||
@@ -22,10 +22,11 @@ use crate::native_bridge::element::{
|
||||
construct_pointer_event_with_related_target_and_modifiers, construct_simple_event,
|
||||
construct_touch_event, construct_touch_event_with_points, construct_wheel_event,
|
||||
contenteditable_editing_host, dispatch_public_event, observable_input_hit_test,
|
||||
observable_scrollbar_hit_test, perform_drop_default_action, perform_mouse_focus_default_action,
|
||||
perform_scrollbar_scroll_default_action, perform_wheel_scroll_default_action,
|
||||
replace_contenteditable_selection, replace_text_control_selection,
|
||||
select_contenteditable_contents, text_control_set_selection_range_internal,
|
||||
observable_input_surface_hit_test, perform_drop_default_action,
|
||||
perform_mouse_focus_default_action, perform_scrollbar_scroll_default_action,
|
||||
perform_wheel_scroll_default_action, replace_contenteditable_selection,
|
||||
replace_text_control_selection, select_contenteditable_contents,
|
||||
text_control_set_selection_range_internal,
|
||||
text_control_set_selection_range_with_direction_internal, text_control_value, update_focus,
|
||||
};
|
||||
use crate::runtime::{
|
||||
@@ -475,17 +476,23 @@ impl ScriptVm {
|
||||
let buttons = buttons.unwrap_or(self.pressed_mouse_buttons);
|
||||
|
||||
if let Some(outcome) =
|
||||
self.dispatch_native_scrollbar_mouse_event(x, y, event_name, button, buttons)?
|
||||
self.dispatch_active_scrollbar_mouse_event(x, y, event_name, button, buttons)?
|
||||
{
|
||||
return Ok(outcome);
|
||||
}
|
||||
|
||||
let root_point = moli_layout::LayoutPoint::new(x as f32, y as f32);
|
||||
let hit = observable_input_hit_test(
|
||||
let surface_hit = observable_input_surface_hit_test(
|
||||
&self._context_host.borrow(),
|
||||
self.document_runtime.document_handle(),
|
||||
root_point,
|
||||
false,
|
||||
matches!(event_name, "mousedown" | "mouseup" | "mousemove"),
|
||||
)?;
|
||||
if let Some(scrollbar) = surface_hit.scrollbar {
|
||||
return self.dispatch_native_scrollbar_hit(scrollbar, event_name, button);
|
||||
}
|
||||
let hit = surface_hit.input;
|
||||
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();
|
||||
@@ -973,7 +980,7 @@ impl ScriptVm {
|
||||
result
|
||||
}
|
||||
|
||||
fn dispatch_native_scrollbar_mouse_event(
|
||||
fn dispatch_active_scrollbar_mouse_event(
|
||||
&mut self,
|
||||
x: f64,
|
||||
y: f64,
|
||||
@@ -981,54 +988,51 @@ impl ScriptVm {
|
||||
button: i32,
|
||||
buttons: i32,
|
||||
) -> Result<Option<RendererInputDispatchOutcome>> {
|
||||
if let Some(drag) = self.active_scrollbar_drag {
|
||||
match event_name {
|
||||
"mousemove" if buttons & 1 != 0 => {
|
||||
let local = drag
|
||||
.viewport_to_local
|
||||
.map_point(moli_layout::LayoutPoint::new(x as f32, y as f32));
|
||||
let coordinate = match drag.scrollbar.axis {
|
||||
moli_layout::LayoutScrollbarAxis::Horizontal => local.x,
|
||||
moli_layout::LayoutScrollbarAxis::Vertical => local.y,
|
||||
};
|
||||
let target = (drag.scrollbar.current_offset
|
||||
+ (coordinate - drag.pointer_origin) * drag.scrollbar.drag_ratio())
|
||||
.clamp(drag.scrollbar.minimum_offset, drag.scrollbar.maximum_offset);
|
||||
self.with_default_context_scope(|scope, runtime_ptr| {
|
||||
perform_scrollbar_scroll_default_action(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
drag.source,
|
||||
drag.scrollbar.axis,
|
||||
f64::from(target),
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
return Ok(Some(input_dispatch_outcome(true)));
|
||||
}
|
||||
"mouseup" if button == 0 => {
|
||||
self.active_scrollbar_drag = None;
|
||||
return Ok(Some(input_dispatch_outcome(true)));
|
||||
}
|
||||
"mousemove" => {
|
||||
self.active_scrollbar_drag = None;
|
||||
}
|
||||
_ => return Ok(Some(input_dispatch_outcome(true))),
|
||||
}
|
||||
}
|
||||
|
||||
if !matches!(event_name, "mousedown" | "mouseup" | "mousemove") {
|
||||
return Ok(None);
|
||||
}
|
||||
let document = self.document_runtime.document_handle();
|
||||
let Some(hit) = observable_scrollbar_hit_test(
|
||||
&self._context_host.borrow(),
|
||||
document,
|
||||
moli_layout::LayoutPoint::new(x as f32, y as f32),
|
||||
)?
|
||||
else {
|
||||
let Some(drag) = self.active_scrollbar_drag else {
|
||||
return Ok(None);
|
||||
};
|
||||
match event_name {
|
||||
"mousemove" if buttons & 1 != 0 => {
|
||||
let local = drag
|
||||
.viewport_to_local
|
||||
.map_point(moli_layout::LayoutPoint::new(x as f32, y as f32));
|
||||
let coordinate = match drag.scrollbar.axis {
|
||||
moli_layout::LayoutScrollbarAxis::Horizontal => local.x,
|
||||
moli_layout::LayoutScrollbarAxis::Vertical => local.y,
|
||||
};
|
||||
let target = (drag.scrollbar.current_offset
|
||||
+ (coordinate - drag.pointer_origin) * drag.scrollbar.drag_ratio())
|
||||
.clamp(drag.scrollbar.minimum_offset, drag.scrollbar.maximum_offset);
|
||||
self.with_default_context_scope(|scope, runtime_ptr| {
|
||||
perform_scrollbar_scroll_default_action(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
drag.source,
|
||||
drag.scrollbar.axis,
|
||||
f64::from(target),
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Some(input_dispatch_outcome(true)))
|
||||
}
|
||||
"mouseup" if button == 0 => {
|
||||
self.active_scrollbar_drag = None;
|
||||
Ok(Some(input_dispatch_outcome(true)))
|
||||
}
|
||||
"mousemove" => {
|
||||
self.active_scrollbar_drag = None;
|
||||
Ok(None)
|
||||
}
|
||||
_ => Ok(Some(input_dispatch_outcome(true))),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_native_scrollbar_hit(
|
||||
&mut self,
|
||||
hit: moli_layout::LayoutScrollbarHit<DomHandle>,
|
||||
event_name: &str,
|
||||
button: i32,
|
||||
) -> Result<RendererInputDispatchOutcome> {
|
||||
if event_name == "mousedown" && button == 0 {
|
||||
match hit.part {
|
||||
moli_layout::LayoutScrollbarPart::Thumb => {
|
||||
@@ -1068,7 +1072,7 @@ impl ScriptVm {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(input_dispatch_outcome(true)))
|
||||
Ok(input_dispatch_outcome(true))
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_touch_event_at_point(
|
||||
|
||||
@@ -587,6 +587,167 @@ fn transformed_constrained_iframe_routes_hover_click_and_wheel_in_child_coordina
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iframe_input_reuses_one_top_level_snapshot_without_parent_child_ping_pong() {
|
||||
let mut vm = new_storage_test_vm("https://iframe-input-snapshot.test/");
|
||||
vm.set_viewport_surface(Some(crate::protocol_types::ViewportSurface {
|
||||
inner_width: 800,
|
||||
inner_height: 600,
|
||||
outer_width: 800,
|
||||
outer_height: 600,
|
||||
device_pixel_ratio: 1.0,
|
||||
screen_width: 800,
|
||||
screen_height: 600,
|
||||
screen_avail_width: 800,
|
||||
screen_avail_height: 600,
|
||||
}))
|
||||
.expect("iframe input snapshot viewport should update");
|
||||
vm.eval(
|
||||
r#"
|
||||
if (!document.documentElement) {
|
||||
document.appendChild(document.createElement('html'));
|
||||
}
|
||||
if (!document.body) {
|
||||
document.documentElement.appendChild(document.createElement('body'));
|
||||
}
|
||||
document.documentElement.style.cssText = 'margin:0;padding:0';
|
||||
document.body.style.cssText = 'margin:0;padding:0';
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = 'snapshot-frame';
|
||||
frame.style.cssText = 'position:absolute;left:100px;top:80px;width:240px;height:180px;border:0;padding:0';
|
||||
document.body.appendChild(frame);
|
||||
const child = frame.contentDocument;
|
||||
child.documentElement.style.cssText = 'margin:0;padding:0';
|
||||
child.body.style.cssText = 'margin:0;padding:0';
|
||||
child.body.innerHTML = '<button id="snapshot-target" style="position:fixed;left:20px;top:20px;width:80px;height:60px">target</button>';
|
||||
'installed'
|
||||
"#,
|
||||
)
|
||||
.expect("iframe input snapshot fixture should initialize");
|
||||
|
||||
assert_eq!(
|
||||
vm.eval("document.body.offsetWidth > 0")
|
||||
.expect("parent geometry should evaluate"),
|
||||
"true"
|
||||
);
|
||||
let passes_before = vm.layout_pass_observability_for_test().1;
|
||||
vm.dispatch_mouse_event_at_point(140.0, 130.0, "mousemove", -1, Some(0), 0.0, 0.0)
|
||||
.expect("cold child hover should dispatch");
|
||||
let passes_after_cold = vm.layout_pass_observability_for_test().1;
|
||||
assert_eq!(
|
||||
passes_after_cold,
|
||||
passes_before + 1,
|
||||
"input must upgrade a geometry-only tree through one shared recursive layout demand"
|
||||
);
|
||||
|
||||
// The first hover transition invalidates its sampled tree because :hover
|
||||
// may affect geometry. Rebuilding for the same target publishes one complete
|
||||
// top-level snapshot; a third move can then reuse it without a child publish
|
||||
// evicting the parent.
|
||||
vm.dispatch_mouse_event_at_point(140.0, 130.0, "mousemove", -1, Some(0), 0.0, 0.0)
|
||||
.expect("stable child hover should rebuild once");
|
||||
let passes_after_rebuild = vm.layout_pass_observability_for_test().1;
|
||||
assert_eq!(passes_after_rebuild, passes_after_cold + 1);
|
||||
vm.dispatch_mouse_event_at_point(140.0, 130.0, "mousemove", -1, Some(0), 0.0, 0.0)
|
||||
.expect("stable child hover should reuse the top-level snapshot");
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
passes_after_rebuild,
|
||||
"an embedded hit must leave the top-level composite snapshot reusable"
|
||||
);
|
||||
|
||||
let cached = vm
|
||||
.layout_snapshot_cache_observability_for_test()
|
||||
.3
|
||||
.expect("stable child input should retain one snapshot");
|
||||
assert_eq!(cached.0, vm.document_runtime.document_handle());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iframe_wheel_batch_reuses_one_composite_snapshot_for_every_scroll_step() {
|
||||
let mut vm = new_storage_test_vm("https://iframe-wheel-snapshot.test/");
|
||||
vm.set_viewport_surface(Some(crate::protocol_types::ViewportSurface {
|
||||
inner_width: 800,
|
||||
inner_height: 600,
|
||||
outer_width: 800,
|
||||
outer_height: 600,
|
||||
device_pixel_ratio: 1.0,
|
||||
screen_width: 800,
|
||||
screen_height: 600,
|
||||
screen_avail_width: 800,
|
||||
screen_avail_height: 600,
|
||||
}))
|
||||
.expect("iframe wheel snapshot viewport should update");
|
||||
vm.eval(
|
||||
r#"
|
||||
if (!document.documentElement) {
|
||||
document.appendChild(document.createElement('html'));
|
||||
}
|
||||
if (!document.body) {
|
||||
document.documentElement.appendChild(document.createElement('body'));
|
||||
}
|
||||
document.documentElement.style.cssText = 'margin:0;padding:0';
|
||||
document.body.style.cssText = 'margin:0;padding:0';
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = 'wheel-frame';
|
||||
frame.style.cssText = 'position:absolute;left:100px;top:80px;width:240px;height:180px;border:0;padding:0';
|
||||
document.body.appendChild(frame);
|
||||
const child = frame.contentDocument;
|
||||
child.documentElement.style.cssText = 'margin:0;padding:0';
|
||||
child.body.style.cssText = 'margin:0;padding:0';
|
||||
child.body.innerHTML = `
|
||||
<div id="wheel-scroller" style="position:absolute;left:10px;top:10px;width:160px;height:100px;overflow:auto">
|
||||
<div id="wheel-content" style="width:140px;height:500px"></div>
|
||||
</div>`;
|
||||
child.defaultView.__wheelGeometryReads = 0;
|
||||
child.getElementById('wheel-scroller').addEventListener('wheel', event => {
|
||||
void event.currentTarget.offsetWidth;
|
||||
child.defaultView.__wheelGeometryReads++;
|
||||
});
|
||||
'installed'
|
||||
"#,
|
||||
)
|
||||
.expect("iframe wheel snapshot fixture should initialize");
|
||||
|
||||
let passes_before = vm.layout_pass_observability_for_test().1;
|
||||
vm.begin_batched_mouse_event_dispatch();
|
||||
for delta_y in [10.0, 20.0, 30.0] {
|
||||
vm.dispatch_mouse_event_at_point_with_pointer_and_modifiers_without_checkpoint(
|
||||
130.0,
|
||||
110.0,
|
||||
"wheel",
|
||||
-1,
|
||||
Some(0),
|
||||
0,
|
||||
0.0,
|
||||
delta_y,
|
||||
crate::runtime::RendererPointerEventProperties::default(),
|
||||
0,
|
||||
)
|
||||
.expect("batched child wheel step should dispatch");
|
||||
}
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
passes_before + 1,
|
||||
"every wheel step must share the hit-test pass's recursively frozen snapshot"
|
||||
);
|
||||
vm.finish_batched_mouse_event_dispatch(Ok(()), true)
|
||||
.expect("batched child wheel effects should commit");
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
passes_before + 1,
|
||||
"committing derived effects must not perform another layout"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
"(() => { const frame = document.getElementById('wheel-frame'); return [frame.contentDocument.getElementById('wheel-scroller').scrollTop, frame.contentWindow.__wheelGeometryReads].join('|'); })()"
|
||||
)
|
||||
.expect("child scroll position should evaluate"),
|
||||
"60|3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focusing_visible_child_target_does_not_scroll_partially_hidden_transformed_iframe() {
|
||||
let mut vm = new_storage_test_vm("https://iframe-focus-scroll.test/");
|
||||
|
||||
Reference in New Issue
Block a user