@@ -211,10 +211,12 @@ enabled by default:
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Selectively enable the persistence required by the workload |
Layout is an on-demand snapshot rather than continuously maintained state. The
-first geometry request (a cold start) builds one complete layout from the
-current DOM/style and retains only the latest `LayoutPassOutput`. After that,
-ordinary geometry reads may reuse the snapshot even if the page has changed;
-screenshots and screencasts always rebuild and never reuse stale results.
+first geometry request (a cold start) builds a working layout tree from the
+current DOM/style, freezes its canonical geometry into an immutable,
+DOM-independent `FrozenLayoutTree`, and retains only that latest tree. Ordinary
+geometry reads may reuse it even if the page has changed; screenshots and
+screencasts always rebuild, replace the frozen tree, and never reuse stale
+paint results.
## Architecture
@@ -229,11 +231,13 @@ Rust, has its own ownership and lifecycle rules, and relies on:
- AnyRender/Vello CPU, `usvg`, and the Rust image ecosystem — software rendering
Document and style have a single source of truth: the native DOM and its Stylo
-integration. Every real refresh rebuilds layout from that source, converts the
-result into immutable, DOM-independent data, and then discards the temporary
-state created during that layout and paint pass. The system has no incremental
-layout tree, damage graph, retained display list, GPU compositor, or persistent
-window.
+integration. Every real refresh builds a temporary working tree from that
+source, optionally produces and consumes one fresh paint snapshot, freezes the
+canonical box/fragment geometry into a compact `FrozenLayoutTree`, and discards
+the working tree, style borrows, layout caches, diagnostics, and paint state.
+Source lookup and hit-test candidates are derived from the frozen tree when
+queried. The system has no incrementally maintained layout tree, damage graph,
+retained display list, GPU compositor, or persistent window.
## Test data
diff --git a/README.zh-CN.md b/README.zh-CN.md
index f43a72ed7b..9715cdbcc2 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -135,8 +135,8 @@ await browser.close();
| 智能体请求 | Moli 的处理方式 |
| --- | --- |
| 提取 HTML/Markdown、查询 DOM、运行 JS、检查网络/存储 | 直接读取浏览器运行时状态——不触发布局或绘制 |
-| 读取元素边界框、对某个坐标做命中测试、发送坐标输入 | 执行一次布局计算,只保留最新的几何快照 |
-| 截图或刷新屏幕串流 | 根据当前 DOM/样式重新构建、渲染新的一帧,用完即丢弃 |
+| 读取元素边界框、对某个坐标做命中测试、发送坐标输入 | 执行一次布局计算,只保留最新的冻结布局树 |
+| 截图或刷新屏幕串流 | 根据当前 DOM/样式重新构建并替换冻结树,渲染新的一帧,帧用完即丢弃 |
@@ -176,7 +176,7 @@ Moli 是 Lexmount 旗下的开源无头浏览器;Lexmount Browser 则是围绕
| `--image`、`--font`、`--audio`、`--video`、`--media`、`--text-track` | 单独启用某一类可选资源 |
| `--profile-dir`、`--http-cache-dir`、`--cookie-file` | 按工作负载需要,选择性开启持久化能力 |
-布局结果是按需采样的一份快照,而不是持续维护的状态:第一次几何请求(冷启动)会根据当前的 DOM/样式完整构建一次布局,并只保留最新的一份 `LayoutPassOutput`。在此之后,即便页面发生了变化,普通的几何读取也可能复用这份快照;但截图和屏幕串流每次都会重新构建,不会复用旧结果。
+布局结果是按需采样的一份快照,而不是持续维护的状态:第一次几何请求(冷启动)会根据当前 DOM/样式构建临时工作布局树,再把工作树丢弃后无法重算的几何冻结成一棵与 DOM 无关、不可变的 `FrozenLayoutTree`,长期只保留最新的这一棵树。在此之后,即便页面发生了变化,普通几何读取也可能复用旧树;截图和屏幕串流则每次都会重新构建并替换冻结树,不会复用旧的绘制结果。
## 架构
@@ -189,7 +189,7 @@ Moli 是一个独立的浏览器内核,而不是对 Chromium 的封装。它
- Taffy + Parley——盒模型与文本布局
- AnyRender/Vello CPU、`usvg` 以及 Rust 图像生态——软件渲染
-文档和样式只有一个事实来源:原生 DOM 与 Stylo 的集成。每次真正的刷新,都会基于这个事实来源重新构建布局,把结果转换成一份与 DOM 无关的不可变数据,然后丢弃这次布局和绘制过程中产生的临时状态。整个系统里没有增量布局树、没有损伤区域图、没有保留式显示列表、没有 GPU 合成器,也没有持久化的窗口。
+文档和样式只有一个事实来源:原生 DOM 与 Stylo 的集成。每次真正的刷新,都会据此构建临时工作树,按需生成并消费一份新的绘制快照,把最终的盒与文字分片几何冻结成紧凑的 `FrozenLayoutTree`,随后丢弃工作树、样式借用、布局缓存、诊断和绘制状态。来源查询表和命中测试候选不再长期保存,而是在查询时从冻结树派生。整个系统没有增量维护的布局树、损伤区域图、保留式显示列表、GPU 合成器或持久化窗口。
## 测试数据
diff --git a/assets/moli_ondemand_rendering_flow.svg b/assets/moli_ondemand_rendering_flow.svg
index e12ea2598a..7f5529f323 100644
--- a/assets/moli_ondemand_rendering_flow.svg
+++ b/assets/moli_ondemand_rendering_flow.svg
@@ -1,6 +1,6 @@
How Moli handles a request: DOM-first by default, layout and paint built fresh only on demand
-Native DOM and Stylo state is the single source of truth. Most requests take the DOM-first path directly, with no layout or paint. Requests that need geometry or pixels trigger an on-demand layout and paint pass, which produces a geometry snapshot or a screenshot, then that pass-local state is dropped.
+Native DOM and Stylo state is the single source of truth. Most requests take the DOM-first path directly, with no layout or paint. Requests that need geometry or pixels trigger an on-demand pass. Its geometry is frozen into the single retained layout tree, while fresh paint and all working state are discarded after use.
@@ -40,7 +40,7 @@
Geometry
- hit-test, input
+ latest frozen tree
@@ -56,8 +56,8 @@
- Layout & paint state dropped
- Kept only until the next request
+ Working layout & paint dropped
+ Only the frozen layout tree remains
-
\ No newline at end of file
+
diff --git a/moli-layout/src/error.rs b/moli-layout/src/error.rs
index 1319319934..080f4165e7 100644
--- a/moli-layout/src/error.rs
+++ b/moli-layout/src/error.rs
@@ -15,11 +15,11 @@ pub enum LayoutError {
/// A screenshot or screencast requested an invalid capture surface.
#[error("invalid paint capture: {detail}")]
InvalidPaintCapture { detail: String },
- /// One owned geometry projection is too large to publish or retain.
+ /// One frozen layout tree is too large to publish or retain.
#[error(
- "layout output contains {boxes} boxes, {fragments} fragments, and an estimated {estimated_bytes} bytes; limits are {max_boxes} boxes, {max_fragments} fragments, and {max_bytes} bytes"
+ "frozen layout tree contains {boxes} boxes, {fragments} fragments, and an estimated {estimated_bytes} bytes; limits are {max_boxes} boxes, {max_fragments} fragments, and {max_bytes} bytes"
)]
- OutputRetentionBudgetExceeded {
+ TreeRetentionBudgetExceeded {
boxes: usize,
fragments: usize,
estimated_bytes: usize,
diff --git a/moli-layout/src/lib.rs b/moli-layout/src/lib.rs
index 9fac49e072..f55bc4c7ce 100644
--- a/moli-layout/src/lib.rs
+++ b/moli-layout/src/lib.rs
@@ -3,7 +3,7 @@
//! The crate knows Stylo and Taffy but never knows Moli's live DOM or V8
//! runtime. Renderer-owned adapters lend a canonical source view and resolve
//! styles; all source/style borrows and per-pass caches are gone before the
-//! returned [`LayoutPassOutput`] or [`PaintSnapshot`] crosses into a consumer.
+//! returned [`FrozenLayoutTree`] or [`PaintSnapshot`] crosses into a consumer.
mod builder;
mod capture;
@@ -41,20 +41,19 @@ pub use normalize_source::{
NormalizedLayoutSourceNode, NormalizedLayoutSourceTree, normalize_layout_source,
};
pub use output::{
- GeometryProvider, LayoutAnswers, LayoutBoxGeometry, LayoutBoxModel, LayoutCaretPosition,
- LayoutClipChainId, LayoutClipNode, LayoutCoordinateSpace, LayoutCoordinateSpaceId,
- LayoutDocumentMetrics, LayoutElementMetrics, LayoutFlushReason, LayoutFragment,
- LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHit, LayoutHitTestEntry,
- LayoutHitTestIndex, LayoutIntersectionGeometry, LayoutNodeOutput, LayoutOutputBoxId,
- LayoutOutputRetentionMetrics, LayoutPassMetrics, LayoutPassOutput, LayoutPoint, LayoutQuad,
- LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect, LayoutScrollContainerMetrics,
- LayoutScrollExtent, LayoutScrollExtentId, LayoutScrollIntoViewGeometry, LayoutSize,
- LayoutTransform2D, LayoutViewport, MAX_RETAINED_LAYOUT_BOXES, MAX_RETAINED_LAYOUT_FRAGMENTS,
- MAX_RETAINED_LAYOUT_OUTPUT_BYTES,
+ 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,
};
pub use pass::{
- EmbeddedFrameRenderer, LayoutPassRequest, ScreenshotLayoutRequest, build_layout_pass_output,
- build_layout_pass_output_with_embedded_frames, build_screenshot_snapshot,
+ EmbeddedFrameRenderer, LayoutPassRequest, ScreenshotLayoutRequest, build_layout_pass,
+ build_layout_pass_with_embedded_frames, build_screenshot_snapshot,
};
pub use snapshot::{
PaintBlendMode, PaintBorderColors, PaintBorderStyle, PaintBorderStyles, PaintBoxShadow,
diff --git a/moli-layout/src/output.rs b/moli-layout/src/output.rs
index 2a392d9733..cc0aba6422 100644
--- a/moli-layout/src/output.rs
+++ b/moli-layout/src/output.rs
@@ -1,17 +1,17 @@
use std::{
- collections::{HashMap, HashSet},
+ collections::HashSet,
fmt::Debug,
hash::Hash,
- ops::Range,
+ ops::{Deref, Range},
time::Duration,
};
-use crate::{LayoutBoxKind, LayoutError, LayoutPosition, PaintDiagnostic, PaintSnapshot};
+use crate::{LayoutError, LayoutPosition, PaintDiagnostic, PaintSnapshot};
/// Viewport inputs shared by layout, geometry queries, and paint projection.
///
/// Dimensions are CSS pixels. Device-pixel conversion belongs to the paint
-/// backend and never changes the geometry stored in a layout output.
+/// backend and never changes the geometry stored in a frozen layout tree.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutViewport {
pub css_width: u32,
@@ -59,7 +59,7 @@ impl LayoutSize {
}
}
-/// An axis-aligned rectangle in one explicit [`LayoutCoordinateSpace`].
+/// An axis-aligned rectangle in one explicit layout coordinate space.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LayoutRect {
pub x: f32,
@@ -230,7 +230,9 @@ macro_rules! dense_output_id {
impl $name {
pub(crate) fn from_index(index: usize) -> Self {
- Self(u32::try_from(index).expect("one layout output exceeded the u32 id limit"))
+ Self(
+ u32::try_from(index).expect("one frozen layout tree exceeded the u32 id limit"),
+ )
}
pub const fn index(self) -> usize {
@@ -244,33 +246,47 @@ dense_output_id!(LayoutOutputBoxId);
dense_output_id!(LayoutFragmentId);
dense_output_id!(LayoutCoordinateSpaceId);
dense_output_id!(LayoutClipChainId);
-dense_output_id!(LayoutScrollExtentId);
-/// One explicit local coordinate system in a pass output.
+/// One explicit local coordinate system in a frozen layout tree.
#[derive(Clone, Debug, PartialEq)]
-pub struct LayoutCoordinateSpace {
- pub id: LayoutCoordinateSpaceId,
- pub owner: Option,
- pub parent: Option,
- pub local_to_parent: LayoutTransform2D,
+pub(crate) struct LayoutCoordinateSpace {
+ pub(crate) id: LayoutCoordinateSpaceId,
+ pub(crate) owner: Option,
+ pub(crate) parent: Option,
+ pub(crate) local_to_parent: LayoutTransform2D,
/// Maps local coordinates to the visual document coordinate system. This
/// includes element scrolling but excludes the viewport scroll offset.
- pub local_to_document: LayoutTransform2D,
+ pub(crate) local_to_document: LayoutTransform2D,
/// Maps local coordinates directly to viewport CSS pixels.
+ pub(crate) local_to_viewport: LayoutTransform2D,
+}
+
+/// Query-facing coordinate data retained for one frozen box-tree node.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FrozenCoordinateSpace {
+ pub owner: Option,
pub local_to_viewport: LayoutTransform2D,
}
+impl From for FrozenCoordinateSpace {
+ fn from(space: LayoutCoordinateSpace) -> Self {
+ Self {
+ owner: space.owner,
+ local_to_viewport: space.local_to_viewport,
+ }
+ }
+}
+
/// One rectangular clip linked to its ancestor clip.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutClipNode {
- pub id: LayoutClipChainId,
pub parent: Option,
pub owner: Option,
pub coordinate_space: LayoutCoordinateSpaceId,
pub rect: LayoutRect,
}
-/// Complete physical box model for one output-local CSS box.
+/// Complete physical box model for one tree-local CSS box.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutBoxModel {
pub content: LayoutQuad,
@@ -282,12 +298,9 @@ pub struct LayoutBoxModel {
/// Per-box scroll geometry in the box's own coordinate space.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutScrollExtent {
- pub id: LayoutScrollExtentId,
- pub owner: LayoutOutputBoxId,
pub scrollport: LayoutRect,
pub scrollable_overflow: LayoutRect,
pub scroll_size: LayoutSize,
- pub requested_offset: LayoutPoint,
pub applied_offset: LayoutPoint,
pub minimum_offset: LayoutPoint,
pub maximum_offset: LayoutPoint,
@@ -295,15 +308,12 @@ pub struct LayoutScrollExtent {
pub clips_overflow: bool,
}
-/// Geometry and provenance retained for one output-local box.
+/// Geometry retained for one tree-local box.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutBoxGeometry {
pub id: LayoutOutputBoxId,
pub parent: Option,
pub layout_parent: Option,
- pub positioned_containing_block: Option,
- pub role: LayoutBoxKind,
- pub source_label: String,
pub position: LayoutPosition,
pub coordinate_space: LayoutCoordinateSpaceId,
pub clip_chain: Option,
@@ -311,7 +321,6 @@ pub struct LayoutBoxGeometry {
pub padding_box: LayoutRect,
pub border_box: LayoutRect,
pub margin_box: LayoutRect,
- pub scroll_extent: LayoutScrollExtentId,
pub fragments: Vec,
/// Untransformed border-box origin in document layout coordinates.
pub layout_origin_in_document: LayoutPoint,
@@ -323,6 +332,31 @@ pub struct LayoutBoxGeometry {
pub pointer_events: bool,
}
+/// One node in the immutable layout tree retained after a full pass.
+///
+/// `geometry_source` associates ordinary CSSOM geometry with its source. A
+/// split inline continuation can therefore share its originating source while
+/// remaining a distinct box-tree node. `hit_source` is separate because a
+/// generated pseudo box participates in hit testing as its originating DOM
+/// element without manufacturing CSSOM rects for that element.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FrozenLayoutBox {
+ pub geometry: LayoutBoxGeometry,
+ pub scroll_extent: LayoutScrollExtent,
+ pub coordinate_space: FrozenCoordinateSpace,
+ pub geometry_source: Option,
+ pub principal_source: Option,
+ pub hit_source: Option,
+}
+
+impl Deref for FrozenLayoutBox {
+ type Target = LayoutBoxGeometry;
+
+ fn deref(&self) -> &Self::Target {
+ &self.geometry
+ }
+}
+
/// Physical boxes retained for one box fragment in that fragment's local
/// coordinate space. Inline elements can own several of these across lines.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -334,7 +368,7 @@ pub struct LayoutFragmentBoxModel {
}
/// A geometry fragment kind. IDs contained here are valid only in the same
-/// [`LayoutPassOutput`].
+/// [`FrozenLayoutTree`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LayoutFragmentKind {
Box {
@@ -353,7 +387,6 @@ pub enum LayoutFragmentKind {
Text {
box_id: LayoutOutputBoxId,
line_index: usize,
- source_byte_range: Range,
source_utf16_range: Range,
rtl: bool,
},
@@ -366,13 +399,12 @@ pub struct LayoutFragment {
pub kind: LayoutFragmentKind,
pub rect: LayoutRect,
pub box_model: Option,
- pub baseline: Option,
pub coordinate_space: LayoutCoordinateSpaceId,
pub clip_chain: Option,
pub paint_order: Option,
}
-/// Output references associated with one source-tree node.
+/// A short-lived source view derived from frozen box provenance.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LayoutNodeOutput {
pub principal_box: Option,
@@ -385,35 +417,28 @@ pub struct LayoutNodeOutput {
/// One front-to-back hit-test candidate.
#[derive(Clone, Debug, PartialEq)]
-pub struct LayoutHitTestEntry {
- pub source: N,
- pub fragment: LayoutFragmentId,
- pub coordinate_space: LayoutCoordinateSpaceId,
- pub clip_chain: Option,
- pub local_rect: LayoutRect,
- pub paint_order: u32,
- pub is_text: bool,
- pub pointer_events: bool,
+struct LayoutHitTestEntry {
+ source: N,
+ fragment: LayoutFragmentId,
+ coordinate_space: LayoutCoordinateSpaceId,
+ clip_chain: Option,
+ local_rect: LayoutRect,
+ paint_order: u32,
+ is_text: bool,
+ pointer_events: bool,
}
-/// A pass-local spatial index. The initial implementation is a compact
-/// paint-ordered vector; the schema does not expose or require a retained R-tree.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct LayoutHitTestIndex {
- pub entries: Vec>,
-}
-
-/// Result of resolving a point against the pass-local hit-test index.
+/// Result of resolving a point against hit candidates derived from the tree.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHit {
pub source: N,
/// The real provider identifies the exact fragment. Explicit mock
/// providers can return a source-only hit without manufacturing a
- /// pass-local fragment identity.
+ /// tree-local fragment identity.
pub fragment: Option,
pub local_point: LayoutPoint,
pub is_text: bool,
- /// Box geometry copied from the same output when the hit source owns a
+ /// 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
/// forcing a second parent-document pass.
@@ -596,7 +621,7 @@ pub enum LayoutQueryAnswer {
EventOffset(Option),
}
-/// Minimal owned results copied from one transient pass output.
+/// Minimal owned results derived from one frozen layout tree.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutAnswers {
pub answers: Vec>,
@@ -610,7 +635,7 @@ pub trait GeometryProvider {
/// Answers a batch from the provider's latest layout state.
///
/// The provider decides whether this requires a fresh pass or can reuse an
- /// already-owned output; callers must not assume that one call equals one
+ /// already-owned tree; callers must not assume that one call equals one
/// layout computation.
fn answer(
&mut self,
@@ -620,14 +645,13 @@ pub trait GeometryProvider {
) -> Result, LayoutError>;
}
-/// Retained geometry footprint for one owned pass output.
+/// Retained footprint for one frozen layout tree.
///
-/// The byte count is an allocation-capacity estimate for the output's
-/// geometry, fragment, mapping, hit-test, and diagnostic storage. It excludes
-/// allocator metadata and the optional paint snapshot, which consumers must
-/// remove before retaining an output as a latest-layout snapshot.
+/// The byte count is an allocation-capacity estimate for the tree's box,
+/// fragment, source-provenance, scroll, transform, and clip storage. It
+/// excludes allocator metadata and every pass-only diagnostic or paint value.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-pub struct LayoutOutputRetentionMetrics {
+pub struct LayoutTreeRetentionMetrics {
pub box_count: usize,
pub fragment_count: usize,
pub estimated_geometry_bytes: usize,
@@ -637,40 +661,63 @@ pub struct LayoutOutputRetentionMetrics {
pub const MAX_RETAINED_LAYOUT_BOXES: usize = 1_000_000;
/// Maximum fragments retained in the single latest-layout snapshot.
pub const MAX_RETAINED_LAYOUT_FRAGMENTS: usize = 4_000_000;
-/// Maximum estimated allocation capacity retained by one latest-layout output.
-pub const MAX_RETAINED_LAYOUT_OUTPUT_BYTES: usize = 256 * 1024 * 1024;
+/// Maximum estimated allocation capacity retained by one frozen layout tree.
+pub const MAX_RETAINED_LAYOUT_TREE_BYTES: usize = 256 * 1024 * 1024;
-/// Immutable geometry, fragment, hit-test, diagnostics, and paint projection
-/// produced by exactly one full layout demand.
-pub struct LayoutPassOutput
+/// Immutable, DOM-independent layout tree produced by one complete pass.
+///
+/// The box tree is stored densely through parent IDs. Text/inline fragments,
+/// coordinate spaces, clips, and scroll extents are canonical layout data:
+/// they preserve results that cannot be reconstructed after the working tree,
+/// Taffy caches, Parley state, and computed styles are dropped. Source and
+/// hit-test indexes are derived from the box provenance and fragments.
+pub struct FrozenLayoutTree
where
N: Copy + Debug + Eq + Hash,
{
- pub schema_version: u32,
pub viewport: LayoutViewport,
pub viewport_scroll: LayoutPoint,
pub content_size: LayoutSize,
pub root_box: LayoutOutputBoxId,
- pub boxes: Vec,
- pub box_sources: Vec>,
+ pub boxes: Vec>,
pub fragments: Vec,
- pub source_mapping: HashMap,
- pub scroll_extents: Vec,
- pub coordinate_spaces: Vec,
+ /// Source/box relationships for `display: contents` nodes, which own no
+ /// principal box but can still nominate rendered descendants for scroll.
+ pub scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
+ viewport_coordinate_space: FrozenCoordinateSpace,
pub clip_chain: Vec,
- pub paint_order: Vec,
- pub hit_test_index: LayoutHitTestIndex,
+}
+
+/// Transient products of exactly one complete layout demand.
+///
+/// Consumers may inspect the tree and take an optional paint snapshot while
+/// handling the demand. Only [`FrozenLayoutTree`] crosses the latest-layout
+/// retention boundary; diagnostics, metrics, and paint remain pass-owned.
+pub struct LayoutPassResult
+where
+ N: Copy + Debug + Eq + Hash,
+{
+ pub tree: FrozenLayoutTree,
pub diagnostics: Vec,
pub metrics: LayoutPassMetrics,
paint_snapshot: Option,
}
-impl LayoutPassOutput
+impl Deref for LayoutPassResult
where
N: Copy + Debug + Eq + Hash,
{
- pub const SCHEMA_VERSION: u32 = 2;
+ type Target = FrozenLayoutTree;
+ fn deref(&self) -> &Self::Target {
+ &self.tree
+ }
+}
+
+impl LayoutPassResult
+where
+ N: Copy + Debug + Eq + Hash,
+{
pub fn paint_snapshot(&self) -> Option<&PaintSnapshot> {
self.paint_snapshot.as_ref()
}
@@ -686,81 +733,115 @@ where
.ok_or(LayoutError::PaintProjectionNotRequested)
}
- pub fn retention_metrics(&self) -> LayoutOutputRetentionMetrics {
+ /// Consumes every pass-only product and returns the sole retainable tree.
+ pub fn into_tree(self) -> FrozenLayoutTree {
+ self.tree
+ }
+
+ pub fn retention_metrics(&self) -> LayoutTreeRetentionMetrics {
+ self.tree.retention_metrics()
+ }
+
+ pub fn validate_retention_budget(&self) -> Result<(), LayoutError> {
+ self.tree.validate_retention_budget()
+ }
+
+ pub fn answer_queries(&self, batch: &LayoutQueryBatch) -> LayoutAnswers {
+ self.tree.answer_queries(batch, self.metrics)
+ }
+}
+
+impl FrozenLayoutTree
+where
+ N: Copy + Debug + Eq + Hash,
+{
+ pub fn retention_metrics(&self) -> LayoutTreeRetentionMetrics {
fn allocation(capacity: usize) -> usize {
capacity.saturating_mul(std::mem::size_of::())
}
- let box_allocations = self.boxes.iter().fold(0usize, |bytes, geometry| {
- bytes
- .saturating_add(geometry.source_label.capacity())
- .saturating_add(allocation::(
- geometry.fragments.capacity(),
- ))
- });
- let source_mapping_allocations = self.source_mapping.values().fold(
- allocation::<(N, LayoutNodeOutput)>(self.source_mapping.capacity()),
- |bytes, output| {
- bytes
- .saturating_add(allocation::(output.fragments.capacity()))
- .saturating_add(allocation::(
- output.scroll_proxy_boxes.capacity(),
- ))
- },
- );
- let diagnostic_allocations = self.diagnostics.iter().fold(0usize, |bytes, diagnostic| {
- bytes
- .saturating_add(diagnostic.code.capacity())
- .saturating_add(diagnostic.message.capacity())
+ let box_allocations = self.boxes.iter().fold(0usize, |bytes, layout_box| {
+ bytes.saturating_add(allocation::(
+ layout_box.fragments.capacity(),
+ ))
});
let estimated_geometry_bytes = std::mem::size_of::()
- .saturating_add(allocation::(self.boxes.capacity()))
- .saturating_add(allocation::>(self.box_sources.capacity()))
+ .saturating_add(allocation::>(self.boxes.capacity()))
.saturating_add(allocation::(self.fragments.capacity()))
- .saturating_add(allocation::(
- self.scroll_extents.capacity(),
- ))
- .saturating_add(allocation::(
- self.coordinate_spaces.capacity(),
+ .saturating_add(allocation::<(N, LayoutOutputBoxId)>(
+ self.scroll_proxy_links.capacity(),
))
.saturating_add(allocation::(self.clip_chain.capacity()))
- .saturating_add(allocation::(self.paint_order.capacity()))
- .saturating_add(allocation::>(
- self.hit_test_index.entries.capacity(),
- ))
- .saturating_add(allocation::(self.diagnostics.capacity()))
- .saturating_add(box_allocations)
- .saturating_add(source_mapping_allocations)
- .saturating_add(diagnostic_allocations);
- LayoutOutputRetentionMetrics {
+ .saturating_add(box_allocations);
+ LayoutTreeRetentionMetrics {
box_count: self.boxes.len(),
fragment_count: self.fragments.len(),
estimated_geometry_bytes,
}
}
- /// Rejects an output that would make the single latest-layout slot an
- /// unbounded retained allocation. Paint resources are intentionally not
- /// counted here because consumers remove them before publication and the
- /// raster boundary applies separate command/glyph/pixel/surface budgets.
+ /// Rejects a tree that would make the single latest-layout slot an
+ /// unbounded retained allocation.
pub fn validate_retention_budget(&self) -> Result<(), LayoutError> {
validate_retention_metrics(self.retention_metrics())
}
pub fn box_geometry(&self, id: LayoutOutputBoxId) -> Option<&LayoutBoxGeometry> {
- self.boxes.get(id.index())
+ self.boxes
+ .get(id.index())
+ .map(|layout_box| &layout_box.geometry)
}
pub fn fragment(&self, id: LayoutFragmentId) -> Option<&LayoutFragment> {
self.fragments.get(id.index())
}
- pub fn coordinate_space(&self, id: LayoutCoordinateSpaceId) -> Option<&LayoutCoordinateSpace> {
- self.coordinate_spaces.get(id.index())
+ pub fn coordinate_space(&self, id: LayoutCoordinateSpaceId) -> Option<&FrozenCoordinateSpace> {
+ match id.index() {
+ 0 => Some(&self.viewport_coordinate_space),
+ index => self
+ .boxes
+ .get(index - 1)
+ .map(|layout_box| &layout_box.coordinate_space),
+ }
}
- pub fn source_output(&self, source: N) -> Option<&LayoutNodeOutput> {
- self.source_mapping.get(&source)
+ pub fn scroll_extent(&self, id: LayoutOutputBoxId) -> Option<&LayoutScrollExtent> {
+ self.boxes
+ .get(id.index())
+ .map(|layout_box| &layout_box.scroll_extent)
+ }
+
+ /// Derives the source view from canonical box provenance.
+ ///
+ /// No source hash table survives the pass. The returned IDs are copied
+ /// into one short-lived query value.
+ pub fn source_output(&self, source: N) -> Option {
+ let mut found = false;
+ let mut output = LayoutNodeOutput::default();
+ for layout_box in &self.boxes {
+ if layout_box.principal_source == Some(source) {
+ output.principal_box = Some(layout_box.id);
+ found = true;
+ }
+ if layout_box.geometry_source == Some(source) {
+ output
+ .fragments
+ .extend(layout_box.fragments.iter().copied().filter(|id| {
+ self.fragment(*id).is_some_and(|fragment| {
+ !matches!(fragment.kind, LayoutFragmentKind::Line { .. })
+ })
+ }));
+ found = true;
+ }
+ }
+ for (proxy_source, box_id) in &self.scroll_proxy_links {
+ if *proxy_source == source {
+ output.scroll_proxy_boxes.push(*box_id);
+ found = true;
+ }
+ }
+ found.then_some(output)
}
pub fn element_metrics_for_source(&self, source: N) -> Option> {
@@ -772,7 +853,7 @@ where
/// ancestor tree scopes.
///
/// Shadow DOM tree-scope visibility is an HTML/DOM concern, not a CSS box
- /// tree concern. The pass output therefore retains the complete box chain
+ /// tree concern. The frozen tree therefore retains the complete box chain
/// and lets its short-lived consumer supply that one predicate. Geometry
/// is still derived wholly from this pass; no live layout state is read or
/// retained here.
@@ -784,12 +865,15 @@ where
let output = self.source_output(source)?;
let box_id = output.principal_box?;
let geometry = self.box_geometry(box_id)?;
- let extent = self.scroll_extents.get(geometry.scroll_extent.index())?;
+ let extent = self.scroll_extent(box_id)?;
let coordinate_space = self.coordinate_space(geometry.coordinate_space)?;
let is_root = box_id == self.root_box;
let offset_parent_id = self.offset_parent_box(box_id, &mut offset_parent_is_exposed);
- let offset_parent =
- offset_parent_id.and_then(|id| self.box_sources.get(id.index()).copied().flatten());
+ let offset_parent = offset_parent_id.and_then(|id| {
+ self.boxes
+ .get(id.index())
+ .and_then(|layout_box| layout_box.geometry_source)
+ });
let offset_parent_origin = offset_parent_id
.and_then(|id| self.box_geometry(id))
.map(|parent| {
@@ -978,10 +1062,12 @@ where
let mut scroll_containers = Vec::new();
while let Some(box_id) = candidate {
let geometry = self.box_geometry(box_id)?;
- let extent = self.scroll_extents.get(geometry.scroll_extent.index())?;
+ let extent = self.scroll_extent(box_id)?;
if (extent.is_scroll_container || box_id == self.root_box)
- && let Some(container_source) =
- self.box_sources.get(box_id.index()).copied().flatten()
+ && let Some(container_source) = self
+ .boxes
+ .get(box_id.index())
+ .and_then(|layout_box| layout_box.geometry_source)
&& seen.insert(container_source)
&& let Some(metrics) = self.element_metrics_for_source(container_source)
{
@@ -1048,7 +1134,9 @@ where
root: Option,
) -> Option {
let target_output = self.source_output(target);
- let target_box = target_output.and_then(|output| output.principal_box);
+ let target_box = target_output
+ .as_ref()
+ .and_then(|output| output.principal_box);
let root_box = root.and_then(|source| self.source_output(source)?.principal_box);
let root_is_layout_ancestor = match (target_box, root_box, root) {
(_, _, None) => true,
@@ -1058,8 +1146,7 @@ where
_ => false,
};
let root_clips_overflow = root_box
- .and_then(|root_box| self.box_geometry(root_box))
- .and_then(|geometry| self.scroll_extents.get(geometry.scroll_extent.index()))
+ .and_then(|root_box| self.scroll_extent(root_box))
.is_some_and(|extent| extent.clips_overflow);
let root_rect = if root.is_none() {
LayoutTransform2D::IDENTITY.map_rect(LayoutRect::new(
@@ -1072,7 +1159,7 @@ where
let geometry = self.box_geometry(root_box)?;
Some((
geometry,
- self.scroll_extents.get(geometry.scroll_extent.index())?,
+ self.scroll_extent(root_box)?,
self.coordinate_space(geometry.coordinate_space)?,
))
}) {
@@ -1105,7 +1192,7 @@ where
clip = node.parent;
}
};
- if let Some(target_output) = target_output {
+ if let Some(target_output) = target_output.as_ref() {
for fragment in target_output
.fragments
.iter()
@@ -1114,7 +1201,9 @@ where
add_clip_chain(fragment.clip_chain);
}
}
- if target_output.is_none_or(|output| output.fragments.is_empty())
+ if target_output
+ .as_ref()
+ .is_none_or(|output| output.fragments.is_empty())
&& let Some(target_box) = target_box
&& let Some(geometry) = self.box_geometry(target_box)
{
@@ -1337,10 +1426,9 @@ where
viewport_point: LayoutPoint,
ignore_pointer_events_none: bool,
) -> Option> {
- self.hit_test_index
- .entries
- .iter()
- .find_map(|entry| self.hit_for_entry(entry, viewport_point, ignore_pointer_events_none))
+ self.hit_test_entries().into_iter().find_map(|entry| {
+ self.hit_for_entry(&entry, viewport_point, ignore_pointer_events_none)
+ })
}
pub fn hit_test_all(
@@ -1350,8 +1438,8 @@ where
) -> Vec> {
let mut seen = HashSet::new();
let mut hits = Vec::new();
- for entry in &self.hit_test_index.entries {
- let Some(hit) = self.hit_for_entry(entry, viewport_point, ignore_pointer_events_none)
+ for entry in self.hit_test_entries() {
+ let Some(hit) = self.hit_for_entry(&entry, viewport_point, ignore_pointer_events_none)
else {
continue;
};
@@ -1363,15 +1451,12 @@ where
}
pub fn caret_position(&self, viewport_point: LayoutPoint) -> Option> {
- let top_entry = self
- .hit_test_index
- .entries
+ let entries = self.hit_test_entries();
+ let top_entry = entries
.iter()
.find(|entry| self.hit_for_entry(entry, viewport_point, true).is_some())?;
let top_box = self.fragment_box_id(top_entry.fragment)?;
- let text_entry = self
- .hit_test_index
- .entries
+ let text_entry = entries
.iter()
.filter(|entry| entry.is_text)
.filter(|entry| {
@@ -1411,6 +1496,42 @@ where
})
}
+ /// Builds the front-to-back hit candidates for one query.
+ ///
+ /// Paint order, source provenance, transforms, and clips are canonical
+ /// tree data. The duplicated candidate vector is deliberately temporary.
+ fn hit_test_entries(&self) -> Vec> {
+ let mut entries = self
+ .fragments
+ .iter()
+ .filter_map(|fragment| {
+ let paint_order = fragment.paint_order?;
+ let (box_id, is_text) = match fragment.kind {
+ LayoutFragmentKind::Box { box_id }
+ | LayoutFragmentKind::InlineBox { box_id, .. } => (box_id, false),
+ LayoutFragmentKind::Text { box_id, .. } => (box_id, true),
+ LayoutFragmentKind::Line { .. } => return None,
+ };
+ let layout_box = self.boxes.get(box_id.index())?;
+ if !layout_box.visible {
+ return None;
+ }
+ Some(LayoutHitTestEntry {
+ source: layout_box.hit_source?,
+ fragment: fragment.id,
+ coordinate_space: fragment.coordinate_space,
+ clip_chain: fragment.clip_chain,
+ local_rect: fragment.rect,
+ paint_order,
+ is_text,
+ pointer_events: layout_box.pointer_events,
+ })
+ })
+ .collect::>();
+ entries.sort_by_key(|entry| std::cmp::Reverse(entry.paint_order));
+ entries
+ }
+
fn caret_position_for_text_entry(
&self,
entry: &LayoutHitTestEntry,
@@ -1505,7 +1626,10 @@ where
let mut seen = HashSet::new();
let mut ancestors = Vec::new();
loop {
- if let Some(source) = self.box_sources.get(box_id.index()).copied().flatten()
+ if let Some(source) = self
+ .boxes
+ .get(box_id.index())
+ .and_then(|layout_box| layout_box.geometry_source)
&& seen.insert(source)
&& let Some(model) = self.box_model_for_source(source)
{
@@ -1545,7 +1669,11 @@ where
})
}
- pub fn answer_queries(&self, batch: &LayoutQueryBatch) -> LayoutAnswers {
+ pub fn answer_queries(
+ &self,
+ batch: &LayoutQueryBatch,
+ metrics: LayoutPassMetrics,
+ ) -> LayoutAnswers {
let answers = batch
.queries
.iter()
@@ -1603,10 +1731,7 @@ where
}
})
.collect();
- LayoutAnswers {
- answers,
- metrics: self.metrics,
- }
+ LayoutAnswers { answers, metrics }
}
fn point_passes_clip_chain(
@@ -1676,7 +1801,10 @@ where
let mut candidate = geometry.parent;
while let Some(id) = candidate {
let parent = self.box_geometry(id)?;
- let source = self.box_sources.get(id.index()).copied().flatten();
+ let source = self
+ .boxes
+ .get(id.index())
+ .and_then(|layout_box| layout_box.geometry_source);
let Some(source) = source else {
candidate = parent.parent;
continue;
@@ -1727,34 +1855,38 @@ where
viewport_scroll: LayoutPoint,
content_size: LayoutSize,
root_box: LayoutOutputBoxId,
- boxes: Vec,
- box_sources: Vec>,
+ boxes: Vec>,
fragments: Vec,
- source_mapping: HashMap,
- scroll_extents: Vec,
- coordinate_spaces: Vec,
+ scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
+ viewport_coordinate_space: FrozenCoordinateSpace,
clip_chain: Vec,
- paint_order: Vec,
- hit_test_index: LayoutHitTestIndex,
- diagnostics: Vec,
- metrics: LayoutPassMetrics,
- paint_snapshot: Option,
) -> Self {
Self {
- schema_version: Self::SCHEMA_VERSION,
viewport,
viewport_scroll,
content_size,
root_box,
boxes,
- box_sources,
fragments,
- source_mapping,
- scroll_extents,
- coordinate_spaces,
+ scroll_proxy_links,
+ viewport_coordinate_space,
clip_chain,
- paint_order,
- hit_test_index,
+ }
+ }
+}
+
+impl LayoutPassResult
+where
+ N: Copy + Debug + Eq + Hash,
+{
+ pub(crate) fn new(
+ tree: FrozenLayoutTree,
+ diagnostics: Vec,
+ metrics: LayoutPassMetrics,
+ paint_snapshot: Option,
+ ) -> Self {
+ Self {
+ tree,
diagnostics,
metrics,
paint_snapshot,
@@ -1766,18 +1898,18 @@ fn axis_aligned_union(left: LayoutQuad, right: LayoutQuad) -> LayoutQuad {
LayoutTransform2D::IDENTITY.map_rect(left.bounding_rect().union(right.bounding_rect()))
}
-fn validate_retention_metrics(metrics: LayoutOutputRetentionMetrics) -> Result<(), LayoutError> {
+fn validate_retention_metrics(metrics: LayoutTreeRetentionMetrics) -> Result<(), LayoutError> {
if metrics.box_count > MAX_RETAINED_LAYOUT_BOXES
|| metrics.fragment_count > MAX_RETAINED_LAYOUT_FRAGMENTS
- || metrics.estimated_geometry_bytes > MAX_RETAINED_LAYOUT_OUTPUT_BYTES
+ || metrics.estimated_geometry_bytes > MAX_RETAINED_LAYOUT_TREE_BYTES
{
- return Err(LayoutError::OutputRetentionBudgetExceeded {
+ return Err(LayoutError::TreeRetentionBudgetExceeded {
boxes: metrics.box_count,
fragments: metrics.fragment_count,
estimated_bytes: metrics.estimated_geometry_bytes,
max_boxes: MAX_RETAINED_LAYOUT_BOXES,
max_fragments: MAX_RETAINED_LAYOUT_FRAGMENTS,
- max_bytes: MAX_RETAINED_LAYOUT_OUTPUT_BYTES,
+ max_bytes: MAX_RETAINED_LAYOUT_TREE_BYTES,
});
}
Ok(())
@@ -1810,30 +1942,30 @@ mod tests {
}
#[test]
- fn retained_output_budget_reports_each_bounded_dimension() {
+ fn retained_tree_budget_reports_each_bounded_dimension() {
for metrics in [
- LayoutOutputRetentionMetrics {
+ LayoutTreeRetentionMetrics {
box_count: MAX_RETAINED_LAYOUT_BOXES + 1,
..Default::default()
},
- LayoutOutputRetentionMetrics {
+ LayoutTreeRetentionMetrics {
fragment_count: MAX_RETAINED_LAYOUT_FRAGMENTS + 1,
..Default::default()
},
- LayoutOutputRetentionMetrics {
- estimated_geometry_bytes: MAX_RETAINED_LAYOUT_OUTPUT_BYTES + 1,
+ LayoutTreeRetentionMetrics {
+ estimated_geometry_bytes: MAX_RETAINED_LAYOUT_TREE_BYTES + 1,
..Default::default()
},
] {
assert!(matches!(
validate_retention_metrics(metrics),
- Err(LayoutError::OutputRetentionBudgetExceeded { .. })
+ Err(LayoutError::TreeRetentionBudgetExceeded { .. })
));
}
- validate_retention_metrics(LayoutOutputRetentionMetrics {
+ validate_retention_metrics(LayoutTreeRetentionMetrics {
box_count: MAX_RETAINED_LAYOUT_BOXES,
fragment_count: MAX_RETAINED_LAYOUT_FRAGMENTS,
- estimated_geometry_bytes: MAX_RETAINED_LAYOUT_OUTPUT_BYTES,
+ estimated_geometry_bytes: MAX_RETAINED_LAYOUT_TREE_BYTES,
})
.expect("each exact retention limit should be accepted");
}
diff --git a/moli-layout/src/pass.rs b/moli-layout/src/pass.rs
index 934ec0ff7f..b9a89f92da 100644
--- a/moli-layout/src/pass.rs
+++ b/moli-layout/src/pass.rs
@@ -1,9 +1,8 @@
use crate::{
- DocumentLayoutServices, LayoutError, LayoutFlushReason, LayoutPassOutput, LayoutSource,
+ DocumentLayoutServices, LayoutError, LayoutFlushReason, LayoutPassResult, LayoutSource,
LayoutStyleResolver, LayoutViewport, PaintCaptureRequest, PaintSnapshot, PaintViewport,
build_layout_world, form::prepare_form_controls, inline::prepare_inline_contexts,
- list::prepare_list_markers, projection::project_layout_output,
- taffy_tree::compute_world_layout,
+ list::prepare_list_markers, projection::finish_layout_pass, taffy_tree::compute_world_layout,
};
use std::collections::HashMap;
use std::time::Instant;
@@ -101,10 +100,11 @@ impl ScreenshotLayoutRequest {
}
}
-/// Builds, lays out, and projects one synchronous source view into an owned snapshot.
+/// Builds, lays out, and paints one synchronous source view into an owned snapshot.
///
-/// The pass-local box tree and Taffy caches are dropped before this function
-/// returns. The snapshot contains no DOM handles or computed-style references.
+/// The working box tree and Taffy caches are dropped before this function
+/// returns. This convenience path consumes the frozen tree as well and returns
+/// only DOM-neutral paint input.
pub fn build_screenshot_snapshot(
source: &S,
styles: &mut R,
@@ -115,39 +115,34 @@ where
S: LayoutSource,
R: LayoutStyleResolver,
{
- build_layout_pass_output(
+ build_layout_pass(
source,
styles,
services,
LayoutPassRequest::with_paint(request.viewport, LayoutFlushReason::Screenshot),
)
- .and_then(LayoutPassOutput::into_paint_snapshot)
+ .and_then(LayoutPassResult::into_paint_snapshot)
}
/// Builds one complete layout result from a borrowed source view.
///
/// Box construction, inline shaping, Taffy caches, and all style borrows remain
-/// local to this call. The returned value owns geometry, source handles,
-/// diagnostics, and hit-test data. A DOM-neutral paint snapshot is included
-/// only when requested. Consumers that need several geometry answers must batch
-/// them against this one value instead of triggering a layout per query.
-pub fn build_layout_pass_output(
+/// local to this call. The returned pass value owns one [`crate::FrozenLayoutTree`]
+/// plus pass-only metrics, diagnostics, and an optional DOM-neutral paint
+/// snapshot. Consumers may retain the tree, but not the surrounding pass
+/// value. Several geometry answers should be batched against one tree instead
+/// of triggering a layout per query.
+pub fn build_layout_pass(
source: &S,
styles: &mut R,
services: &mut DocumentLayoutServices,
request: LayoutPassRequest,
-) -> Result, LayoutError>
+) -> Result, LayoutError>
where
S: LayoutSource,
R: LayoutStyleResolver,
{
- build_layout_pass_output_with_embedded_frames(
- source,
- styles,
- services,
- request,
- &mut NoEmbeddedFrames,
- )
+ build_layout_pass_with_embedded_frames(source, styles, services, request, &mut NoEmbeddedFrames)
}
/// Builds one complete layout result and resolves embedded frame pixels after
@@ -156,13 +151,13 @@ where
/// 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.
-pub fn build_layout_pass_output_with_embedded_frames(
+pub fn build_layout_pass_with_embedded_frames(
source: &S,
styles: &mut R,
services: &mut DocumentLayoutServices,
request: LayoutPassRequest,
frames: &mut F,
-) -> Result, LayoutError>
+) -> Result, LayoutError>
where
S: LayoutSource,
R: LayoutStyleResolver,
@@ -212,7 +207,7 @@ where
}
}
}
- project_layout_output(
+ finish_layout_pass(
&world,
request.viewport,
request.reason,
diff --git a/moli-layout/src/projection.rs b/moli-layout/src/projection.rs
index 52c62b323c..d101f3621a 100644
--- a/moli-layout/src/projection.rs
+++ b/moli-layout/src/projection.rs
@@ -2,25 +2,25 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Instant};
use taffy::ResolveOrZero;
+use crate::output::LayoutCoordinateSpace;
use crate::stacking::{PaintOrderEvent, build_paint_order};
use crate::{
- LayoutAnonymousReason, LayoutBoxGeometry, LayoutBoxId, LayoutClipChainId, LayoutClipNode,
- LayoutCoordinateSpace, LayoutCoordinateSpaceId, LayoutError, LayoutFlushReason, LayoutFragment,
- LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHitTestEntry,
- LayoutHitTestIndex, LayoutNodeOutput, LayoutOutputBoxId, LayoutPassMetrics, LayoutPassOutput,
- LayoutPoint, LayoutRect, LayoutScrollExtent, LayoutScrollExtentId, LayoutSize,
- LayoutTransform2D, LayoutViewport, LayoutWorld, PaintCaptureRequest, PaintDiagnostic,
- PaintDiagnosticSeverity,
+ FrozenCoordinateSpace, FrozenLayoutBox, FrozenLayoutTree, LayoutAnonymousReason,
+ LayoutBoxGeometry, LayoutBoxId, LayoutClipChainId, LayoutClipNode, LayoutCoordinateSpaceId,
+ LayoutError, LayoutFlushReason, LayoutFragment, LayoutFragmentBoxModel, LayoutFragmentId,
+ LayoutFragmentKind, LayoutOutputBoxId, LayoutPassMetrics, LayoutPassResult, LayoutPoint,
+ LayoutRect, LayoutScrollExtent, LayoutSize, LayoutTransform2D, LayoutViewport, LayoutWorld,
+ PaintCaptureRequest, PaintDiagnostic, PaintDiagnosticSeverity,
};
-pub(crate) fn project_layout_output(
+pub(crate) fn finish_layout_pass(
world: &LayoutWorld,
viewport: LayoutViewport,
reason: LayoutFlushReason,
started: Instant,
paint_capture: Option,
embedded_frames: &mut HashMap,
-) -> Result, LayoutError>
+) -> Result, LayoutError>
where
N: Copy + Debug + Eq + Hash,
{
@@ -28,7 +28,7 @@ where
projection.build_local_box_geometry();
projection.resolve_scrollable_overflow();
projection.build_coordinate_spaces()?;
- projection.build_fragments_and_source_mapping();
+ projection.build_fragments();
projection.assign_clip_and_paint_order();
let content_size = projection.document_content_size();
@@ -72,22 +72,9 @@ where
.count(),
};
- Ok(LayoutPassOutput::new(
- viewport,
- projection.viewport_scroll,
- content_size,
- LayoutOutputBoxId::from_index(world.root.index()),
- projection.boxes,
- projection.box_sources,
- projection.fragments,
- projection.source_mapping,
- projection.scroll_extents,
- projection.coordinate_spaces,
- projection.clip_chain,
- projection.paint_order,
- LayoutHitTestIndex {
- entries: projection.hit_entries,
- },
+ let tree = projection.into_frozen_tree(content_size);
+ Ok(LayoutPassResult::new(
+ tree,
diagnostics,
metrics,
paint_snapshot,
@@ -103,13 +90,14 @@ where
viewport_scroll: LayoutPoint,
pub(crate) boxes: Vec,
box_sources: Vec>,
+ principal_sources: Vec >,
+ hit_sources: Vec >,
fragments: Vec,
- source_mapping: HashMap,
+ scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
pub(crate) scroll_extents: Vec,
pub(crate) coordinate_spaces: Vec,
pub(crate) clip_chain: Vec,
- paint_order: Vec,
- hit_entries: Vec>,
+ paint_order_count: usize,
pub(crate) diagnostics: Vec,
resolved_transforms: Vec,
overflow: Vec,
@@ -127,19 +115,33 @@ where
{
fn new(world: &'a LayoutWorld, viewport: LayoutViewport) -> Self {
let count = world.boxes.len();
+ let mut principal_sources = vec![None; count];
+ for (source, box_id) in &world.source_mapping {
+ principal_sources[box_id.index()] = Some(*source);
+ }
+ let scroll_proxy_links = world
+ .display_contents_mapping
+ .iter()
+ .flat_map(|(source, boxes)| {
+ boxes
+ .iter()
+ .map(|box_id| (*source, LayoutOutputBoxId::from_index(box_id.index())))
+ })
+ .collect();
Self {
world,
viewport,
viewport_scroll: LayoutPoint::ZERO,
boxes: Vec::with_capacity(count),
box_sources: Vec::with_capacity(count),
+ principal_sources,
+ hit_sources: Vec::with_capacity(count),
fragments: Vec::new(),
- source_mapping: HashMap::with_capacity(world.source_mapping.len()),
+ scroll_proxy_links,
scroll_extents: Vec::with_capacity(count),
coordinate_spaces: Vec::with_capacity(count + 1),
clip_chain: Vec::new(),
- paint_order: Vec::new(),
- hit_entries: Vec::new(),
+ paint_order_count: 0,
diagnostics: Vec::new(),
resolved_transforms: vec![LayoutTransform2D::IDENTITY; count],
overflow: vec![LayoutRect::ZERO; count],
@@ -239,6 +241,13 @@ where
.flatten()
});
self.box_sources.push(source);
+ self.hit_sources.push(layout_box.source.or_else(|| {
+ (layout_box.pseudo.is_some()
+ || layout_box.anonymous_reason
+ == Some(LayoutAnonymousReason::InlineSplitContinuation))
+ .then_some(layout_box.owner)
+ .flatten()
+ }));
let semantics = layout_box.element_semantics();
let (layout_x, layout_y) = self
.world
@@ -251,11 +260,6 @@ where
layout_parent: layout_box
.layout_parent
.map(|parent| LayoutOutputBoxId::from_index(parent.index())),
- positioned_containing_block: layout_box
- .positioned_containing_block
- .map(|parent| LayoutOutputBoxId::from_index(parent.index())),
- role: layout_box.kind,
- source_label: layout_box.source_label.clone(),
position: layout_box.style.position(),
coordinate_space: LayoutCoordinateSpaceId::from_index(index + 1),
clip_chain: None,
@@ -263,7 +267,6 @@ where
padding_box,
border_box,
margin_box,
- scroll_extent: LayoutScrollExtentId::from_index(index),
fragments: Vec::new(),
layout_origin_in_document: LayoutPoint::new(layout_x, layout_y),
is_body_element: semantics.is_some_and(|element| element.is_html_element("body")),
@@ -368,12 +371,9 @@ where
LayoutPoint::ZERO
};
self.scroll_extents.push(LayoutScrollExtent {
- id: LayoutScrollExtentId::from_index(index),
- owner: LayoutOutputBoxId::from_index(index),
scrollport,
scrollable_overflow: overflow,
scroll_size,
- requested_offset: requested,
applied_offset: applied,
minimum_offset,
maximum_offset,
@@ -419,23 +419,7 @@ where
Ok(())
}
- fn build_fragments_and_source_mapping(&mut self) {
- for (source, box_id) in &self.world.source_mapping {
- self.source_mapping
- .entry(*source)
- .or_default()
- .principal_box = Some(LayoutOutputBoxId::from_index(box_id.index()));
- }
- for (source, boxes) in &self.world.display_contents_mapping {
- self.source_mapping
- .entry(*source)
- .or_default()
- .scroll_proxy_boxes = boxes
- .iter()
- .map(|id| LayoutOutputBoxId::from_index(id.index()))
- .collect();
- }
-
+ fn build_fragments(&mut self) {
for index in 0..self.world.boxes.len() {
let layout_box = &self.world.boxes[index];
let output_box = LayoutOutputBoxId::from_index(index);
@@ -451,7 +435,6 @@ where
border: self.boxes[index].border_box,
margin: self.boxes[index].margin_box,
}),
- baseline: None,
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -477,7 +460,6 @@ where
},
rect: offset_rect(line.rect, content_origin),
box_model: None,
- baseline: Some(content_origin.y + line.baseline),
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -497,7 +479,6 @@ where
},
rect: box_model.border,
box_model: Some(box_model),
- baseline: None,
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -512,13 +493,11 @@ where
kind: LayoutFragmentKind::Text {
box_id: LayoutOutputBoxId::from_index(target),
line_index: text.line_index,
- source_byte_range: text.source_byte_range.clone(),
source_utf16_range: text.source_utf16_range.clone(),
rtl: text.rtl,
},
rect: offset_rect(text.rect, content_origin),
box_model: None,
- baseline: None,
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -531,19 +510,6 @@ where
fn register_box_fragment(&mut self, box_index: usize, fragment: LayoutFragmentId) {
self.boxes[box_index].fragments.push(fragment);
- let layout_box = &self.world.boxes[box_index];
- let source = layout_box.source.or_else(|| {
- (layout_box.anonymous_reason == Some(LayoutAnonymousReason::InlineSplitContinuation))
- .then_some(layout_box.owner)
- .flatten()
- });
- if let Some(source) = source {
- self.source_mapping
- .entry(source)
- .or_default()
- .fragments
- .push(fragment);
- }
}
fn push_fragment(&mut self, mut fragment: LayoutFragment) -> LayoutFragmentId {
@@ -589,8 +555,6 @@ where
| PaintOrderEvent::PopStackingContext(_) => {}
}
}
- self.hit_entries
- .sort_by_key(|entry| std::cmp::Reverse(entry.paint_order));
}
fn assign_box_clip_metadata(
@@ -644,42 +608,18 @@ where
fragment_id: LayoutFragmentId,
clip_chain: Option,
) {
- let order = u32::try_from(self.paint_order.len())
- .expect("one layout output exceeded the u32 paint-order limit");
- let (box_index, is_text) = match self.fragments[fragment_id.index()].kind {
- LayoutFragmentKind::Box { box_id } => (box_id.index(), false),
- LayoutFragmentKind::InlineBox { box_id, .. } => (box_id.index(), false),
- LayoutFragmentKind::Text { box_id, .. } => (box_id.index(), true),
+ let order = u32::try_from(self.paint_order_count)
+ .expect("one frozen layout tree exceeded the u32 paint-order limit");
+ match self.fragments[fragment_id.index()].kind {
+ LayoutFragmentKind::Box { .. }
+ | LayoutFragmentKind::InlineBox { .. }
+ | LayoutFragmentKind::Text { .. } => {}
LayoutFragmentKind::Line { .. } => return,
- };
+ }
let fragment = &mut self.fragments[fragment_id.index()];
fragment.clip_chain = clip_chain;
fragment.paint_order = Some(order);
- self.paint_order.push(fragment_id);
-
- let layout_box = &self.world.boxes[box_index];
- if !layout_box.style.is_visible() {
- return;
- }
- let source = layout_box.source.or_else(|| {
- (layout_box.pseudo.is_some()
- || layout_box.anonymous_reason
- == Some(LayoutAnonymousReason::InlineSplitContinuation))
- .then_some(layout_box.owner)
- .flatten()
- });
- if let Some(source) = source {
- self.hit_entries.push(LayoutHitTestEntry {
- source,
- fragment: fragment_id,
- coordinate_space: fragment.coordinate_space,
- clip_chain,
- local_rect: fragment.rect,
- paint_order: order,
- is_text,
- pointer_events: layout_box.style.accepts_pointer_events(),
- });
- }
+ self.paint_order_count = self.paint_order_count.saturating_add(1);
}
fn push_clip(
@@ -691,7 +631,6 @@ where
) -> LayoutClipChainId {
let id = LayoutClipChainId::from_index(self.clip_chain.len());
self.clip_chain.push(LayoutClipNode {
- id,
parent,
owner,
coordinate_space,
@@ -718,6 +657,49 @@ where
(self.viewport.css_height as f32).max(overflow.bottom().max(0.0)),
)
}
+
+ fn into_frozen_tree(self, content_size: LayoutSize) -> FrozenLayoutTree {
+ let root_box = LayoutOutputBoxId::from_index(self.world.root.index());
+ let mut coordinate_spaces = self.coordinate_spaces.into_iter();
+ let viewport_coordinate_space = FrozenCoordinateSpace::from(
+ coordinate_spaces
+ .next()
+ .expect("a frozen layout tree always owns the viewport coordinate space"),
+ );
+ let boxes = self
+ .boxes
+ .into_iter()
+ .zip(self.box_sources)
+ .zip(self.principal_sources)
+ .zip(self.hit_sources)
+ .zip(self.scroll_extents)
+ .zip(coordinate_spaces.map(FrozenCoordinateSpace::from))
+ .map(
+ |(
+ ((((geometry, geometry_source), principal_source), hit_source), scroll_extent),
+ coordinate_space,
+ )| FrozenLayoutBox {
+ geometry,
+ scroll_extent,
+ coordinate_space,
+ geometry_source,
+ principal_source,
+ hit_source,
+ },
+ )
+ .collect();
+ FrozenLayoutTree::new(
+ self.viewport,
+ self.viewport_scroll,
+ content_size,
+ root_box,
+ boxes,
+ self.fragments,
+ self.scroll_proxy_links,
+ viewport_coordinate_space,
+ self.clip_chain,
+ )
+ }
}
#[allow(clippy::too_many_arguments)]
diff --git a/moli-layout/tests/phase5_output_contract.rs b/moli-layout/tests/phase5_output_contract.rs
index 7b4cb34bb5..09ddc58ef1 100644
--- a/moli-layout/tests/phase5_output_contract.rs
+++ b/moli-layout/tests/phase5_output_contract.rs
@@ -2,10 +2,10 @@ use std::collections::HashMap;
use moli_layout::{
DocumentLayoutServices, LayoutDisplay, LayoutElementCategory, LayoutElementSemantics,
- LayoutError, LayoutFlushReason, LayoutFragmentKind, LayoutNamespace, LayoutPassOutput,
- LayoutPassRequest, LayoutPoint, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect,
+ LayoutError, LayoutFlushReason, LayoutFragmentKind, LayoutNamespace, LayoutPassRequest,
+ LayoutPassResult, LayoutPoint, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect,
LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D, LayoutViewport,
- PaintColor, ResolvedLayoutStyle, build_layout_pass_output,
+ PaintColor, ResolvedLayoutStyle, build_layout_pass,
};
use style::Atom;
use taffy::{
@@ -126,7 +126,7 @@ fn fixed_size(display: LayoutDisplay, width: f32, height: f32) -> ResolvedLayout
)
}
-fn build(source: &Source, styles: &mut Styles) -> LayoutPassOutput {
+fn build(source: &Source, styles: &mut Styles) -> LayoutPassResult {
build_with_request(
source,
styles,
@@ -138,8 +138,8 @@ fn build_with_request(
source: &Source,
styles: &mut Styles,
request: LayoutPassRequest,
-) -> LayoutPassOutput {
- build_layout_pass_output(source, styles, &mut DocumentLayoutServices::new(), request).unwrap()
+) -> LayoutPassResult {
+ build_layout_pass(source, styles, &mut DocumentLayoutServices::new(), request).unwrap()
}
fn assert_close(actual: f32, expected: f32) {
@@ -178,7 +178,7 @@ fn display_none_root_uses_an_unmapped_internal_carrier() {
}
#[test]
-fn output_owns_complete_box_models_and_answers_a_batch_from_one_pass() {
+fn pass_result_owns_complete_box_models_and_answers_a_batch_from_one_pass() {
let source = Source(vec![Node::element("root", Vec::new())]);
let mut styles = Styles::default();
styles.0.insert(
@@ -257,6 +257,46 @@ fn output_owns_complete_box_models_and_answers_a_batch_from_one_pass() {
));
}
+#[test]
+fn pass_output_freezes_into_the_sole_queryable_retained_tree() {
+ let source = Source(vec![
+ Node::element("root", vec![1]),
+ Node::element("target", Vec::new()),
+ ]);
+ let mut styles = Styles::default();
+ styles
+ .0
+ .insert(0, fixed_size(LayoutDisplay::Block, 200.0, 100.0));
+ styles
+ .0
+ .insert(1, fixed_size(LayoutDisplay::Block, 80.0, 30.0));
+
+ let output = build_with_request(
+ &source,
+ &mut styles,
+ LayoutPassRequest::with_paint(LayoutViewport::new(200, 100, 1.0), LayoutFlushReason::Test),
+ );
+ assert!(output.paint_snapshot().is_some());
+ let metrics = output.metrics;
+ let tree = output.into_tree();
+
+ assert!(tree.source_output(1).is_some());
+ assert_eq!(
+ tree.hit_test(LayoutPoint::new(10.0, 10.0), false)
+ .expect("the frozen tree derives a hit-test view")
+ .source,
+ 1
+ );
+ let answers = tree.answer_queries(
+ &LayoutQueryBatch::new(vec![LayoutQuery::BoxModel { source: 1 }]),
+ metrics,
+ );
+ assert!(matches!(
+ answers.answers.as_slice(),
+ [LayoutQueryAnswer::BoxModel(Some(_))]
+ ));
+}
+
#[test]
fn document_content_size_includes_visible_descendant_end_margin() {
let source = Source(vec![
@@ -377,18 +417,14 @@ fn inline_output_preserves_line_text_and_utf16_source_fragments() {
.filter_map(|id| output.fragment(*id))
.filter_map(|fragment| match &fragment.kind {
LayoutFragmentKind::Text {
- source_byte_range,
- source_utf16_range,
- ..
- } => Some((source_byte_range.clone(), source_utf16_range.clone())),
+ source_utf16_range, ..
+ } => Some(source_utf16_range.clone()),
_ => None,
})
.collect::>();
assert!(!text_fragments.is_empty());
- assert_eq!(text_fragments.first().unwrap().0.start, 0);
- assert_eq!(text_fragments.last().unwrap().0.end, "ab😀cd".len());
- assert_eq!(text_fragments.first().unwrap().1.start, 0);
- assert_eq!(text_fragments.last().unwrap().1.end, 6);
+ assert_eq!(text_fragments.first().unwrap().start, 0);
+ assert_eq!(text_fragments.last().unwrap().end, 6);
let inline_model = output.box_model_for_source(1).unwrap();
assert!(inline_model.border.bounding_rect().width > 0.0);
@@ -553,8 +589,8 @@ fn caret_query_uses_parley_cluster_sides_and_inline_direction() {
let fragment = output
.source_output(1)
.into_iter()
- .flat_map(|source| &source.fragments)
- .filter_map(|id| output.fragment(*id))
+ .flat_map(|source| source.fragments)
+ .filter_map(|id| output.fragment(id))
.find(|fragment| {
matches!(
fragment.kind,
@@ -681,13 +717,8 @@ fn scroll_is_sampled_per_pass_and_updates_geometry_clip_and_hit_testing() {
.insert(2, fixed_size(LayoutDisplay::Block, 300.0, 200.0));
let first = build(&source, &mut styles);
- let scroll_extent = &first.scroll_extents[first
- .source_output(1)
- .unwrap()
- .principal_box
- .unwrap()
- .index()];
- assert_eq!(scroll_extent.requested_offset, LayoutPoint::new(40.0, 30.0));
+ let scroll_box = first.source_output(1).unwrap().principal_box.unwrap();
+ let scroll_extent = first.scroll_extent(scroll_box).unwrap();
assert_eq!(scroll_extent.applied_offset, LayoutPoint::new(40.0, 30.0));
assert_eq!(scroll_extent.minimum_offset, LayoutPoint::ZERO);
assert_eq!(scroll_extent.maximum_offset, LayoutPoint::new(200.0, 120.0));
diff --git a/moli-renderer-v8/src/layout_renderer/mod.rs b/moli-renderer-v8/src/layout_renderer/mod.rs
index 68d3c82a73..1934a8f364 100644
--- a/moli-renderer-v8/src/layout_renderer/mod.rs
+++ b/moli-renderer-v8/src/layout_renderer/mod.rs
@@ -5,8 +5,8 @@ mod style_resolver;
use std::collections::HashMap;
use moli_layout::{
- DocumentLayoutServices, EmbeddedFrameRenderer, LayoutPassOutput, LayoutPassRequest,
- LayoutSource, LayoutViewport, PaintSnapshot, build_layout_pass_output_with_embedded_frames,
+ DocumentLayoutServices, EmbeddedFrameRenderer, LayoutPassRequest, LayoutPassResult,
+ LayoutSource, LayoutViewport, PaintSnapshot, build_layout_pass_with_embedded_frames,
};
use style::values::generics::image::GenericImage;
@@ -105,15 +105,15 @@ fn collect_computed_css_image_urls(
}
}
-pub(crate) fn build_native_layout_pass_output(
+pub(crate) fn build_native_layout_pass(
runtime: &JsContextHost,
root: DomHandle,
services: &mut DocumentLayoutServices,
embedded_document_services: &mut HashMap,
request: LayoutPassRequest,
-) -> Result, moli_layout::LayoutError> {
+) -> Result, moli_layout::LayoutError> {
let mut document_stack = Vec::new();
- build_native_layout_pass_output_recursive(
+ build_native_layout_pass_recursive(
runtime,
root,
services,
@@ -123,14 +123,14 @@ pub(crate) fn build_native_layout_pass_output(
)
}
-fn build_native_layout_pass_output_recursive(
+fn build_native_layout_pass_recursive(
runtime: &JsContextHost,
root: DomHandle,
services: &mut DocumentLayoutServices,
embedded_document_services: &mut HashMap,
request: LayoutPassRequest,
document_stack: &mut Vec,
-) -> Result, moli_layout::LayoutError> {
+) -> Result, moli_layout::LayoutError> {
let document = runtime
.dom_host()
.owner_document_handle(root)
@@ -152,13 +152,7 @@ fn build_native_layout_pass_output_recursive(
document_stack,
embedded_document_services,
};
- build_layout_pass_output_with_embedded_frames(
- &source,
- &mut styles,
- services,
- request,
- &mut frames,
- )
+ build_layout_pass_with_embedded_frames(&source, &mut styles, services, request, &mut frames)
};
document_stack.pop();
result
@@ -202,7 +196,7 @@ impl EmbeddedFrameRenderer for NativeEmbeddedFrameRenderer<'_> {
.unwrap_or_default();
let mut capture = moli_layout::PaintCaptureRequest::viewport();
capture.include_backgrounds = self.include_backgrounds;
- let result = build_native_layout_pass_output_recursive(
+ let result = build_native_layout_pass_recursive(
self.runtime,
root,
&mut services,
diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout.rs b/moli-renderer-v8/src/native_bridge/context_host/layout.rs
index d3f3f80710..7b1c95049f 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/layout.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/layout.rs
@@ -1,7 +1,8 @@
use moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE;
use moli_layout::{
- GeometryProvider, LayoutAnswers, LayoutError, LayoutFlushReason, LayoutPassOutput,
- LayoutPassRequest, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutViewport,
+ FrozenLayoutTree, GeometryProvider, LayoutAnswers, LayoutError, LayoutFlushReason,
+ LayoutPassRequest, LayoutPassResult, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch,
+ LayoutViewport,
};
use super::JsContextHost;
@@ -40,7 +41,7 @@ pub(crate) struct LayoutSnapshotCacheObservability {
pub(crate) hits: u64,
pub(crate) misses: u64,
pub(crate) publishes: u64,
- pub(crate) cached: Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)>,
+ pub(crate) cached: Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)>,
}
impl JsContextHost {
@@ -92,11 +93,11 @@ impl JsContextHost {
)
}
- pub(crate) fn with_fresh_layout_output_for_document(
+ pub(crate) fn with_fresh_layout_pass_for_document(
&self,
document: DomHandle,
request: LayoutPassRequest,
- consume: impl FnOnce(&mut LayoutPassOutput) -> Result,
+ consume: impl FnOnce(&mut LayoutPassResult) -> Result,
) -> Result, LayoutError> {
let Some(root) = self
.dom_host()
@@ -106,7 +107,7 @@ impl JsContextHost {
return Ok(None);
};
let _active = ActiveLayoutPass::enter(&self.layout_pass_active)?;
- let mut output = {
+ let mut pass = {
let mut state = self.document_layout_state.borrow_mut();
state.retain_live_embedded_document_services(|candidate| {
self.child_browsing_context_host_for_document_handle(candidate)
@@ -116,7 +117,7 @@ impl JsContextHost {
document,
self.document_handle(),
|services, embedded_document_services| {
- crate::layout_renderer::build_native_layout_pass_output(
+ crate::layout_renderer::build_native_layout_pass(
self,
root,
services,
@@ -131,14 +132,16 @@ impl JsContextHost {
self.completed_layout_pass_time.set(
self.completed_layout_pass_time
.get()
- .saturating_add(output.metrics.elapsed),
+ .saturating_add(pass.metrics.elapsed),
);
- self.last_layout_pass_metrics.set(Some(output.metrics));
- output.validate_retention_budget()?;
- let consumed = consume(&mut output)?;
+ pass.validate_retention_budget()?;
+ let consumed = consume(&mut pass)?;
+ let metrics = pass.metrics;
+ let tree = pass.into_tree();
self.document_layout_state
.borrow_mut()
- .publish_latest_layout(document, output);
+ .publish_latest_layout(document, tree);
+ self.last_layout_pass_metrics.set(Some(metrics));
self.layout_snapshot_cache_publishes
.set(self.layout_snapshot_cache_publishes.get().saturating_add(1));
Ok(Some(consumed))
@@ -165,16 +168,16 @@ impl JsContextHost {
.is_some()
}
- /// Inspects the latest owned geometry snapshot for one exact Document.
+ /// Inspects the latest frozen layout tree for one exact Document.
///
- /// The callback cannot retain the output or force a refresh. Consumers
+ /// The callback cannot retain the tree or force a refresh. Consumers
/// such as lazy-image admission may combine this sampled geometry with
/// cheap live browser state, but must tolerate the snapshot being absent
/// or stale after DOM/style mutation.
- pub(crate) fn with_latest_layout_output_for_document(
+ pub(crate) fn with_latest_layout_tree_for_document(
&self,
document: DomHandle,
- inspect: impl FnOnce(&LayoutPassOutput) -> T,
+ inspect: impl FnOnce(&FrozenLayoutTree) -> T,
) -> Option {
let state = self.document_layout_state.borrow();
state.latest_layout(document).map(inspect)
@@ -193,9 +196,11 @@ impl JsContextHost {
let reuse_latest = true;
let cached = if reuse_latest {
let state = self.document_layout_state.borrow();
- state
- .latest_layout(document)
- .map(|output| self.answer_layout_queries(output, viewport, queries))
+ state.latest_layout(document).and_then(|tree| {
+ self.last_layout_pass_metrics
+ .get()
+ .map(|metrics| self.answer_layout_queries(tree, metrics, viewport, queries))
+ })
} else {
None
};
@@ -207,21 +212,22 @@ impl JsContextHost {
self.layout_snapshot_cache_misses
.set(self.layout_snapshot_cache_misses.get().saturating_add(1));
- self.with_fresh_layout_output_for_document(
+ self.with_fresh_layout_pass_for_document(
document,
LayoutPassRequest::new(viewport, reason),
- |output| Ok(self.answer_layout_queries(output, viewport, queries)),
+ |pass| Ok(self.answer_layout_queries(&pass.tree, pass.metrics, viewport, queries)),
)?
.ok_or(LayoutError::NoLayoutRoot)
}
fn answer_layout_queries(
&self,
- output: &LayoutPassOutput,
+ tree: &FrozenLayoutTree,
+ metrics: moli_layout::LayoutPassMetrics,
viewport: LayoutViewport,
queries: &LayoutQueryBatch,
) -> LayoutAnswers {
- let mut answers = output.answer_queries(queries);
+ let mut answers = tree.answer_queries(queries, metrics);
for (query, answer) in queries.queries.iter().zip(&mut answers.answers) {
match (query, answer) {
(LayoutQuery::DocumentMetrics, LayoutQueryAnswer::DocumentMetrics(metrics)) => {
@@ -235,7 +241,7 @@ impl JsContextHost {
LayoutQuery::ElementMetrics { source },
LayoutQueryAnswer::ElementMetrics(metrics),
) => {
- *metrics = output.element_metrics_for_source_with_offset_parent_filter(
+ *metrics = tree.element_metrics_for_source_with_offset_parent_filter(
*source,
|candidate| self.offset_parent_candidate_is_exposed(*source, candidate),
);
diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout_snapshot.rs b/moli-renderer-v8/src/native_bridge/context_host/layout_snapshot.rs
index f1bd0a706f..391e371279 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/layout_snapshot.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/layout_snapshot.rs
@@ -1,41 +1,34 @@
-use moli_layout::LayoutPassOutput;
+use moli_layout::FrozenLayoutTree;
use crate::document_runtime::DomHandle;
-struct LatestLayoutSnapshot {
+struct LatestFrozenLayout {
document: DomHandle,
- output: Box>,
+ tree: Box>,
}
-/// Single-slot storage for the latest successful owned layout projection.
+/// Single-slot storage for the latest successful frozen layout tree.
///
-/// This deliberately owns no layout world, Taffy cache, style borrow, paint
-/// snapshot, timer, freshness stamp, or invalidation policy. The context-host
-/// orchestration decides when to read, publish, or clear the slot.
+/// 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.
#[derive(Default)]
-pub(super) struct LatestLayoutSnapshotCache {
- latest: Option,
+pub(super) struct LatestLayoutTreeCache {
+ latest: Option,
}
-impl LatestLayoutSnapshotCache {
- pub(super) fn get(&self, document: DomHandle) -> Option<&LayoutPassOutput> {
+impl LatestLayoutTreeCache {
+ pub(super) fn get(&self, document: DomHandle) -> Option<&FrozenLayoutTree> {
self.latest
.as_ref()
.filter(|snapshot| snapshot.document == document)
- .map(|snapshot| snapshot.output.as_ref())
+ .map(|snapshot| snapshot.tree.as_ref())
}
- pub(super) fn publish(&mut self, document: DomHandle, mut output: LayoutPassOutput) {
- debug_assert!(
- output.paint_snapshot().is_none(),
- "a retained layout output must not own paint resources"
- );
- if output.paint_snapshot().is_some() {
- drop(output.take_paint_snapshot());
- }
- self.latest = Some(LatestLayoutSnapshot {
+ pub(super) fn publish(&mut self, document: DomHandle, tree: FrozenLayoutTree) {
+ self.latest = Some(LatestFrozenLayout {
document,
- output: Box::new(output),
+ tree: Box::new(tree),
});
}
@@ -46,13 +39,9 @@ impl LatestLayoutSnapshotCache {
#[cfg(test)]
pub(super) fn observability(
&self,
- ) -> Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)> {
- self.latest.as_ref().map(|snapshot| {
- (
- snapshot.document,
- snapshot.output.retention_metrics(),
- snapshot.output.paint_snapshot().is_some(),
- )
- })
+ ) -> Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)> {
+ self.latest
+ .as_ref()
+ .map(|snapshot| (snapshot.document, snapshot.tree.retention_metrics()))
}
}
diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs
index d8cc7a869a..3d71557554 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs
@@ -1,8 +1,8 @@
use std::collections::HashMap;
-use moli_layout::{DocumentLayoutServices, LayoutPassOutput};
+use moli_layout::{DocumentLayoutServices, FrozenLayoutTree};
-use super::layout_snapshot::LatestLayoutSnapshotCache;
+use super::layout_snapshot::LatestLayoutTreeCache;
use crate::{
css_resource_urls::{CompletedStylesheetWebFont, StylesheetLoadBlockingResource},
document_runtime::DomHandle,
@@ -13,18 +13,18 @@ use crate::{
///
/// `ScriptVm` outlives `document.open()`, so the main-document owner
/// transition replaces this value explicitly. Full layout passes borrow the
-/// services and discard every pass-local tree/cache. Only the latest owned
-/// geometry projection and document-owned font/text sidecars survive a pass.
+/// services and discard every working tree/cache. Only the latest frozen
+/// 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 output.
+/// document tree; its identity is stored alongside the tree.
pub(super) struct DocumentLayoutState {
services: DocumentLayoutServices,
embedded_document_services: HashMap,
web_fonts: DocumentWebFontState,
web_font_sources_dirty: bool,
- latest_layout: LatestLayoutSnapshotCache,
+ latest_layout: LatestLayoutTreeCache,
}
impl Default for DocumentLayoutState {
@@ -34,7 +34,7 @@ impl Default for DocumentLayoutState {
embedded_document_services: HashMap::new(),
web_fonts: DocumentWebFontState::default(),
web_font_sources_dirty: true,
- latest_layout: LatestLayoutSnapshotCache::default(),
+ latest_layout: LatestLayoutTreeCache::default(),
}
}
}
@@ -84,16 +84,16 @@ impl DocumentLayoutState {
pub(super) fn latest_layout(
&self,
document: DomHandle,
- ) -> Option<&LayoutPassOutput> {
+ ) -> Option<&FrozenLayoutTree> {
self.latest_layout.get(document)
}
pub(super) fn publish_latest_layout(
&mut self,
document: DomHandle,
- output: LayoutPassOutput,
+ tree: FrozenLayoutTree,
) {
- self.latest_layout.publish(document, output);
+ self.latest_layout.publish(document, tree);
}
pub(super) fn clear_latest_layout(&mut self) {
@@ -103,7 +103,7 @@ impl DocumentLayoutState {
#[cfg(test)]
pub(super) fn latest_layout_observability(
&self,
- ) -> Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)> {
+ ) -> Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)> {
self.latest_layout.observability()
}
diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/layout.rs b/moli-renderer-v8/src/native_bridge/element/geometry/layout.rs
index e78019319b..86f21e4ab7 100644
--- a/moli-renderer-v8/src/native_bridge/element/geometry/layout.rs
+++ b/moli-renderer-v8/src/native_bridge/element/geometry/layout.rs
@@ -80,7 +80,7 @@ fn default_child_frame_client_rect() -> ClientRect {
// Legacy geometry policy for explicit LayoutPolicy::Mock:
//
// OnDemand consumers must enter the unified GeometryProvider, which answers
-// from the latest owned output or performs a cold refresh, and must never call
+// from the latest frozen tree or performs a cold refresh, and must never call
// these helpers. Mock remains deterministic, cheap, and side-effect free for
// the default CLI policy without `--layout`:
//
diff --git a/moli-renderer-v8/src/native_bridge/element/images/lazy.rs b/moli-renderer-v8/src/native_bridge/element/images/lazy.rs
index 3b084e66a7..c17ff29da1 100644
--- a/moli-renderer-v8/src/native_bridge/element/images/lazy.rs
+++ b/moli-renderer-v8/src/native_bridge/element/images/lazy.rs
@@ -2,11 +2,11 @@
//!
//! Lazy loading is a resource decision, not observable geometry. It must not
//! force layout or retain a layout world. The renderer instead inspects its
-//! one latest owned [`LayoutPassOutput`] and combines that sampled geometry
+//! one latest owned [`FrozenLayoutTree`] and combines that sampled geometry
//! with live scroll offsets. A later fresh screenshot or screencast frame
//! naturally replaces the sample.
-use moli_layout::{LayoutPassOutput, LayoutPosition, LayoutRect};
+use moli_layout::{FrozenLayoutTree, LayoutPosition, LayoutRect};
use crate::{document_runtime::DomHandle, dom::native::Node, native_bridge::JsContextHost};
@@ -54,7 +54,7 @@ pub(super) fn image_load_is_deferred(runtime: &JsContextHost, handle: DomHandle)
pub(super) fn revealed_lazy_image_handles(
runtime: &JsContextHost,
document: DomHandle,
- output: &LayoutPassOutput,
+ output: &FrozenLayoutTree,
) -> Vec {
(0..runtime.dom_host().dom().nodes().len())
.map(DomHandle::new)
@@ -85,7 +85,7 @@ fn is_unadmitted_lazy_image(
fn image_is_near_live_viewport(
runtime: &JsContextHost,
document: DomHandle,
- output: &LayoutPassOutput,
+ output: &FrozenLayoutTree,
handle: DomHandle,
) -> bool {
let Some(geometry) = output.intersection_geometry(handle, None) else {
@@ -122,7 +122,7 @@ fn image_is_near_live_viewport(
fn live_scroll_translation(
runtime: &JsContextHost,
- output: &LayoutPassOutput,
+ output: &FrozenLayoutTree,
handle: DomHandle,
) -> (f32, f32) {
let Some(geometry) = output.scroll_into_view_geometry_for_source(handle) else {
diff --git a/moli-renderer-v8/src/native_bridge/element/images/src.rs b/moli-renderer-v8/src/native_bridge/element/images/src.rs
index c4eafaa235..96c964965e 100644
--- a/moli-renderer-v8/src/native_bridge/element/images/src.rs
+++ b/moli-renderer-v8/src/native_bridge/element/images/src.rs
@@ -74,7 +74,7 @@ pub(crate) fn queue_revealed_lazy_image_loads(
let handles = {
let runtime = unsafe { &*runtime_ptr };
runtime
- .with_latest_layout_output_for_document(document, |output| {
+ .with_latest_layout_tree_for_document(document, |output| {
revealed_lazy_image_handles(runtime, document, output)
})
.unwrap_or_default()
diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs
index 022f4d6297..1e319ff1de 100644
--- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs
+++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs
@@ -100,7 +100,7 @@ getComputedStyle(document.getElementById('fallback')).display
}
#[tokio::test(flavor = "current_thread")]
-async fn geometry_batch_reuses_latest_output_until_fresh_paint_replaces_it() {
+async fn geometry_batch_reuses_latest_tree_until_fresh_paint_replaces_it() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
@@ -257,16 +257,12 @@ document.body.innerHTML = '
<
assert_eq!(cache_after_screenshot.0, cache_before.0 + 1);
assert_eq!(cache_after_screenshot.1, cache_before.1 + 1);
assert_eq!(cache_after_screenshot.2, cache_before.2 + 2);
- let (_, retention, retains_paint) = cache_after_screenshot
+ let (_, retention) = cache_after_screenshot
.3
- .expect("fresh paint layout should publish its geometry output");
+ .expect("fresh paint layout should publish its frozen tree");
assert!(retention.box_count > 0);
assert!(retention.fragment_count > 0);
assert!(retention.estimated_geometry_bytes > 0);
- assert!(
- !retains_paint,
- "the cached output must not retain paint resources"
- );
let screenshot_metrics = after_screenshot.3.expect("screenshot layout metrics");
assert_eq!(
screenshot_metrics.reason,
@@ -354,7 +350,7 @@ document.body.innerHTML = '
';
.vm()
.layout_snapshot_cache_observability_for_test();
assert_eq!(cache_after_first.2, cache_before.2 + 1);
- assert!(cache_after_first.3.is_some_and(|(_, _, paint)| !paint));
+ assert!(cache_after_first.3.is_some());
page_vm
.vm_mut()
@@ -394,7 +390,7 @@ document.body.innerHTML = '
';
assert_eq!(cache_after_second.0, cache_before.0 + 1);
assert_eq!(cache_after_second.1, cache_before.1);
assert_eq!(cache_after_second.2, cache_before.2 + 2);
- assert!(cache_after_second.3.is_some_and(|(_, _, paint)| !paint));
+ assert!(cache_after_second.3.is_some());
let refreshed = moli_layout::GeometryProvider::answer(
page_vm.vm_mut(),
diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs
index cb5303985f..bb82f74482 100644
--- a/moli-renderer-v8/src/script_vm.rs
+++ b/moli-renderer-v8/src/script_vm.rs
@@ -2379,7 +2379,7 @@ impl ScriptVm {
&mut self,
viewport: moli_layout::LayoutViewport,
) -> Result {
- self.with_fresh_layout_output(
+ self.with_fresh_layout_pass(
moli_layout::LayoutPassRequest::new(viewport, moli_layout::LayoutFlushReason::Test),
|_| Ok(()),
)
@@ -2405,18 +2405,18 @@ impl ScriptVm {
reason: moli_layout::LayoutFlushReason,
capture: moli_layout::PaintCaptureRequest,
) -> anyhow::Result> {
- self.with_fresh_layout_output(
+ self.with_fresh_layout_pass(
moli_layout::LayoutPassRequest::with_capture(viewport, reason, capture),
- moli_layout::LayoutPassOutput::take_paint_snapshot,
+ moli_layout::LayoutPassResult::take_paint_snapshot,
)
.map_err(anyhow::Error::new)
}
- fn with_fresh_layout_output(
+ fn with_fresh_layout_pass(
&mut self,
request: moli_layout::LayoutPassRequest,
consume: impl FnOnce(
- &mut moli_layout::LayoutPassOutput,
+ &mut moli_layout::LayoutPassResult,
) -> Result,
) -> Result, moli_layout::LayoutError> {
// Font/CSS source reconciliation is a pre-pass lifecycle step. Once
@@ -2430,7 +2430,7 @@ impl ScriptVm {
let context_host = self._context_host.borrow();
let document = context_host.document_handle();
let result =
- context_host.with_fresh_layout_output_for_document(document, request, consume);
+ context_host.with_fresh_layout_pass_for_document(document, request, consume);
(document, result)
};
if matches!(&result, Ok(Some(_)))
@@ -2498,7 +2498,7 @@ impl ScriptVm {
u64,
u64,
u64,
- Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)>,
+ Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)>,
) {
let observability = self
._context_host
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs
index 0fe7d32d83..2874f96beb 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs
@@ -8023,7 +8023,7 @@ fn inner_text_new_sources_wait_for_a_fresh_paint_layout() {
)
.expect("the warm innerText read should evaluate"),
"a",
- "a text source absent from the latest layout output remains unrendered until refresh"
+ "a text source absent from the latest frozen layout tree remains unrendered until refresh"
);
assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 1);
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs
index 09c983764c..68804f9911 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs
@@ -1482,7 +1482,7 @@ fn switching_to_mock_geometry_drops_the_latest_real_layout_snapshot() {
vm.layout_snapshot_cache_observability_for_test()
.3
.is_none(),
- "Mock policy must not retain a real layout output"
+ "Mock policy must not retain a real frozen layout tree"
);
let passes_before_mock = vm.layout_pass_observability_for_test().1;
vm.eval("document.body.offsetTop")