azul_layout/solver3/layout_tree.rs
1//! Layout tree construction from a styled DOM, including anonymous box generation
2use std::{
3 collections::{BTreeMap, HashMap},
4 hash::{Hash, Hasher},
5 sync::{
6 atomic::{AtomicU32, Ordering},
7 Arc,
8 },
9};
10
11use azul_core::diff::NodeDataFingerprint;
12
13use crate::text3::cache::UnifiedConstraints;
14
15/// Global counter for IFC IDs. Resets to 0 when layout() callback is invoked.
16static IFC_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
17
18/// Unique identifier for an Inline Formatting Context (IFC).
19///
20/// An IFC represents a region where inline content (text, inline-blocks, images)
21/// is laid out together. One IFC can contain content from multiple DOM nodes
22/// (e.g., `<p>Hello <span>world</span>!</p>` is one IFC with 3 text runs).
23///
24/// The ID is generated using a global atomic counter that resets at the start
25/// of each layout pass. This ensures:
26/// - IDs are unique within a layout pass
27/// - The same logical IFC gets the same ID across frames (for selection stability)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub struct IfcId(pub u32);
30
31impl IfcId {
32 /// Generate a new unique IFC ID.
33 pub fn unique() -> Self {
34 Self(IFC_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
35 }
36
37 /// Reset the IFC ID counter. Called at the start of each layout pass.
38 pub fn reset_counter() {
39 IFC_ID_COUNTER.store(0, Ordering::Relaxed);
40 }
41}
42
43/// Tracks a layout node's membership in an Inline Formatting Context.
44///
45/// Text nodes don't store their own `inline_layout_result` - instead, they
46/// participate in their parent's IFC. This struct provides the link from
47/// a text node back to its IFC's layout data.
48///
49/// # Architecture
50///
51/// ```text
52/// DOM: <p>Hello <span>world</span>!</p>
53///
54/// Layout Tree:
55/// ├── LayoutNode (p) - IFC root
56/// │ └── inline_layout_result: Some(UnifiedLayout)
57/// │ └── ifc_id: IfcId(5)
58/// │
59/// ├── LayoutNode (::text "Hello ")
60/// │ └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 0 })
61/// │
62/// ├── LayoutNode (span)
63/// │ └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
64/// │ └── LayoutNode (::text "world")
65/// │ └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
66/// │
67/// └── LayoutNode (::text "!")
68/// └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 2 })
69/// ```
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct IfcMembership {
72 /// The IFC ID this node's content was laid out in.
73 pub ifc_id: IfcId,
74 /// The index of the IFC root LayoutNode in the layout tree.
75 /// Used to quickly find the node with `inline_layout_result`.
76 pub ifc_root_layout_index: usize,
77 /// Which run index within the IFC corresponds to this node's text.
78 /// Maps to `ContentIndex::run_index` in the shaped items.
79 pub run_index: u32,
80}
81
82use azul_core::{
83 dom::{FormattingContext, NodeData, NodeId, NodeType},
84 geom::{LogicalPosition, LogicalRect, LogicalSize},
85 styled_dom::StyledDom,
86};
87use azul_css::{
88 corety::LayoutDebugMessage,
89 css::CssPropertyValue,
90 format_rust_code::GetHash,
91 props::{
92 basic::{
93 pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
94 },
95 layout::{
96 LayoutDisplay, LayoutFloat, LayoutHeight, LayoutMaxHeight, LayoutMaxWidth,
97 LayoutMinHeight, LayoutMinWidth, LayoutOverflow, LayoutPosition, LayoutWidth,
98 LayoutWritingMode,
99 },
100 property::{CssProperty, CssPropertyType},
101 style::{StyleTextAlign, StyleWhiteSpace},
102 },
103};
104use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
105
106#[cfg(feature = "text_layout")]
107use crate::text3;
108use crate::{
109 debug_log,
110 font::parsed::ParsedFont,
111 font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
112 solver3::{
113 geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
114 getters::{
115 get_css_height, get_css_max_height, get_css_max_width, get_css_min_height,
116 get_css_min_width, get_css_width, get_direction_property as get_direction,
117 get_display_property, get_float, get_overflow_x,
118 get_overflow_y, get_position, get_text_align,
119 get_text_orientation_property as get_text_orientation,
120 get_white_space_property, get_writing_mode, MultiValue,
121 },
122 scrollbar::ScrollbarRequirements,
123 LayoutContext, Result,
124 },
125 text3::cache::AvailableSpace,
126};
127
128/// Represents the invalidation state of a layout node.
129///
130/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
131/// A node marked for `Layout` does not also need to be marked for `Paint`.
132///
133/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
134///
135/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
136/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
137/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
139pub enum DirtyFlag {
140 /// The node's layout is valid and no repaint is needed. This is the "clean" state.
141 #[default]
142 None,
143 /// The node's geometry is valid, but its appearance (e.g., color) has changed.
144 /// Requires a display list update only.
145 Paint,
146 /// The node's geometry (size or position) is invalid.
147 /// Requires a full layout pass and a display list update.
148 Layout,
149}
150
151/// A hash that represents the content and style of a node PLUS all of its descendants.
152/// If two SubtreeHashes are equal, their entire subtrees are considered identical for layout
153/// purposes.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
155pub struct SubtreeHash(pub u64);
156
157/// Per-item metrics cached from the last IFC layout.
158///
159/// These metrics enable incremental IFC relayout (Phase 2 optimization):
160/// when a single inline item changes, we can check whether its advance width
161/// changed and potentially skip full line-breaking for unaffected lines.
162///
163/// Index in `CachedInlineLayout::item_metrics` matches the item order in
164/// `UnifiedLayout::items`.
165#[derive(Debug, Clone)]
166pub struct InlineItemMetrics {
167 /// The DOM NodeId of the source node for this item (for dirty checking).
168 /// `None` for generated content (list markers, hyphens, etc.)
169 pub source_node_id: Option<NodeId>,
170 /// Advance width of this item (glyph run width, inline-block width, etc.)
171 pub advance_width: f32,
172 /// Advance height contribution from this item to its line box.
173 pub line_height_contribution: f32,
174 /// Whether this item can participate in line breaking.
175 /// `false` for items inside `white-space: nowrap` or `white-space: pre`.
176 pub can_break: bool,
177 /// Which line this item was placed on (0-indexed).
178 pub line_index: u32,
179 /// X offset within its line.
180 pub x_offset: f32,
181}
182
183/// Cached inline layout result with the constraints used to compute it.
184///
185/// This structure solves a fundamental architectural problem: inline layouts
186/// (text wrapping, inline-block positioning) depend on the available width.
187/// Different layout phases may compute the layout with different widths:
188///
189/// 1. **Min-content measurement**: width = MinContent (effectively 0)
190/// 2. **Max-content measurement**: width = MaxContent (effectively infinite)
191/// 3. **Final layout**: width = Definite(actual_column_width)
192///
193/// Without tracking which constraints were used, a cached result from phase 1
194/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
195/// positions (the root cause of table cell width bugs).
196///
197/// By storing the constraints alongside the result, we can:
198/// - Invalidate the cache when constraints change
199/// - Keep multiple cached results for different constraint types if needed
200/// - Ensure the final render always uses a layout computed with correct widths
201#[derive(Debug, Clone)]
202pub struct CachedInlineLayout {
203 /// The computed inline layout
204 pub layout: Arc<UnifiedLayout>,
205 /// The available width constraint used to compute this layout.
206 /// This is the key for cache validity checking.
207 /// +spec:writing-modes:1dcba2 - "available width" (CSS2.1) = auto size in inline axis
208 pub available_width: AvailableSpace,
209 /// Whether this layout was computed with float exclusions.
210 /// Float-aware layouts should not be overwritten by non-float layouts.
211 pub has_floats: bool,
212 /// The full constraints used to compute this layout.
213 /// Used for quick relayout after text edits without rebuilding from CSS.
214 pub constraints: Option<UnifiedConstraints>,
215 /// Per-item metrics for incremental IFC relayout (Phase 2).
216 ///
217 /// Each entry corresponds to one `PositionedItem` in `layout.items`.
218 /// These metrics enable the IFC relayout decision tree:
219 /// - Check if a dirty node's advance_width changed → skip repositioning if not
220 /// - Use `can_break` + `line_index` for the nowrap fast path
221 /// - Use `x_offset` for shifting subsequent items without full line-breaking
222 pub item_metrics: Vec<InlineItemMetrics>,
223 /// Cached line break boundaries for incremental relayout.
224 /// Enables checking if a width change fits on the same line without
225 /// re-running the full line-breaking algorithm.
226 pub line_breaks: Option<crate::text3::cache::CachedLineBreaks>,
227}
228
229impl CachedInlineLayout {
230 /// Creates a new cached inline layout.
231 pub fn new(
232 layout: Arc<UnifiedLayout>,
233 available_width: AvailableSpace,
234 has_floats: bool,
235 ) -> Self {
236 let item_metrics = Self::extract_item_metrics(&layout);
237 Self {
238 layout,
239 available_width,
240 has_floats,
241 constraints: None,
242 item_metrics,
243 line_breaks: None,
244 }
245 }
246
247 /// Creates a new cached inline layout with full constraints.
248 pub fn new_with_constraints(
249 layout: Arc<UnifiedLayout>,
250 available_width: AvailableSpace,
251 has_floats: bool,
252 constraints: UnifiedConstraints,
253 ) -> Self {
254 let item_metrics = Self::extract_item_metrics(&layout);
255 let available_width_px = match available_width {
256 AvailableSpace::Definite(w) => w,
257 _ => f32::MAX,
258 };
259 let line_breaks = Some(crate::text3::cache::extract_line_breaks(
260 &layout.items, available_width_px,
261 ));
262 Self {
263 layout,
264 available_width,
265 has_floats,
266 constraints: Some(constraints),
267 item_metrics,
268 line_breaks,
269 }
270 }
271
272 /// Extracts per-item metrics from a computed `UnifiedLayout`.
273 ///
274 /// This is called automatically by the constructors. The metrics
275 /// enable incremental IFC relayout in Phase 2c/2d by providing
276 /// cached advance widths, line assignments, and break information
277 /// for each positioned item.
278 fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
279 use crate::text3::cache::{ShapedItem, get_item_vertical_metrics_approx};
280
281 layout.items.iter().map(|positioned_item| {
282 let bounds = positioned_item.item.bounds();
283 let (ascent, descent) = get_item_vertical_metrics_approx(&positioned_item.item);
284
285 let source_node_id = match &positioned_item.item {
286 ShapedItem::Cluster(c) => c.source_node_id,
287 // Objects (inline-blocks, images) and other generated items
288 // don't expose source_node_id directly on ShapedItem.
289 // Phase 2c will refine this via the ContentIndex mapping.
290 ShapedItem::Object { .. }
291 | ShapedItem::CombinedBlock { .. }
292 | ShapedItem::Tab { .. }
293 | ShapedItem::Break { .. } => None,
294 };
295
296 // For Phase 2a, default can_break = true for all items.
297 // Phase 2c will refine this by checking the white-space property
298 // on the IFC root's style or the item's own style context.
299 // (Note: text3::StyleProperties doesn't carry white-space;
300 // that's resolved at the IFC/BFC boundary level.)
301 let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
302
303 InlineItemMetrics {
304 source_node_id,
305 advance_width: bounds.width,
306 line_height_contribution: ascent + descent,
307 can_break,
308 line_index: positioned_item.line_index as u32,
309 x_offset: positioned_item.position.x,
310 }
311 }).collect()
312 }
313
314 /// Checks if this cached layout is valid for the given constraints.
315 ///
316 /// A cached layout is valid if:
317 /// 1. The available width matches (definite widths must be equal, or both are the same
318 /// indefinite type)
319 /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
320 ///
321 /// The second condition preserves float-aware layouts, which are more "correct" than
322 /// non-float layouts and shouldn't be overwritten.
323 pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
324 // If we have a float-aware layout and the new request doesn't have floats,
325 // keep the float-aware layout (it's more accurate)
326 if self.has_floats && !new_has_floats {
327 // But only if the width constraint type matches
328 return self.width_constraint_matches(new_width);
329 }
330
331 // Otherwise, require exact width match
332 self.width_constraint_matches(new_width)
333 }
334
335 /// Tolerance for comparing definite layout widths (in logical pixels).
336 /// Sub-pixel differences below this threshold are treated as identical
337 /// to avoid unnecessary relayout from floating-point rounding.
338 const LAYOUT_WIDTH_EPSILON: f32 = 0.1;
339
340 /// Checks if the width constraint matches.
341 fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
342 match (self.available_width, new_width) {
343 // Definite widths must match within a small epsilon
344 (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
345 (old - new).abs() < Self::LAYOUT_WIDTH_EPSILON
346 }
347 // MinContent matches MinContent
348 (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
349 // MaxContent matches MaxContent
350 (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
351 // Different constraint types don't match
352 _ => false,
353 }
354 }
355
356 /// Determines if this cached layout should be replaced by a new layout.
357 ///
358 /// Returns true if the new layout should replace this one.
359 pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
360 // Always replace if we gain float information
361 if new_has_floats && !self.has_floats {
362 return true;
363 }
364
365 // Replace if width constraint changed
366 !self.width_constraint_matches(new_width)
367 }
368
369 /// Returns a reference to the inner UnifiedLayout.
370 ///
371 /// This is a convenience method for code that only needs the layout data
372 /// and doesn't care about the caching metadata.
373 #[inline]
374 pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
375 &self.layout
376 }
377
378 /// Returns a clone of the inner Arc<UnifiedLayout>.
379 ///
380 /// This is useful for APIs that need to return an owned reference
381 /// to the layout without exposing the caching metadata.
382 #[inline]
383 pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
384 self.layout.clone()
385 }
386}
387
388/// A layout tree node representing the CSS box model.
389///
390/// ## Memory Layout Optimization (`#[repr(C)]`)
391///
392/// Fields are ordered by access frequency (hottest first) to maximize CPU
393/// cache line utilization during tree traversal. With `#[repr(C)]`, the
394/// compiler preserves this ordering. The 6 hottest fields (~140 bytes)
395/// occupy the first 2-3 cache lines (64 bytes each), which are loaded
396/// first by the hardware prefetcher.
397///
398/// | Tier | Fields | ~Bytes | Accesses |
399/// |--------|-----------------------------------------|--------|----------|
400/// | HOT | box_props, dom_node_id, children, | ~140 | 410+ |
401/// | | used_size, formatting_context, parent | | |
402/// | WARM | intrinsic_sizes..computed_style | ~220 | ~80 |
403/// | COLD | dirty_flag..is_anonymous | ~190 | ~20 |
404///
405/// Note: An absolute position is a final paint-time value and shouldn't be
406/// cached on the node itself, as it can change even if the node's
407/// layout is clean (e.g., if a sibling changes size). We will calculate
408/// it in a separate map.
409#[derive(Debug, Clone)]
410#[repr(C)]
411pub struct LayoutNode {
412 // ── HOT tier: accessed on every node in every layout pass ────────────
413 // These fields should fit in the first 2-3 cache lines (~128-192 bytes).
414
415 /// The resolved box model properties (margin, border, padding)
416 /// in logical pixels. Cached after first resolution.
417 /// (148 accesses — hottest field)
418 pub box_props: BoxProps,
419 /// Reference back to the original DOM node (None for anonymous boxes)
420 /// (111 accesses)
421 pub dom_node_id: Option<NodeId>,
422 /// Children indices in the layout tree
423 /// (53 accesses)
424 pub children: Vec<usize>,
425 /// The size used during the last layout pass.
426 /// (43 accesses)
427 pub used_size: Option<LogicalSize>,
428 /// The formatting context this node establishes or participates in.
429 /// (30 accesses)
430 pub formatting_context: FormattingContext,
431 /// Parent index (None for root)
432 /// (25 accesses)
433 pub parent: Option<usize>,
434
435 // ── WARM tier: frequently accessed but not on every node ─────────────
436
437 /// Cached intrinsic sizes (min-content, max-content, etc.)
438 /// (16 accesses — sizing pass only)
439 pub intrinsic_sizes: Option<IntrinsicSizes>,
440 // +spec:display-property:af3a89 - alignment baseline for inline-level boxes
441 /// The baseline of this box, if applicable, measured from its content-box top edge.
442 /// (14 accesses — IFC/table alignment)
443 pub baseline: Option<f32>,
444 /// Cached inline layout result with the constraints used to compute it.
445 ///
446 /// This field stores both the computed layout AND the constraints (available width,
447 /// float state) under which it was computed. This is essential for correctness:
448 ///
449 /// - Table cells are measured multiple times with different widths
450 /// - Min-content/max-content intrinsic sizing uses special constraint values
451 /// - The final layout must use the actual available width, not a measurement width
452 ///
453 /// By tracking the constraints, we avoid the bug where a min-content measurement
454 /// (with width=0) would be incorrectly reused for final rendering.
455 /// (13 accesses — IFC roots / table cells)
456 pub inline_layout_result: Option<CachedInlineLayout>,
457 /// Cached scrollbar information (calculated during layout)
458 /// Used to determine if scrollbars appeared/disappeared requiring reflow
459 /// (12 accesses — scrollable containers only)
460 pub scrollbar_info: Option<ScrollbarRequirements>,
461 /// The position of this node *relative to its parent's content box*.
462 /// (9 accesses — positioning pass)
463 pub relative_position: Option<LogicalPosition>,
464 /// The actual content size (children overflow size) for scrollable containers.
465 /// This is the size of all content that might need to be scrolled, which can
466 /// be larger than `used_size` when content overflows the container.
467 /// (7 accesses — scrollable containers)
468 pub overflow_content_size: Option<LogicalSize>,
469 /// Cache for Taffy layout computations for this node.
470 /// (6 accesses — Taffy bridge)
471 pub taffy_cache: TaffyCache,
472 /// Pre-computed CSS properties needed during layout.
473 /// Computed once during layout tree build to avoid repeated style lookups.
474 /// (5 accesses — cache.rs only)
475 pub computed_style: ComputedLayoutStyle,
476 /// Pseudo-element type (::marker, ::before, ::after) if this node is a pseudo-element
477 /// (5 accesses — pseudo-elements only)
478 pub pseudo_element: Option<PseudoElement>,
479 /// Escaped top margin (CSS 2.1 margin collapsing)
480 /// If this BFC's first child's top margin "escaped" the BFC, this contains
481 /// the collapsed margin that should be applied by the parent.
482 /// (4 accesses — BFC margin collapsing)
483 pub escaped_top_margin: Option<f32>,
484 /// Escaped bottom margin (CSS 2.1 margin collapsing)
485 /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
486 /// the collapsed margin that should be applied by the parent.
487 /// (4 accesses)
488 pub escaped_bottom_margin: Option<f32>,
489 /// Parent's formatting context (needed to determine if stretch applies)
490 /// (4 accesses — flex/grid children)
491 pub parent_formatting_context: Option<FormattingContext>,
492 /// If this node participates in an IFC (is inline content like text),
493 /// stores the reference back to the IFC root and the run index.
494 /// This allows text nodes to find their layout data in the parent's IFC.
495 /// (3 accesses — text nodes only)
496 pub ifc_membership: Option<IfcMembership>,
497 /// The layout tree index of this node's containing block.
498 /// - For abs-pos elements: nearest positioned (non-static) ancestor
499 /// - For fixed elements: root / None (viewport)
500 /// - For normal-flow: parent (None = implicit)
501 /// Used for clip exemption: abs-pos elements whose containing block
502 /// is above an overflow clipper should not be clipped.
503 pub containing_block_index: Option<usize>,
504
505 // ── COLD tier: construction / reconciliation / debugging only ────────
506
507 /// Type of anonymous box (if applicable)
508 /// (2 accesses)
509 pub anonymous_type: Option<AnonymousBoxType>,
510 /// Multi-field fingerprint of this node's data (style, text, etc.)
511 /// for granular change detection during reconciliation.
512 /// (2 accesses — reconciliation only)
513 pub node_data_fingerprint: NodeDataFingerprint,
514 /// A hash of this node's data and all of its descendants. Used for
515 /// fast reconciliation.
516 /// (9 accesses — all in cache.rs reconciliation)
517 pub subtree_hash: SubtreeHash,
518 /// Dirty flags to track what needs recalculation.
519 /// (7 accesses — reconciliation setup)
520 pub dirty_flag: DirtyFlag,
521 /// Unresolved box model properties (raw CSS values).
522 /// These are resolved lazily during layout when containing block is known.
523 /// (1 access — initial resolution only)
524 pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
525 /// If this node is an IFC root, stores the IFC ID.
526 /// Used to identify which IFC this node's `inline_layout_result` belongs to.
527 /// (1 access — IFC creation only)
528 pub ifc_id: Option<IfcId>,
529}
530
531/// Pre-computed CSS properties needed during layout.
532///
533/// This struct stores resolved CSS values that are frequently accessed during
534/// layout calculations. By computing these once during layout tree construction,
535/// we avoid O(n * m) style lookups where n = nodes and m = layout passes.
536///
537/// All values are resolved to their final form (no 'inherit', 'initial', etc.)
538#[derive(Debug, Clone, Default)]
539pub struct ComputedLayoutStyle {
540 /// CSS `display` property
541 pub display: LayoutDisplay,
542 /// CSS `position` property
543 pub position: LayoutPosition,
544 /// CSS `float` property
545 pub float: LayoutFloat,
546 /// CSS `overflow-x` property
547 pub overflow_x: LayoutOverflow,
548 /// CSS `overflow-y` property
549 pub overflow_y: LayoutOverflow,
550 /// CSS `writing-mode` property
551 pub writing_mode: azul_css::props::layout::LayoutWritingMode,
552 /// CSS `direction` property (ltr/rtl)
553 pub direction: azul_css::props::style::StyleDirection,
554 /// CSS `text-orientation` property (for vertical writing modes)
555 pub text_orientation: azul_css::props::style::effects::StyleTextOrientation,
556 /// CSS `width` property (None = auto)
557 pub width: Option<azul_css::props::layout::LayoutWidth>,
558 /// CSS `height` property (None = auto)
559 pub height: Option<azul_css::props::layout::LayoutHeight>,
560 /// CSS `min-width` property
561 pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
562 /// CSS `min-height` property
563 pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
564 /// CSS `max-width` property
565 pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
566 /// CSS `max-height` property
567 pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
568 /// CSS `text-align` property
569 pub text_align: azul_css::props::style::StyleTextAlign,
570}
571
572// Note: LayoutNode methods that cross hot/warm/cold boundaries have been
573// moved to LayoutTree methods (resolve_box_props, get_content_size).
574
575/// CSS pseudo-elements that can be generated
576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
577pub enum PseudoElement {
578 /// ::marker pseudo-element for list items
579 Marker,
580 /// ::before pseudo-element
581 Before,
582 /// ::after pseudo-element
583 After,
584}
585
586// +spec:display-property:b7f4bf - anonymous inline/block boxes are both called "anonymous boxes"
587/// Types of anonymous boxes that can be generated
588// +spec:display-property:ae4f16 - anonymous boxes are treated as descendants alongside pseudo-elements
589#[derive(Debug, Clone, Copy, PartialEq)]
590pub enum AnonymousBoxType {
591 /// Anonymous block box wrapping inline content
592 InlineWrapper,
593 /// Anonymous box for a list item marker (bullet or number)
594 /// DEPRECATED: Use PseudoElement::Marker instead
595 ListItemMarker,
596 /// Anonymous table wrapper
597 TableWrapper,
598 /// Anonymous table row group (tbody)
599 TableRowGroup,
600 /// Anonymous table row
601 TableRow,
602 /// Anonymous table cell
603 TableCell,
604}
605
606// =============================================================================
607// SoA (struct-of-arrays) layout node split for cache performance
608// =============================================================================
609
610/// Hot layout node fields — accessed on every node in every layout pass.
611///
612/// Stored in a separate `Vec` for cache locality. At ~100 bytes per node,
613/// 1000 nodes fit in ~100 KB (L2 cache), vs ~550 KB with the monolithic struct.
614#[derive(Debug, Clone)]
615pub struct LayoutNodeHot {
616 /// The resolved box model properties (margin, border, padding)
617 /// Stored in packed i16×10 encoding to reduce cache footprint.
618 /// Use `box_props.unpack()` to get f32 `ResolvedBoxProps` for computation.
619 pub box_props: crate::solver3::geometry::PackedBoxProps,
620 /// Reference back to the original DOM node (None for anonymous boxes)
621 pub dom_node_id: Option<NodeId>,
622 /// The size used during the last layout pass.
623 pub used_size: Option<LogicalSize>,
624 /// The formatting context this node establishes or participates in.
625 pub formatting_context: FormattingContext,
626 /// Parent index (None for root)
627 pub parent: Option<usize>,
628}
629
630/// Warm layout node fields — accessed frequently but not on every node.
631///
632/// Stored in a separate `Vec`. These fields are accessed during specific
633/// layout phases (sizing, IFC, table alignment) but not during the main
634/// constraint-solving loop.
635#[derive(Debug, Clone, Default)]
636pub struct LayoutNodeWarm {
637 /// Cached intrinsic sizes (min-content, max-content, etc.)
638 pub intrinsic_sizes: Option<IntrinsicSizes>,
639 /// The baseline of this box, measured from its content-box top edge.
640 pub baseline: Option<f32>,
641 /// Cached inline layout result with the constraints used to compute it.
642 pub inline_layout_result: Option<CachedInlineLayout>,
643 /// Cached scrollbar information
644 pub scrollbar_info: Option<ScrollbarRequirements>,
645 /// The position relative to parent's content box.
646 pub relative_position: Option<LogicalPosition>,
647 /// The actual content size for scrollable containers.
648 pub overflow_content_size: Option<LogicalSize>,
649 /// Cache for Taffy layout computations.
650 pub taffy_cache: TaffyCache,
651 /// Pre-computed CSS properties needed during layout.
652 pub computed_style: ComputedLayoutStyle,
653 /// Pseudo-element type if this node is a pseudo-element
654 pub pseudo_element: Option<PseudoElement>,
655 /// Escaped top margin (CSS 2.1 margin collapsing)
656 pub escaped_top_margin: Option<f32>,
657 /// Escaped bottom margin (CSS 2.1 margin collapsing)
658 pub escaped_bottom_margin: Option<f32>,
659 /// Parent's formatting context
660 pub parent_formatting_context: Option<FormattingContext>,
661 /// IFC membership for text nodes
662 pub ifc_membership: Option<IfcMembership>,
663 /// Containing block index for clip exemption
664 pub containing_block_index: Option<usize>,
665}
666
667/// Cold layout node fields — construction / reconciliation / debugging only.
668///
669/// Stored in a separate `Vec`. These fields are rarely accessed during layout;
670/// mostly used during tree construction, reconciliation, and dirty tracking.
671#[derive(Debug, Clone)]
672pub struct LayoutNodeCold {
673 /// Type of anonymous box (if applicable)
674 pub anonymous_type: Option<AnonymousBoxType>,
675 /// Multi-field fingerprint for granular change detection.
676 pub node_data_fingerprint: NodeDataFingerprint,
677 /// Hash of this node's data + all descendants.
678 pub subtree_hash: SubtreeHash,
679 /// Dirty flags for recalculation tracking.
680 pub dirty_flag: DirtyFlag,
681 /// Unresolved box model properties (raw CSS values).
682 pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
683 /// IFC ID if this node is an IFC root.
684 pub ifc_id: Option<IfcId>,
685}
686
687impl Default for LayoutNodeCold {
688 fn default() -> Self {
689 Self {
690 anonymous_type: None,
691 node_data_fingerprint: NodeDataFingerprint::default(),
692 subtree_hash: SubtreeHash::default(),
693 dirty_flag: DirtyFlag::default(),
694 unresolved_box_props: Default::default(),
695 ifc_id: None,
696 }
697 }
698}
699
700impl LayoutNode {
701 /// Split this full layout node into hot/warm/cold components.
702 /// Used during `LayoutTreeBuilder::build()` to create the SoA layout.
703 pub fn split(self) -> (LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold) {
704 (
705 LayoutNodeHot {
706 box_props: crate::solver3::geometry::PackedBoxProps::pack(&self.box_props),
707 dom_node_id: self.dom_node_id,
708 used_size: self.used_size,
709 formatting_context: self.formatting_context,
710 parent: self.parent,
711 },
712 LayoutNodeWarm {
713 intrinsic_sizes: self.intrinsic_sizes,
714 baseline: self.baseline,
715 inline_layout_result: self.inline_layout_result,
716 scrollbar_info: self.scrollbar_info,
717 relative_position: self.relative_position,
718 overflow_content_size: self.overflow_content_size,
719 taffy_cache: self.taffy_cache,
720 computed_style: self.computed_style,
721 pseudo_element: self.pseudo_element,
722 escaped_top_margin: self.escaped_top_margin,
723 escaped_bottom_margin: self.escaped_bottom_margin,
724 parent_formatting_context: self.parent_formatting_context,
725 ifc_membership: self.ifc_membership,
726 containing_block_index: self.containing_block_index,
727 },
728 LayoutNodeCold {
729 anonymous_type: self.anonymous_type,
730 node_data_fingerprint: self.node_data_fingerprint,
731 subtree_hash: self.subtree_hash,
732 dirty_flag: self.dirty_flag,
733 unresolved_box_props: self.unresolved_box_props,
734 ifc_id: self.ifc_id,
735 },
736 )
737 }
738}
739
740/// The complete layout tree structure.
741///
742/// Uses a struct-of-arrays (SoA) layout for cache performance:
743/// - `nodes` (hot): accessed on every node in every layout pass
744/// - `warm`: accessed during specific layout phases
745/// - `cold`: construction / reconciliation only
746#[derive(Debug, Clone)]
747pub struct LayoutTree {
748 /// Hot layout data — box props, parent, used_size, formatting context
749 pub nodes: Vec<LayoutNodeHot>,
750 /// Warm layout data — intrinsic sizes, baseline, inline layout, etc.
751 pub warm: Vec<LayoutNodeWarm>,
752 /// Cold layout data — dirty flags, fingerprints, reconciliation data
753 pub cold: Vec<LayoutNodeCold>,
754 /// Root node index
755 pub root: usize,
756 /// Mapping from DOM node IDs to layout node indices
757 pub dom_to_layout: HashMap<NodeId, Vec<usize>>,
758 /// Flat arena holding all children indices contiguously.
759 pub children_arena: Vec<usize>,
760 /// Per-node (start, len) into `children_arena`. Indexed by node index.
761 pub children_offsets: Vec<(u32, u32)>,
762 /// Per-node bit: this node or any descendant establishes a shrink-to-fit
763 /// (STF) context whose sizing algorithm reads children's intrinsic sizes
764 /// (flex/grid/table/inline-block containers, floats, or abspos elements).
765 ///
766 /// If `subtree_needs_intrinsic[i]` is false AND no ancestor of `i` is STF
767 /// either, the intrinsic sizing pass can skip the entire subtree — nothing
768 /// will ever read those values. This is the static-DOM optimization from
769 /// §58 Win #3 (the "safely re-enabled Fix C").
770 ///
771 /// Computed once at tree build time in `generate_layout_tree`. An empty
772 /// vec means "assume every subtree needs intrinsics" (safe fallback for
773 /// code paths that construct `LayoutTree` without going through the
774 /// builder — currently none, but preserves the invariant for tests).
775 pub subtree_needs_intrinsic: Vec<bool>,
776}
777
778/// Approximate per-field heap-byte breakdown of a [`LayoutTree`].
779#[derive(Debug, Clone, Default)]
780pub struct LayoutTreeMemoryReport {
781 pub node_count: usize,
782 pub hot_bytes: usize,
783 pub warm_bytes: usize,
784 pub warm_inline_layout_bytes: usize,
785 pub warm_taffy_cache_bytes: usize,
786 pub cold_bytes: usize,
787 pub dom_to_layout_bytes: usize,
788 pub children_arena_bytes: usize,
789 pub children_offsets_bytes: usize,
790}
791
792impl LayoutTreeMemoryReport {
793 pub fn total_bytes(&self) -> usize {
794 self.hot_bytes
795 + self.warm_bytes
796 + self.warm_inline_layout_bytes
797 + self.warm_taffy_cache_bytes
798 + self.cold_bytes
799 + self.dom_to_layout_bytes
800 + self.children_arena_bytes
801 + self.children_offsets_bytes
802 }
803}
804
805impl LayoutTree {
806 /// Approximate heap bytes retained by this LayoutTree.
807 pub fn memory_report(&self) -> LayoutTreeMemoryReport {
808 let mut report = LayoutTreeMemoryReport {
809 node_count: self.nodes.len(),
810 hot_bytes: self.nodes.capacity() * core::mem::size_of::<LayoutNodeHot>(),
811 warm_bytes: self.warm.capacity() * core::mem::size_of::<LayoutNodeWarm>(),
812 cold_bytes: self.cold.capacity() * core::mem::size_of::<LayoutNodeCold>(),
813 children_arena_bytes: self.children_arena.capacity() * core::mem::size_of::<usize>(),
814 children_offsets_bytes: self.children_offsets.capacity() * core::mem::size_of::<(u32, u32)>(),
815 dom_to_layout_bytes: 0,
816 warm_inline_layout_bytes: 0,
817 warm_taffy_cache_bytes: 0,
818 };
819 // HashMap<NodeId, Vec<usize>> — approximate: (key + Vec-header) per entry
820 // plus heap for each inner Vec.
821 let entries = self.dom_to_layout.len();
822 report.dom_to_layout_bytes = entries * (core::mem::size_of::<NodeId>() + core::mem::size_of::<Vec<usize>>());
823 for v in self.dom_to_layout.values() {
824 report.dom_to_layout_bytes += v.capacity() * core::mem::size_of::<usize>();
825 }
826 // Inline layout data lives behind Arc — count Arc heap-shares once
827 // per node that has a cached layout. Counted conservatively.
828 for w in &self.warm {
829 if let Some(cached) = &w.inline_layout_result {
830 // Arc<UnifiedLayout> — count the UnifiedLayout header + its items.
831 report.warm_inline_layout_bytes += core::mem::size_of::<crate::text3::cache::UnifiedLayout>();
832 report.warm_inline_layout_bytes += cached.layout.items.capacity()
833 * core::mem::size_of::<crate::text3::cache::PositionedItem>();
834 report.warm_inline_layout_bytes += cached.item_metrics.capacity()
835 * core::mem::size_of::<InlineItemMetrics>();
836 // Glyph bytes inside ShapedItem::Cluster — unbounded but bounded
837 // per entry. Approximate by counting clusters × 32 bytes/glyph.
838 for item in cached.layout.items.iter() {
839 if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
840 report.warm_inline_layout_bytes += c.glyphs.capacity()
841 * core::mem::size_of::<crate::text3::cache::ShapedGlyph>();
842 report.warm_inline_layout_bytes += c.text.capacity();
843 }
844 }
845 }
846 // Taffy cache — each slot is an Option, ~50 B empty
847 report.warm_taffy_cache_bytes += core::mem::size_of::<TaffyCache>();
848 }
849 report
850 }
851
852 /// Returns the children of node `index` as a contiguous slice from the arena.
853 #[inline]
854 pub fn children(&self, index: usize) -> &[usize] {
855 if let Some(&(start, len)) = self.children_offsets.get(index) {
856 &self.children_arena[(start as usize)..((start as usize) + (len as usize))]
857 } else {
858 &[]
859 }
860 }
861
862 /// Get hot layout data for a node (box_props, dom_node_id, used_size, etc.)
863 #[inline]
864 pub fn get(&self, index: usize) -> Option<&LayoutNodeHot> {
865 self.nodes.get(index)
866 }
867
868 /// Get mutable hot layout data for a node.
869 #[inline]
870 pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNodeHot> {
871 self.nodes.get_mut(index)
872 }
873
874 /// Get warm layout data for a node (intrinsic_sizes, baseline, inline_layout, etc.)
875 #[inline]
876 pub fn warm(&self, index: usize) -> Option<&LayoutNodeWarm> {
877 self.warm.get(index)
878 }
879
880 /// Get mutable warm layout data for a node.
881 #[inline]
882 pub fn warm_mut(&mut self, index: usize) -> Option<&mut LayoutNodeWarm> {
883 self.warm.get_mut(index)
884 }
885
886 /// Get cold layout data for a node (dirty_flag, subtree_hash, fingerprint, etc.)
887 #[inline]
888 pub fn cold(&self, index: usize) -> Option<&LayoutNodeCold> {
889 self.cold.get(index)
890 }
891
892 /// Get mutable cold layout data for a node.
893 #[inline]
894 pub fn cold_mut(&mut self, index: usize) -> Option<&mut LayoutNodeCold> {
895 self.cold.get_mut(index)
896 }
897
898 pub fn root_node(&self) -> &LayoutNodeHot {
899 &self.nodes[self.root]
900 }
901
902 /// Reconstruct a full `LayoutNode` from the split hot/warm/cold arrays.
903 ///
904 /// Used when passing node data to `LayoutTreeBuilder::clone_node_from_old()`.
905 pub fn get_full_node(&self, index: usize) -> Option<LayoutNode> {
906 let hot = self.nodes.get(index)?;
907 let warm = self.warm.get(index).cloned().unwrap_or_default();
908 let cold = self.cold.get(index).cloned().unwrap_or_default();
909 let children = self.children(index).to_vec();
910 Some(LayoutNode {
911 box_props: hot.box_props.unpack(),
912 dom_node_id: hot.dom_node_id,
913 children,
914 used_size: hot.used_size,
915 formatting_context: hot.formatting_context.clone(),
916 parent: hot.parent,
917 intrinsic_sizes: warm.intrinsic_sizes,
918 baseline: warm.baseline,
919 inline_layout_result: warm.inline_layout_result,
920 scrollbar_info: warm.scrollbar_info,
921 relative_position: warm.relative_position,
922 overflow_content_size: warm.overflow_content_size,
923 taffy_cache: warm.taffy_cache,
924 computed_style: warm.computed_style,
925 pseudo_element: warm.pseudo_element,
926 escaped_top_margin: warm.escaped_top_margin,
927 escaped_bottom_margin: warm.escaped_bottom_margin,
928 parent_formatting_context: warm.parent_formatting_context,
929 ifc_membership: warm.ifc_membership,
930 containing_block_index: warm.containing_block_index,
931 anonymous_type: cold.anonymous_type,
932 node_data_fingerprint: cold.node_data_fingerprint,
933 subtree_hash: cold.subtree_hash,
934 dirty_flag: cold.dirty_flag,
935 unresolved_box_props: cold.unresolved_box_props,
936 ifc_id: cold.ifc_id,
937 })
938 }
939
940 /// Re-resolve box properties for a node with the actual containing block size.
941 pub fn resolve_box_props(
942 &mut self,
943 node_index: usize,
944 containing_block: LogicalSize,
945 viewport_size: LogicalSize,
946 element_font_size: f32,
947 root_font_size: f32,
948 ) {
949 let params = crate::solver3::geometry::ResolutionParams {
950 containing_block,
951 viewport_size,
952 element_font_size,
953 root_font_size,
954 };
955 if let (Some(hot), Some(cold)) = (self.nodes.get_mut(node_index), self.cold.get(node_index)) {
956 hot.box_props = crate::solver3::geometry::PackedBoxProps::pack(&cold.unresolved_box_props.resolve(¶ms));
957 }
958 }
959
960 /// Marks a node and its ancestors as dirty with the given flag.
961 pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
962 if flag == DirtyFlag::None {
963 return;
964 }
965
966 let mut current_index = Some(start_index);
967 while let Some(index) = current_index {
968 let cold = match self.cold.get_mut(index) {
969 Some(c) => c,
970 None => break,
971 };
972 if cold.dirty_flag >= flag {
973 break;
974 }
975 cold.dirty_flag = flag;
976 current_index = self.nodes.get(index).and_then(|n| n.parent);
977 }
978 }
979
980 /// Marks a node and its entire subtree of descendants with the given dirty flag.
981 pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
982 if flag == DirtyFlag::None {
983 return;
984 }
985
986 let mut stack = vec![start_index];
987 while let Some(index) = stack.pop() {
988 let children = self.children(index).to_vec();
989 if let Some(cold) = self.cold.get_mut(index) {
990 if cold.dirty_flag < flag {
991 cold.dirty_flag = flag;
992 }
993 stack.extend_from_slice(&children);
994 }
995 }
996 }
997
998 /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
999 pub fn clear_all_dirty_flags(&mut self) {
1000 for cold in &mut self.cold {
1001 cold.dirty_flag = DirtyFlag::None;
1002 }
1003 }
1004
1005 /// Get inline layout for a node, navigating through IFC membership if needed.
1006 pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
1007 let warm = self.warm.get(layout_index)?;
1008
1009 // First, check if this node has its own inline_layout_result (it's an IFC root)
1010 if let Some(cached) = &warm.inline_layout_result {
1011 return Some(cached.get_layout());
1012 }
1013
1014 // For text nodes, check if they have ifc_membership pointing to the IFC root
1015 if let Some(ifc_membership) = &warm.ifc_membership {
1016 let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
1017 if let Some(cached) = &ifc_root_warm.inline_layout_result {
1018 return Some(cached.get_layout());
1019 }
1020 }
1021
1022 None
1023 }
1024
1025 /// Get the content size of a node (for scrollbar calculations).
1026 pub fn get_content_size(&self, index: usize) -> LogicalSize {
1027 let warm = match self.warm.get(index) {
1028 Some(w) => w,
1029 None => return LogicalSize::default(),
1030 };
1031
1032 if let Some(content_size) = warm.overflow_content_size {
1033 return content_size;
1034 }
1035
1036 let hot = match self.nodes.get(index) {
1037 Some(h) => h,
1038 None => return LogicalSize::default(),
1039 };
1040
1041 let mut content_size = hot.used_size.unwrap_or_default();
1042
1043 if let Some(ref cached_layout) = warm.inline_layout_result {
1044 let text_layout = &cached_layout.layout;
1045 let mut max_x: f32 = 0.0;
1046 let mut max_y: f32 = 0.0;
1047 for positioned_item in &text_layout.items {
1048 let item_bounds = positioned_item.item.bounds();
1049 max_x = max_x.max(positioned_item.position.x + item_bounds.width);
1050 max_y = max_y.max(positioned_item.position.y + item_bounds.height);
1051 }
1052 content_size.width = content_size.width.max(max_x);
1053 content_size.height = content_size.height.max(max_y);
1054 }
1055
1056 content_size
1057 }
1058}
1059
1060/// Generate layout tree from styled DOM with proper anonymous box generation
1061pub fn generate_layout_tree<T: ParsedFontTrait>(
1062 ctx: &mut LayoutContext<'_, T>,
1063) -> Result<LayoutTree> {
1064 let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
1065 let root_id = ctx
1066 .styled_dom
1067 .root
1068 .into_crate_internal()
1069 .unwrap_or(NodeId::ZERO);
1070 let root_index =
1071 builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
1072 let mut layout_tree = builder.build(root_index);
1073
1074 // Pre-compute the STF (shrink-to-fit) subtree bitmap. This is static-DOM
1075 // information: whether a subtree establishes any shrink-to-fit context
1076 // depends only on the DOM structure + formatting context, both of which
1077 // are frozen from here until the next layout-tree rebuild. The intrinsic
1078 // sizing pass reads this to skip subtrees whose intrinsics are never
1079 // consumed (§58 Win #3).
1080 layout_tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(ctx.styled_dom, &layout_tree);
1081
1082 debug_log!(
1083 ctx,
1084 "Generated layout tree with {} nodes (incl. anonymous)",
1085 layout_tree.nodes.len()
1086 );
1087
1088 Ok(layout_tree)
1089}
1090
1091/// Returns true if `(dom_node_id, fc)` establishes a formatting context whose
1092/// sizing algorithm reads children's intrinsic sizes. Covers:
1093/// - flex containers (flex item sizing uses child min/max-content),
1094/// - grid containers (grid-track sizing likewise),
1095/// - tables and table cells,
1096/// - inline-block (its own width may be shrink-to-fit),
1097/// - floats and abspos elements (their `auto` width resolves to shrink-to-fit).
1098///
1099/// A `FormattingContext::Block` with a definite CSS width is NOT shrink-to-fit —
1100/// its inner layout gets the width top-down, so descendant intrinsics don't
1101/// feed back up. That's the path Fix C short-circuits.
1102pub(crate) fn is_shrink_to_fit_context(
1103 styled_dom: &StyledDom,
1104 dom_node_id: Option<NodeId>,
1105 fc: &FormattingContext,
1106) -> bool {
1107 use crate::solver3::getters::{get_float, MultiValue};
1108 use crate::solver3::positioning::get_position_type;
1109 use azul_css::props::layout::{LayoutFloat, LayoutPosition};
1110
1111 match fc {
1112 FormattingContext::Flex
1113 | FormattingContext::Grid
1114 | FormattingContext::Table
1115 | FormattingContext::InlineBlock => return true,
1116 _ => {}
1117 }
1118 let Some(dom_id) = dom_node_id else { return false; };
1119 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1120 let float_val = match get_float(styled_dom, dom_id, node_state) {
1121 MultiValue::Exact(v) => v,
1122 _ => LayoutFloat::None,
1123 };
1124 if float_val != LayoutFloat::None {
1125 return true;
1126 }
1127 let pos = get_position_type(styled_dom, Some(dom_id));
1128 if pos == LayoutPosition::Absolute || pos == LayoutPosition::Fixed {
1129 // Abspos only becomes shrink-to-fit when width is `auto`.
1130 // Being conservative: treat as STF whenever abspos so we still
1131 // compute intrinsics for the auto-width case. Misses no work.
1132 return true;
1133 }
1134 false
1135}
1136
1137/// Per-node bitmap of "this node or any descendant establishes a shrink-to-fit
1138/// context." Post-order walk: `out[i] = self_stf(i) || any(out[child_of_i])`.
1139/// Layout tree nodes are built top-down (pre-order), so iterating from the end
1140/// visits children before parents.
1141fn compute_subtree_needs_intrinsic(
1142 styled_dom: &StyledDom,
1143 tree: &LayoutTree,
1144) -> Vec<bool> {
1145 let n = tree.nodes.len();
1146 let mut out = vec![false; n];
1147 for idx in (0..n).rev() {
1148 let hot = &tree.nodes[idx];
1149 let self_stf = is_shrink_to_fit_context(styled_dom, hot.dom_node_id, &hot.formatting_context);
1150 let mut any = self_stf;
1151 if !any {
1152 for &child in tree.children(idx) {
1153 if out.get(child).copied().unwrap_or(false) {
1154 any = true;
1155 break;
1156 }
1157 }
1158 }
1159 out[idx] = any;
1160 }
1161 out
1162}
1163
1164/// Incrementally builds a [`LayoutTree`] from a [`StyledDom`].
1165///
1166/// Usage: create via [`LayoutTreeBuilder::new`], call [`process_node`](Self::process_node)
1167/// on the root DOM node, then call [`build`](Self::build) to produce the final
1168/// SoA-split `LayoutTree`. During `process_node`, anonymous boxes are generated
1169/// as required by CSS 2.2 §9.2.1.1 (inline wrappers) and §17.2.1 (table fixup).
1170pub struct LayoutTreeBuilder {
1171 nodes: Vec<LayoutNode>,
1172 dom_to_layout: HashMap<NodeId, Vec<usize>>,
1173 viewport_size: LogicalSize,
1174}
1175
1176impl LayoutTreeBuilder {
1177 pub fn new(viewport_size: LogicalSize) -> Self {
1178 Self {
1179 nodes: Vec::new(),
1180 dom_to_layout: HashMap::new(),
1181 viewport_size,
1182 }
1183 }
1184
1185 pub fn get(&self, index: usize) -> Option<&LayoutNode> {
1186 self.nodes.get(index)
1187 }
1188
1189 pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
1190 self.nodes.get_mut(index)
1191 }
1192
1193 // +spec:display-property:2188b7 - builds box tree: each element's principal box is child of nearest ancestor's principal box, with anonymous boxes for tables/inline wrapping
1194 /// Main entry point for recursively building the layout tree.
1195 /// This function dispatches to specialized handlers based on the node's
1196 /// `display` property to correctly generate anonymous boxes.
1197 pub fn process_node(
1198 &mut self,
1199 styled_dom: &StyledDom,
1200 dom_id: NodeId,
1201 parent_idx: Option<usize>,
1202 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1203 ) -> Result<usize> {
1204 let node_data = &styled_dom.node_data.as_container()[dom_id];
1205 let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages);
1206 let raw_display = get_display_type(styled_dom, dom_id);
1207
1208 // +spec:display-property:042f56 - replaced elements with layout-internal display use inline
1209 // CSS Display 3 §2.4: "When the display property of a replaced element computes to
1210 // one of the layout-internal values, it is handled as having a used value of inline."
1211 let raw_display = if raw_display.is_layout_internal() && is_replaced_element(node_data) {
1212 LayoutDisplay::Inline
1213 } else {
1214 raw_display
1215 };
1216
1217 // +spec:display-property:0b40af - display/position/float interaction per CSS 2.2 §9.7
1218 // +spec:display-property:ba53ba - float!=none or position!=static causes display to blockify
1219 // +spec:positioning:69468c - absolute/fixed blockifies the box, float computes to none
1220 // +spec:table-layout:cfc60a - CSS 2.2 §9.7: display/position/float interaction
1221 // Blockification rules (CSS Display 3 §2.7 / §2.8):
1222 // 1. Root element → blockify
1223 // 2. position:absolute or position:fixed → float computes to 'none', blockify
1224 // 3. float is not 'none' → blockify
1225 // 4. Flex/Grid children → blockify
1226 let node_position = self.nodes.get(node_idx).map(|n| n.computed_style.position).unwrap_or_default();
1227 let node_float = self.nodes.get(node_idx).map(|n| n.computed_style.float).unwrap_or_default();
1228 let is_absolute_or_fixed = matches!(node_position, LayoutPosition::Absolute | LayoutPosition::Fixed);
1229 let is_floated = node_float != LayoutFloat::None;
1230 let is_root = parent_idx.is_none();
1231
1232 // Per CSS 2.2 §9.7: if position is absolute or fixed, float computes to 'none'
1233 if is_absolute_or_fixed && is_floated {
1234 if let Some(node) = self.nodes.get_mut(node_idx) {
1235 node.computed_style.float = LayoutFloat::None;
1236 }
1237 }
1238
1239 let is_flex_grid_child = parent_idx
1240 .and_then(|p| self.nodes.get(p).map(|n| matches!(n.formatting_context, FormattingContext::Flex | FormattingContext::Grid)))
1241 .unwrap_or(false);
1242
1243 let display_type = crate::solver3::getters::get_computed_display(
1244 raw_display, is_absolute_or_fixed, is_floated, is_root, is_flex_grid_child,
1245 );
1246
1247 // If blockification changed the display type, update the node's formatting context
1248 if display_type != raw_display {
1249 if let Some(node) = self.nodes.get_mut(node_idx) {
1250 node.computed_style.display = display_type;
1251 node.formatting_context = determine_formatting_context_for_display(
1252 styled_dom, dom_id, display_type,
1253 );
1254 }
1255 }
1256
1257 // Compute containing block index for abs-pos clip exemption
1258 if is_absolute_or_fixed {
1259 let cb_index = if matches!(node_position, LayoutPosition::Fixed) {
1260 // Fixed elements: containing block is the root (viewport)
1261 None
1262 } else {
1263 // Absolute elements: containing block is nearest positioned ancestor
1264 let mut ancestor = parent_idx;
1265 loop {
1266 match ancestor {
1267 Some(idx) => {
1268 let pos = self.nodes.get(idx)
1269 .map(|n| n.computed_style.position)
1270 .unwrap_or_default();
1271 if pos.is_positioned() {
1272 break Some(idx);
1273 }
1274 ancestor = self.nodes.get(idx).and_then(|n| n.parent);
1275 }
1276 None => break None, // root
1277 }
1278 }
1279 };
1280 if let Some(node) = self.nodes.get_mut(node_idx) {
1281 node.containing_block_index = cb_index;
1282 }
1283 }
1284
1285 if parent_idx.is_none() {
1286 if let Some(node) = self.nodes.get_mut(node_idx) {
1287 if let FormattingContext::Block { ref mut establishes_new_context } = node.formatting_context {
1288 *establishes_new_context = true;
1289 }
1290 }
1291 }
1292
1293 // +spec:display-property:1f4039 - list-item generates ::marker pseudo-element + principal box
1294 // +spec:display-property:2bb592 - list-item generates ::marker pseudo-element with list-style content
1295 // +spec:display-property:3b507e - list-item generates ::marker pseudo-element
1296 // +spec:display-property:a48f00 - additional boxes (marker, table wrapper) placed w.r.t. principal box
1297 // +spec:display-property:998063 - list-item generates principal block box + marker box
1298 // If this is a list-item, inject a ::marker pseudo-element as its first child
1299 // +spec:display-property:a42905 - list-item generates ::marker pseudo-element with list-style content, principal box outer=block inner=flow
1300 if display_type == LayoutDisplay::ListItem {
1301 self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
1302 }
1303
1304 // +spec:display-contents:376f2e - display:contents removes principal box, children render normally
1305 // +spec:display-contents:3c7066 - display:contents strips element from formatting tree, hoists children
1306 // +spec:display-contents:3f4884 - replaced elements / form controls not specially handled yet (spec note: use display:none instead)
1307 // +spec:display-contents:4f9129 - semantic container role preserved: children promoted but DOM structure unchanged
1308 // +spec:display-contents:7558e8 - display:contents is rendering-time only; DOM relationships unaffected
1309 // +spec:display-contents:a079e3 - display:contents generates no box; children promoted to nearest non-contents ancestor (writing-mode parent lookup skips these)
1310 // +spec:display-contents:e202d5 - display:contents removes principal box, children render as normal
1311 // +spec:display-contents:6bbdf4 - display:contents preserves semantic container role (visibility context)
1312 // +spec:display-property:d7a8de - display:none/contents elements generate no box; anonymous box generation ignores them
1313 // +spec:display-property:dc2132 - display:none and display:contents control box generation
1314 // display:contents - element generates no box; promote children to parent
1315 // +spec:display-contents:61992e - element itself generates no boxes, children promoted to parent
1316 // +spec:display-contents:af8feb - treated as if replaced in element tree by its contents
1317 // +spec:display-contents:353e71 - display:contents box generation behavior
1318 // +spec:display-contents:b0a76b - display:contents generates no box; children promoted to parent
1319 // +spec:display-property:e370af - display:contents generates no box; children promoted to parent
1320 //
1321 // +spec:display-contents:852a59 - display:contents computes to display:none for replaced elements
1322 // +spec:display-contents:4a524e - display:contents computes to display:none on replaced elements
1323 // +spec:replaced-elements:af1e68 - display:contents on replaced elements has no effect (element renders normally)
1324 // Per CSS Display 3 §2.5 / Appendix B: replaced elements (img, canvas, embed, object,
1325 // audio, iframe, video, input, textarea, select, br, wbr, meter, progress)
1326 // and similar cannot be "un-boxed" — display:contents becomes display:none.
1327 if display_type == LayoutDisplay::Contents && is_replaced_element(node_data) {
1328 // Treat as display:none — remove node from parent and skip children
1329 if let Some(parent) = parent_idx {
1330 if let Some(p) = self.nodes.get_mut(parent) {
1331 p.children.retain(|&c| c != node_idx);
1332 }
1333 }
1334 if let Some(node) = self.nodes.get_mut(node_idx) {
1335 node.computed_style.display = LayoutDisplay::None;
1336 node.formatting_context = FormattingContext::None;
1337 }
1338 return Ok(node_idx);
1339 }
1340
1341 if display_type == LayoutDisplay::Contents {
1342 // Remove the node we just created — it shouldn't generate a box
1343 if let Some(parent) = parent_idx {
1344 if let Some(p) = self.nodes.get_mut(parent) {
1345 p.children.retain(|&c| c != node_idx);
1346 }
1347 }
1348 // Process children as if they belong to the parent (or root if no parent)
1349 let effective_parent = parent_idx.unwrap_or(node_idx);
1350 for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1351 self.process_node(styled_dom, child_dom_id, Some(effective_parent), debug_messages)?;
1352 }
1353 return Ok(node_idx);
1354 }
1355
1356 match display_type {
1357 LayoutDisplay::Block
1358 | LayoutDisplay::InlineBlock
1359 | LayoutDisplay::FlowRoot
1360 | LayoutDisplay::ListItem => {
1361 self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?
1362 }
1363 // +spec:table-layout:d52e09 - display:table/inline-table cause element to behave like a table element
1364 // +spec:table-layout:360da0 - table display values cause table formatting behavior
1365 LayoutDisplay::Table | LayoutDisplay::InlineTable => {
1366 self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?
1367 }
1368 LayoutDisplay::TableRowGroup
1369 | LayoutDisplay::TableHeaderGroup
1370 | LayoutDisplay::TableFooterGroup => {
1371 self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?
1372 }
1373 LayoutDisplay::TableRow => {
1374 self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?
1375 }
1376 LayoutDisplay::TableColumn => {
1377 // +spec:table-layout:77974f - Stage 1: all children of table-column treated as display:none
1378 // +spec:table-layout:c8dc69 - Stage 1: remove irrelevant boxes from table-column
1379 // CSS 2.2 §17.2.1: "All child boxes of a 'table-column' parent are
1380 // treated as if they had 'display: none'." - skip all children.
1381 }
1382 LayoutDisplay::TableColumnGroup => {
1383 // CSS 2.2 §17.2.1: "If a child C of a 'table-column-group' parent is not
1384 // a 'table-column' box, then it is treated as if it had 'display: none'."
1385 for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1386 let child_display = get_display_type(styled_dom, child_dom_id);
1387 if child_display == LayoutDisplay::TableColumn {
1388 self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1389 }
1390 // Non-table-column children are suppressed (treated as display:none)
1391 }
1392 }
1393 // Inline, TableCell, etc., have their children processed as part of their
1394 // formatting context layout and don't require anonymous box generation at this stage.
1395 // of table-internal display values is handled via blockify_flex_item_if_table_internal
1396 _ => {
1397 // +spec:display-contents:34008d - display:none elements generate no boxes; excluded from formatting structure
1398 // +spec:display-property:1f38b2 - display:none creates no box at all, filter from layout tree
1399 // +spec:display-property:eb53f7 - display:none suppresses box generation; visibility:hidden boxes still affect layout
1400 // Filter out display: none children - they don't participate in layout
1401 // +spec:display-property:d1600a - display:none suppresses box generation; visibility:hidden boxes still affect layout
1402 // ALSO filter out whitespace-only text nodes for Flex/Grid/etc containers
1403 // to prevent them from becoming unwanted anonymous items.
1404 let children: Vec<NodeId> = dom_id
1405 .az_children(&styled_dom.node_hierarchy.as_container())
1406 // +spec:display-property:9f02c6 - display:none elements generate no boxes
1407 .filter(|&child_id| {
1408 // +spec:display-property:3b507e - display:none excludes subtree from box tree
1409 if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
1410 return false;
1411 }
1412 // Check for whitespace-only text
1413 let node_data = &styled_dom.node_data.as_container()[child_id];
1414 if let NodeType::Text(text) = node_data.get_node_type() {
1415 // Skip if text is empty or just whitespace
1416 return !text.as_str().trim().is_empty();
1417 }
1418 true
1419 })
1420 .collect();
1421
1422 let is_flex_or_grid = matches!(
1423 display_type,
1424 LayoutDisplay::Flex | LayoutDisplay::InlineFlex
1425 | LayoutDisplay::Grid | LayoutDisplay::InlineGrid
1426 );
1427
1428 for child_dom_id in children {
1429 // +spec:display-property:934c84 - table wrapper box generation: display:table/inline-table generates a principal block container (table wrapper box) that establishes BFC and contains the table box + caption boxes
1430 // +spec:width-calculation:59d456 - table wrapper box is block-level, establishes BFC (CSS 2.2 §17.4)
1431 // the table wrapper box becomes the flex item; align-self applies to the
1432 // wrapper, flex longhands apply to the inner table box, caption contents
1433 // contribute to wrapper min/max-content sizes
1434 let child_display = get_display_type(styled_dom, child_dom_id);
1435 if is_flex_or_grid && child_display.creates_table_context() {
1436 let wrapper_idx = self.create_anonymous_node(
1437 node_idx,
1438 AnonymousBoxType::TableWrapper,
1439 FormattingContext::Block { establishes_new_context: true },
1440 );
1441 self.process_node(styled_dom, child_dom_id, Some(wrapper_idx), debug_messages)?;
1442 } else {
1443 let child_idx = self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1444 // table-internal flex items are blockified, preventing anonymous table
1445 // box generation (e.g. two display:table-cell flex items become two
1446 // separate display:block flex items)
1447 if is_flex_or_grid {
1448 blockify_flex_item_if_table_internal(&mut self.nodes, child_idx);
1449 }
1450 }
1451 }
1452 }
1453 }
1454 Ok(node_idx)
1455 }
1456
1457 // +spec:display-property:5572e7 - Anonymous block boxes: wrap inline runs when block container has mixed block/inline children
1458 // +spec:display-property:090043 - Anonymous block box properties inherited from enclosing non-anonymous box; non-inherited props get initial values
1459 // +spec:display-property:7b9f7a - Block-level vs inline-level classification and anonymous block box creation
1460 // +spec:display-property:078fe5 - Anonymous block boxes wrapping inline content in mixed block/inline contexts
1461 // +spec:display-property:8d8ef3 - block container anonymous box generation: wraps inline runs in anonymous block boxes to ensure block containers contain only block-level or only inline-level boxes
1462 // +spec:display-property:1fe2be - inline box construction with anonymous text interspersed with inline elements
1463 // +spec:display-property:be80e3 - Anonymous inline boxes: text in block containers treated as anonymous inlines, whitespace-only runs collapsed
1464 /// Handles children of a block-level element, creating anonymous block
1465 /// wrappers for consecutive runs of inline-level children if necessary.
1466 // +spec:display-property:b73c50 - blockify inline content by wrapping in anonymous block containers
1467 fn process_block_children(
1468 &mut self,
1469 styled_dom: &StyledDom,
1470 parent_dom_id: NodeId,
1471 parent_idx: usize,
1472 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1473 ) -> Result<()> {
1474 // Filter out display: none children - they don't participate in layout
1475 let children: Vec<NodeId> = parent_dom_id
1476 .az_children(&styled_dom.node_hierarchy.as_container())
1477 .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
1478 .collect();
1479
1480 // Debug: log which children we found
1481 if let Some(msgs) = debug_messages.as_mut() {
1482 msgs.push(LayoutDebugMessage::info(format!(
1483 "[process_block_children] DOM node {} has {} children: {:?}",
1484 parent_dom_id.index(),
1485 children.len(),
1486 children.iter().map(|c| c.index()).collect::<Vec<_>>()
1487 )));
1488 }
1489
1490 let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
1491
1492 if let Some(msgs) = debug_messages.as_mut() {
1493 msgs.push(LayoutDebugMessage::info(format!(
1494 "[process_block_children] has_block_child={}, children display types: {:?}",
1495 has_block_child,
1496 children
1497 .iter()
1498 .map(|c| {
1499 let dt = get_display_type(styled_dom, *c);
1500 let is_block = is_block_level(styled_dom, *c);
1501 format!("{}:{:?}(block={})", c.index(), dt, is_block)
1502 })
1503 .collect::<Vec<_>>()
1504 )));
1505 }
1506
1507 if !has_block_child {
1508 // All children are inline, no anonymous boxes needed.
1509 if let Some(msgs) = debug_messages.as_mut() {
1510 msgs.push(LayoutDebugMessage::info(format!(
1511 "[process_block_children] All inline, processing {} children directly",
1512 children.len()
1513 )));
1514 }
1515 for child_id in children {
1516 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1517 }
1518 return Ok(());
1519 }
1520
1521 // Mixed block and inline content requires anonymous wrappers.
1522 let mut inline_run = Vec::new();
1523
1524 for child_id in children {
1525 if is_block_level(styled_dom, child_id) {
1526 // +spec:display-contents:02a534 - contiguous text sequences with no text don't generate boxes
1527 // End the current inline run — but skip if all nodes are whitespace-only text.
1528 // +spec:display-property:7d1570 - whitespace-only text that would be collapsed does not generate anonymous inline boxes
1529 // +spec:white-space-processing:b32f69 - whitespace-only inline runs between blocks don't generate anonymous inline boxes
1530 // CSS 2.1 §9.2.2.1: "White space content that would subsequently be collapsed
1531 // away according to the 'white-space' property does not generate any anonymous
1532 // inline boxes."
1533 if !inline_run.is_empty() {
1534 let all_whitespace = inline_run
1535 .iter()
1536 .all(|id| is_whitespace_only_text(styled_dom, *id));
1537 if all_whitespace {
1538 if let Some(msgs) = debug_messages.as_mut() {
1539 msgs.push(LayoutDebugMessage::info(format!(
1540 "[process_block_children] Skipping whitespace-only inline run between blocks: {:?}",
1541 inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
1542 )));
1543 }
1544 inline_run.clear();
1545 } else {
1546 if let Some(msgs) = debug_messages.as_mut() {
1547 msgs.push(LayoutDebugMessage::info(format!(
1548 "[process_block_children] Creating anon wrapper for inline run: {:?}",
1549 inline_run
1550 .iter()
1551 .map(|c: &NodeId| c.index())
1552 .collect::<Vec<_>>()
1553 )));
1554 }
1555 let anon_idx = self.create_anonymous_node(
1556 parent_idx,
1557 AnonymousBoxType::InlineWrapper,
1558 FormattingContext::Block {
1559 // Anonymous wrappers are BFC roots
1560 establishes_new_context: true,
1561 },
1562 );
1563 for inline_child_id in inline_run.drain(..) {
1564 self.process_node(
1565 styled_dom,
1566 inline_child_id,
1567 Some(anon_idx),
1568 debug_messages,
1569 )?;
1570 }
1571 }
1572 }
1573 // Process the block-level child directly
1574 if let Some(msgs) = debug_messages.as_mut() {
1575 msgs.push(LayoutDebugMessage::info(format!(
1576 "[process_block_children] Processing block child DOM {}",
1577 child_id.index()
1578 )));
1579 }
1580 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1581 } else {
1582 inline_run.push(child_id);
1583 }
1584 }
1585 // Process any remaining inline children at the end — skip if all whitespace
1586 if !inline_run.is_empty() {
1587 let all_whitespace = inline_run
1588 .iter()
1589 .all(|id| is_whitespace_only_text(styled_dom, *id));
1590 if all_whitespace {
1591 if let Some(msgs) = debug_messages.as_mut() {
1592 msgs.push(LayoutDebugMessage::info(format!(
1593 "[process_block_children] Skipping trailing whitespace-only inline run: {:?}",
1594 inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
1595 )));
1596 }
1597 } else {
1598 if let Some(msgs) = debug_messages.as_mut() {
1599 msgs.push(LayoutDebugMessage::info(format!(
1600 "[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
1601 inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
1602 )));
1603 }
1604 let anon_idx = self.create_anonymous_node(
1605 parent_idx,
1606 AnonymousBoxType::InlineWrapper,
1607 FormattingContext::Block {
1608 establishes_new_context: true, // Anonymous wrappers are BFC roots
1609 },
1610 );
1611 for inline_child_id in inline_run {
1612 self.process_node(
1613 styled_dom,
1614 inline_child_id,
1615 Some(anon_idx),
1616 debug_messages,
1617 )?;
1618 }
1619 }
1620 }
1621
1622 Ok(())
1623 }
1624
1625 // +spec:table-layout:6bb84e - Anonymous table object generation (stages 1-3: remove irrelevant boxes, generate missing child wrappers, generate missing parents)
1626 // +spec:table-layout:77974f - Stage 2: generate missing child wrappers for table/inline-table
1627 // +spec:table-layout:c8dc69 - Stage 2: wrap non-proper children in anonymous table-row
1628 /// CSS 2.2 Section 17.2.1 - Anonymous box generation for tables:
1629 /// "If a child C of a 'table' or 'inline-table' box is not a proper table child,
1630 /// then generate an anonymous 'table-row' box around C and all consecutive
1631 /// siblings of C that are not proper table children."
1632 ///
1633 // +spec:display-property:6f8f13 - anonymous table object generation (§17.2.1): suppress table-column/table-column-group children, wrap non-proper children in anonymous rows/cells
1634 /// Proper table children are: table-row-group, table-header-group,
1635 /// table-footer-group, table-row, table-column-group, table-column, table-caption.
1636 fn process_table_children(
1637 &mut self,
1638 styled_dom: &StyledDom,
1639 parent_dom_id: NodeId,
1640 parent_idx: usize,
1641 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1642 ) -> Result<()> {
1643 let parent_display = get_display_type(styled_dom, parent_dom_id);
1644 let mut non_proper_children = Vec::new();
1645
1646 for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1647 // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
1648 if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1649 continue;
1650 }
1651
1652 let child_display = get_display_type(styled_dom, child_id);
1653
1654 if is_proper_table_child(child_display) {
1655 // Flush any accumulated non-proper children into an anonymous table-row
1656 if !non_proper_children.is_empty() {
1657 let anon_row_idx = self.create_anonymous_node(
1658 parent_idx,
1659 AnonymousBoxType::TableRow,
1660 FormattingContext::TableRow,
1661 );
1662
1663 for np_id in non_proper_children.drain(..) {
1664 self.process_node(styled_dom, np_id, Some(anon_row_idx), debug_messages)?;
1665 }
1666 }
1667
1668 // Process proper table child directly (row, row-group, caption, etc.)
1669 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1670 } else {
1671 // Non-proper table child: accumulate for wrapping
1672 non_proper_children.push(child_id);
1673 }
1674 }
1675
1676 // Flush any remaining accumulated non-proper children
1677 if !non_proper_children.is_empty() {
1678 let anon_row_idx = self.create_anonymous_node(
1679 parent_idx,
1680 AnonymousBoxType::TableRow,
1681 FormattingContext::TableRow,
1682 );
1683
1684 for np_id in non_proper_children {
1685 self.process_node(styled_dom, np_id, Some(anon_row_idx), debug_messages)?;
1686 }
1687 }
1688
1689 Ok(())
1690 }
1691
1692 /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1693 /// "If a child C of a row group box is not a 'table-row' box, then generate
1694 /// an anonymous 'table-row' box around C and all consecutive siblings of C
1695 /// that are not 'table-row' boxes."
1696 fn process_table_row_group_children(
1697 &mut self,
1698 styled_dom: &StyledDom,
1699 parent_dom_id: NodeId,
1700 parent_idx: usize,
1701 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1702 ) -> Result<()> {
1703 let parent_display = get_display_type(styled_dom, parent_dom_id);
1704 let mut non_row_children = Vec::new();
1705
1706 for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1707 if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1708 continue;
1709 }
1710
1711 let child_display = get_display_type(styled_dom, child_id);
1712
1713 if child_display == LayoutDisplay::TableRow {
1714 // Flush accumulated non-row children into anonymous row
1715 if !non_row_children.is_empty() {
1716 let anon_row_idx = self.create_anonymous_node(
1717 parent_idx,
1718 AnonymousBoxType::TableRow,
1719 FormattingContext::TableRow,
1720 );
1721 for nr_id in non_row_children.drain(..) {
1722 self.process_node(styled_dom, nr_id, Some(anon_row_idx), debug_messages)?;
1723 }
1724 }
1725 // Process table-row child directly
1726 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1727 } else {
1728 non_row_children.push(child_id);
1729 }
1730 }
1731
1732 // Flush remaining
1733 if !non_row_children.is_empty() {
1734 let anon_row_idx = self.create_anonymous_node(
1735 parent_idx,
1736 AnonymousBoxType::TableRow,
1737 FormattingContext::TableRow,
1738 );
1739 for nr_id in non_row_children {
1740 self.process_node(styled_dom, nr_id, Some(anon_row_idx), debug_messages)?;
1741 }
1742 }
1743
1744 Ok(())
1745 }
1746
1747 /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1748 /// "If a child C of a 'table-row' box is not a 'table-cell', then generate an
1749 /// anonymous 'table-cell' box around C and all consecutive siblings of C that
1750 /// are not 'table-cell' boxes."
1751 fn process_table_row_children(
1752 &mut self,
1753 styled_dom: &StyledDom,
1754 parent_dom_id: NodeId,
1755 parent_idx: usize,
1756 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1757 ) -> Result<()> {
1758 let parent_display = get_display_type(styled_dom, parent_dom_id);
1759 let mut non_cell_children = Vec::new();
1760
1761 for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1762 if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1763 continue;
1764 }
1765
1766 let child_display = get_display_type(styled_dom, child_id);
1767
1768 if child_display == LayoutDisplay::TableCell {
1769 // Flush accumulated non-cell children into one anonymous table-cell
1770 if !non_cell_children.is_empty() {
1771 let anon_cell_idx = self.create_anonymous_node(
1772 parent_idx,
1773 AnonymousBoxType::TableCell,
1774 FormattingContext::Block {
1775 establishes_new_context: true,
1776 },
1777 );
1778 for nc_id in non_cell_children.drain(..) {
1779 self.process_node(styled_dom, nc_id, Some(anon_cell_idx), debug_messages)?;
1780 }
1781 }
1782 // Process table-cell child directly
1783 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1784 } else {
1785 // Accumulate consecutive non-cell children
1786 non_cell_children.push(child_id);
1787 }
1788 }
1789
1790 // Flush remaining non-cell children
1791 if !non_cell_children.is_empty() {
1792 let anon_cell_idx = self.create_anonymous_node(
1793 parent_idx,
1794 AnonymousBoxType::TableCell,
1795 FormattingContext::Block {
1796 establishes_new_context: true,
1797 },
1798 );
1799 for nc_id in non_cell_children {
1800 self.process_node(styled_dom, nc_id, Some(anon_cell_idx), debug_messages)?;
1801 }
1802 }
1803
1804 Ok(())
1805 }
1806 // +spec:display-property:52f497 - anonymous inline boxes inherit inheritable properties from block parent; non-inherited properties use initial values (dom_node_id: None + BoxProps::default())
1807 /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1808 /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
1809 /// to satisfy the constraints of the table model."
1810 ///
1811 // +spec:display-property:ee83bf - Anonymous box generation: boxes not associated with elements, inheriting through box tree parentage
1812 /// Helper to create an anonymous node in the tree.
1813 /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
1814 /// the CSS box model structure (e.g., wrapping inline content in blocks,
1815 /// or creating missing table structural elements).
1816 // +spec:display-property:6ff51a - anonymous block boxes have no styles (box_props default), so parent element properties still apply to its content
1817 pub fn create_anonymous_node(
1818 &mut self,
1819 parent: usize,
1820 anon_type: AnonymousBoxType,
1821 fc: FormattingContext,
1822 ) -> usize {
1823 let index = self.nodes.len();
1824
1825 // +spec:display-property:e67146 - Anonymous boxes inherit from enclosing non-anonymous box; non-inherited props use initial values
1826 let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
1827
1828 self.nodes.push(LayoutNode {
1829 // ── HOT ──
1830 box_props: BoxProps::default(),
1831 dom_node_id: None,
1832 children: Vec::new(),
1833 used_size: None,
1834 formatting_context: fc,
1835 parent: Some(parent),
1836 // ── WARM ──
1837 intrinsic_sizes: None,
1838 baseline: None,
1839 inline_layout_result: None,
1840 scrollbar_info: None,
1841 relative_position: None,
1842 overflow_content_size: None,
1843 taffy_cache: TaffyCache::new(),
1844 computed_style: ComputedLayoutStyle::default(),
1845 pseudo_element: None,
1846 escaped_top_margin: None,
1847 escaped_bottom_margin: None,
1848 parent_formatting_context: parent_fc,
1849 ifc_membership: None,
1850 containing_block_index: None,
1851 // ── COLD ──
1852 anonymous_type: Some(anon_type),
1853 node_data_fingerprint: NodeDataFingerprint::default(),
1854 subtree_hash: SubtreeHash(0),
1855 dirty_flag: DirtyFlag::Layout,
1856 unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1857 ifc_id: None,
1858 });
1859
1860 self.nodes[parent].children.push(index);
1861 index
1862 }
1863
1864 /// Creates a ::marker pseudo-element as the first child of a list-item.
1865 ///
1866 /// Per CSS Lists Module Level 3, Section 3.1:
1867 /// "For elements with display: list-item, user agents must generate a
1868 /// ::marker pseudo-element as the first child of the principal box."
1869 ///
1870 /// The ::marker references the same DOM node as its parent list-item,
1871 /// but is marked as a pseudo-element for proper counter resolution and styling.
1872 pub fn create_marker_pseudo_element(
1873 &mut self,
1874 styled_dom: &StyledDom,
1875 list_item_dom_id: NodeId,
1876 list_item_idx: usize,
1877 ) -> usize {
1878 let index = self.nodes.len();
1879
1880 // The marker references the same DOM node as the list-item
1881 // This is important for style resolution (the marker inherits from the list-item)
1882 let parent_fc = self
1883 .nodes
1884 .get(list_item_idx)
1885 .map(|n| n.formatting_context.clone());
1886 self.nodes.push(LayoutNode {
1887 // ── HOT ──
1888 box_props: BoxProps::default(),
1889 dom_node_id: Some(list_item_dom_id),
1890 children: Vec::new(),
1891 used_size: None,
1892 formatting_context: FormattingContext::Inline,
1893 parent: Some(list_item_idx),
1894 // ── WARM ──
1895 intrinsic_sizes: None,
1896 baseline: None,
1897 inline_layout_result: None,
1898 scrollbar_info: None,
1899 relative_position: None,
1900 overflow_content_size: None,
1901 taffy_cache: TaffyCache::new(),
1902 computed_style: ComputedLayoutStyle::default(),
1903 pseudo_element: Some(PseudoElement::Marker),
1904 escaped_top_margin: None,
1905 escaped_bottom_margin: None,
1906 parent_formatting_context: parent_fc,
1907 ifc_membership: None,
1908 containing_block_index: None,
1909 // ── COLD ──
1910 anonymous_type: None,
1911 node_data_fingerprint: NodeDataFingerprint::default(),
1912 subtree_hash: SubtreeHash(0),
1913 dirty_flag: DirtyFlag::Layout,
1914 unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1915 ifc_id: None,
1916 });
1917
1918 // Insert as FIRST child (per spec)
1919 self.nodes[list_item_idx].children.insert(0, index);
1920
1921 // Register with DOM mapping for counter resolution
1922 self.dom_to_layout
1923 .entry(list_item_dom_id)
1924 .or_default()
1925 .push(index);
1926
1927 index
1928 }
1929
1930 // M12.7: returns `usize`, NOT `Result<usize>` — this fn has no error path
1931 // (always `Ok(index)`). The `Result` forced callers to use `?`, whose lifted
1932 // discriminant decode mis-reads the Ok as Err (the rc=5 root cause: reconcile
1933 // reaches this fn but returns Err before its own Ok). Dropping the Result
1934 // removes that mis-lifting `?`.
1935 pub fn create_node_from_dom(
1936 &mut self,
1937 styled_dom: &StyledDom,
1938 dom_id: NodeId,
1939 parent: Option<usize>,
1940 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1941 ) -> usize {
1942 let index = self.nodes.len();
1943 // M12.7 diag: 0x400B4 = create_node_from_dom's pre-push index (= nodes.len()
1944 // as IT sees it). If this is 0 but build() sees 0 nodes, the push is lost
1945 // between here and build (builder &mut threading); if garbage, len mis-reads.
1946 unsafe { core::ptr::write_volatile(0x400B4 as *mut u32, 0xCE00_0000u32 | (index as u32 & 0xffff)); }
1947 let parent_fc =
1948 parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1949 // M12.7 diag: 0x400CC = parent.and_then done (Option<usize> discriminant). If
1950 // this is reached but step A is NOT, collect_box_props diverges; if this is
1951 // NOT reached, the parent Option discriminant mis-lifts (None→Some garbage).
1952 unsafe { core::ptr::write_volatile(0x400CC as *mut u32, 0xCD00_0001u32 | ((parent_fc.is_some() as u32) << 8)); }
1953 let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
1954 // M12.7 diag: 0x400C0 = collect_box_props returned (step A).
1955 unsafe { core::ptr::write_volatile(0x400C0 as *mut u32, 0xCA00_0001u32); }
1956 self.nodes.push(LayoutNode {
1957 // ── HOT ──
1958 box_props: collected.resolved,
1959 dom_node_id: Some(dom_id),
1960 children: Vec::new(),
1961 used_size: None,
1962 formatting_context: determine_formatting_context(styled_dom, dom_id),
1963 parent,
1964 // ── WARM ──
1965 intrinsic_sizes: None,
1966 baseline: None,
1967 inline_layout_result: None,
1968 scrollbar_info: None,
1969 relative_position: None,
1970 overflow_content_size: None,
1971 taffy_cache: TaffyCache::new(),
1972 // +spec:overflow:8f9f7e - viewport overflow propagation: visible→auto, clip→hidden
1973 computed_style: {
1974 let mut style = compute_layout_style(styled_dom, dom_id);
1975 if parent.is_none() {
1976 // CSS Overflow 3 §3.3: If visible is applied to the viewport,
1977 // it must be interpreted as auto. If clip is applied to the
1978 // viewport, it must be interpreted as hidden.
1979 use azul_css::props::layout::LayoutOverflow;
1980 if style.overflow_x == LayoutOverflow::Visible {
1981 style.overflow_x = LayoutOverflow::Auto;
1982 } else if style.overflow_x == LayoutOverflow::Clip {
1983 style.overflow_x = LayoutOverflow::Hidden;
1984 }
1985 if style.overflow_y == LayoutOverflow::Visible {
1986 style.overflow_y = LayoutOverflow::Auto;
1987 } else if style.overflow_y == LayoutOverflow::Clip {
1988 style.overflow_y = LayoutOverflow::Hidden;
1989 }
1990 }
1991 style
1992 },
1993 pseudo_element: None,
1994 escaped_top_margin: None,
1995 escaped_bottom_margin: None,
1996 parent_formatting_context: parent_fc,
1997 ifc_membership: None,
1998 containing_block_index: None,
1999 // ── COLD ──
2000 anonymous_type: None,
2001 node_data_fingerprint: NodeDataFingerprint::compute(
2002 &styled_dom.node_data.as_container()[dom_id],
2003 styled_dom.styled_nodes.as_container().get(dom_id).map(|n| &n.styled_node_state),
2004 ),
2005 subtree_hash: SubtreeHash(0),
2006 dirty_flag: DirtyFlag::Layout,
2007 unresolved_box_props: collected.unresolved,
2008 ifc_id: None,
2009 });
2010 // M12.7 diag: 0x400C4 = LayoutNode literal + self.nodes.push done (step B).
2011 unsafe { core::ptr::write_volatile(0x400C4 as *mut u32, 0xCB00_0001u32 | ((self.nodes.len() as u32 & 0xff) << 8)); }
2012 if let Some(p) = parent {
2013 self.nodes[p].children.push(index);
2014 }
2015 self.dom_to_layout.entry(dom_id).or_default().push(index);
2016 // M12.7 diag: 0x400B8 = nodes.len() AFTER the push (should be index+1).
2017 unsafe { core::ptr::write_volatile(0x400B8 as *mut u32, 0xCF00_0000u32 | (self.nodes.len() as u32 & 0xffff)); }
2018 index
2019 }
2020
2021 pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
2022 let index = self.nodes.len();
2023 let mut new_node = old_node.clone();
2024 new_node.parent = parent;
2025 new_node.parent_formatting_context =
2026 parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
2027 new_node.children = Vec::new();
2028 new_node.dirty_flag = DirtyFlag::None;
2029 self.nodes.push(new_node);
2030 if let Some(p) = parent {
2031 self.nodes[p].children.push(index);
2032 }
2033 if let Some(dom_id) = old_node.dom_node_id {
2034 self.dom_to_layout.entry(dom_id).or_default().push(index);
2035 }
2036 index
2037 }
2038
2039 pub fn build(self, root_idx: usize) -> LayoutTree {
2040 let nodes = self.nodes;
2041 let node_count = nodes.len();
2042
2043 // Flatten per-node children Vecs into a single contiguous arena.
2044 let total_children: usize = nodes.iter().map(|n| n.children.len()).sum();
2045 let mut arena = Vec::with_capacity(total_children);
2046 let mut offsets = Vec::with_capacity(node_count);
2047
2048 // Split monolithic LayoutNodes into hot/warm/cold SoA arrays
2049 let mut hot_nodes = Vec::with_capacity(node_count);
2050 let mut warm_nodes = Vec::with_capacity(node_count);
2051 let mut cold_nodes = Vec::with_capacity(node_count);
2052
2053 for node in nodes {
2054 // Flatten children into arena first
2055 let start = arena.len() as u32;
2056 let len = node.children.len() as u32;
2057 arena.extend_from_slice(&node.children);
2058 offsets.push((start, len));
2059
2060 // Split into hot/warm/cold
2061 let (hot, warm, cold) = node.split();
2062 hot_nodes.push(hot);
2063 warm_nodes.push(warm);
2064 cold_nodes.push(cold);
2065 }
2066
2067 // M12.7 diag: 0x400B0 = 0xBD00_<len><root> — plain field reads (NOT a
2068 // discriminant). If len>0 but calculate_intrinsic_recursive's
2069 // `tree.get(root).ok_or(InvalidTree)?` still errors, that `?`/null-check
2070 // mis-discriminates Some→None. If len==0, build's input was empty.
2071 unsafe {
2072 core::ptr::write_volatile(
2073 0x400B0 as *mut u32,
2074 0xBD00_0000u32 | (((hot_nodes.len() as u32) & 0xff) << 8) | (root_idx as u32 & 0xff),
2075 );
2076 }
2077
2078 LayoutTree {
2079 nodes: hot_nodes,
2080 warm: warm_nodes,
2081 cold: cold_nodes,
2082 root: root_idx,
2083 dom_to_layout: self.dom_to_layout,
2084 children_arena: arena,
2085 children_offsets: offsets,
2086 // Populated by `generate_layout_tree` after the tree is built,
2087 // since the computation needs styled_dom for float/position lookup.
2088 subtree_needs_intrinsic: Vec::new(),
2089 }
2090 }
2091}
2092
2093// +spec:display-property:697082 - outer display type determines principal box's role in flow layout (block vs inline)
2094// +spec:display-property:0d251b - Block-level elements: display 'block', 'list-item', 'table' generate block-level boxes
2095// +spec:display-property:9464be - block-level vs block container distinction: not all block-level boxes are block containers (e.g. replaced elements, flex containers)
2096pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2097 matches!(
2098 get_display_type(styled_dom, node_id),
2099 LayoutDisplay::Block
2100 | LayoutDisplay::FlowRoot
2101 | LayoutDisplay::Flex
2102 | LayoutDisplay::Grid
2103 | LayoutDisplay::Table
2104 | LayoutDisplay::TableCaption
2105 | LayoutDisplay::TableRow
2106 | LayoutDisplay::TableRowGroup
2107 | LayoutDisplay::TableHeaderGroup
2108 | LayoutDisplay::TableFooterGroup
2109 | LayoutDisplay::TableCell
2110 | LayoutDisplay::ListItem
2111 )
2112}
2113
2114// +spec:display-property:23f111 - Inline-level elements: inline, inline-block, inline-table, inline-flex, inline-grid
2115/// Checks if a node is inline-level (including text nodes).
2116/// According to CSS spec, inline-level content includes:
2117///
2118/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
2119/// - Text nodes
2120/// - Generated content
2121fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2122 // Text nodes are always inline-level
2123 let node_data = &styled_dom.node_data.as_container()[node_id];
2124 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2125 return true;
2126 }
2127
2128 // Check the display property
2129 matches!(
2130 get_display_type(styled_dom, node_id),
2131 LayoutDisplay::Inline
2132 | LayoutDisplay::InlineBlock
2133 | LayoutDisplay::InlineTable
2134 | LayoutDisplay::InlineFlex
2135 | LayoutDisplay::InlineGrid
2136 )
2137}
2138
2139// +spec:display-property:c2520b - Block containers with only inline-level children establish IFC; mixed content gets anonymous block wrappers
2140/// Checks if a block container has only inline-level children.
2141/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
2142/// by a block container box that contains no block-level boxes."
2143// +spec:display-property:75d642 - block container with only inline-level content establishes IFC
2144// +spec:display-property:c188d6 - IFC: all inline content within a containing block flows together as continuous text
2145fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2146 let hierarchy = styled_dom.node_hierarchy.as_container();
2147 let node_hier = match hierarchy.get(node_id) {
2148 Some(n) => n,
2149 None => {
2150 return false;
2151 }
2152 };
2153
2154 // Get the first child
2155 let mut current_child = node_hier.first_child_id(node_id);
2156
2157 // If there are no children, it's not an IFC (it's empty)
2158 if current_child.is_none() {
2159 return false;
2160 }
2161
2162 // Check all children
2163 while let Some(child_id) = current_child {
2164 let is_inline = is_inline_level(styled_dom, child_id);
2165
2166 if !is_inline {
2167 // Found a block-level child
2168 return false;
2169 }
2170
2171 // Move to next sibling
2172 if let Some(child_hier) = hierarchy.get(child_id) {
2173 current_child = child_hier.next_sibling_id();
2174 } else {
2175 break;
2176 }
2177 }
2178
2179 // All children are inline-level
2180 true
2181}
2182
2183/// Pre-computes all CSS properties needed during layout for a single node.
2184///
2185/// This is called once per node during layout tree construction, avoiding
2186/// repeated style lookups during the actual layout pass (O(n) vs O(n²)).
2187fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
2188 let styled_node_state = styled_dom
2189 .styled_nodes
2190 .as_container()
2191 .get(dom_id)
2192 .map(|n| n.styled_node_state.clone())
2193 .unwrap_or_default();
2194
2195 // Get display property
2196 let display = match get_display_property(styled_dom, Some(dom_id)) {
2197 MultiValue::Exact(d) => d,
2198 MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
2199 };
2200
2201 // Get position property
2202 let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2203
2204 // Get float property
2205 let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2206
2207 // Get overflow properties
2208 // +spec:overflow:48890c - overflow:hidden treated as overflow:clip on replaced elements
2209 let is_replaced = matches!(
2210 styled_dom.node_data.as_container()[dom_id].get_node_type(),
2211 NodeType::Image(_) | NodeType::VirtualView
2212 );
2213 let overflow_x = {
2214 let v = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2215 if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2216 };
2217 let overflow_y = {
2218 let v = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2219 if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2220 };
2221
2222 // Get writing mode, direction, and text-orientation
2223 // +spec:writing-modes:2af307 - Propagate used writing-mode from <body> to <html> root
2224 let writing_mode = {
2225 let own_wm = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2226 let nd = &styled_dom.node_data.as_container()[dom_id];
2227 if matches!(nd.node_type, NodeType::Html) {
2228 // If root <html>, propagate writing-mode from first <body> child
2229 styled_dom
2230 .node_hierarchy
2231 .as_container()
2232 .get(dom_id)
2233 .and_then(|node| node.first_child_id(dom_id))
2234 .and_then(|child_id| {
2235 let child_data = &styled_dom.node_data.as_container()[child_id];
2236 if matches!(child_data.node_type, NodeType::Body) {
2237 let child_state = &styled_dom
2238 .styled_nodes
2239 .as_container()[child_id]
2240 .styled_node_state;
2241 Some(get_writing_mode(styled_dom, child_id, child_state)
2242 .unwrap_or_default())
2243 } else {
2244 None
2245 }
2246 })
2247 .unwrap_or(own_wm)
2248 } else {
2249 own_wm
2250 }
2251 };
2252 let direction = get_direction(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2253 let text_orientation = get_text_orientation(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2254
2255 // Get text-align
2256 let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2257
2258 // Get explicit width/height (None = auto)
2259 let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
2260 MultiValue::Exact(w) => Some(w),
2261 _ => None,
2262 };
2263 let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
2264 MultiValue::Exact(h) => Some(h),
2265 _ => None,
2266 };
2267
2268 // Get min/max constraints
2269 let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
2270 MultiValue::Exact(v) => Some(v),
2271 _ => None,
2272 };
2273 let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
2274 MultiValue::Exact(v) => Some(v),
2275 _ => None,
2276 };
2277 let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
2278 MultiValue::Exact(v) => Some(v),
2279 _ => None,
2280 };
2281 let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
2282 MultiValue::Exact(v) => Some(v),
2283 _ => None,
2284 };
2285
2286 ComputedLayoutStyle {
2287 display,
2288 position,
2289 float,
2290 overflow_x,
2291 overflow_y,
2292 writing_mode,
2293 direction,
2294 text_orientation,
2295 width,
2296 height,
2297 min_width,
2298 min_height,
2299 max_width,
2300 max_height,
2301 text_align,
2302 }
2303}
2304
2305// hash_node_data() removed — replaced by NodeDataFingerprint::compute()
2306
2307/// Helper function to get element's computed font-size
2308fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2309 unsafe { core::ptr::write_volatile(0x400E0 as *mut u32, 0xC3_000001u32); } // 2-arg wrapper entered
2310 let node_state = styled_dom
2311 .styled_nodes
2312 .as_container()
2313 .get(dom_id)
2314 .map(|n| &n.styled_node_state)
2315 .cloned()
2316 .unwrap_or_default();
2317 unsafe { core::ptr::write_volatile(0x400E0 as *mut u32, 0xC3_000002u32); } // after node_state (clone); next = 3-arg call
2318
2319 crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
2320}
2321
2322/// Helper function to get parent's computed font-size
2323fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2324 styled_dom
2325 .node_hierarchy
2326 .as_container()
2327 .get(dom_id)
2328 .and_then(|node| node.parent_id())
2329 .map(|parent_id| get_element_font_size(styled_dom, parent_id))
2330 .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
2331}
2332
2333/// Helper function to get root element's font-size
2334fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
2335 // Root is always NodeId(0) in Azul
2336 get_element_font_size(styled_dom, NodeId::new(0))
2337}
2338
2339/// Create a ResolutionContext for a given node
2340fn create_resolution_context(
2341 styled_dom: &StyledDom,
2342 dom_id: NodeId,
2343 containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
2344 viewport_size: LogicalSize,
2345) -> azul_css::props::basic::ResolutionContext {
2346 unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000001u32); } // create_resolution_context entered
2347 let element_font_size = get_element_font_size(styled_dom, dom_id);
2348 unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000002u32); } // after get_element_font_size
2349 let parent_font_size = get_parent_font_size(styled_dom, dom_id);
2350 unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000003u32); } // after get_parent_font_size
2351 let root_font_size = get_root_font_size(styled_dom);
2352 unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000004u32); } // after get_root_font_size
2353
2354 ResolutionContext {
2355 element_font_size,
2356 parent_font_size,
2357 root_font_size,
2358 // +spec:box-model:ec6466 - percentage margins/padding resolve to 0 when containing block is unknown (intrinsic sizing), breaking cyclic dependencies per css-sizing-3 §5.2.1
2359 containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
2360 element_size: None, // Not yet laid out
2361 viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
2362 }
2363}
2364
2365/// Result of collecting box properties from the styled DOM.
2366struct CollectedBoxProps {
2367 unresolved: crate::solver3::geometry::UnresolvedBoxProps,
2368 resolved: BoxProps,
2369}
2370
2371/// Collects box properties from the styled DOM and returns both unresolved and resolved forms.
2372///
2373/// The unresolved form stores the raw CSS values for later re-resolution when
2374/// the containing block size is known. The resolved form is an initial resolution
2375/// using viewport_size for viewport-relative units.
2376fn collect_box_props(
2377 styled_dom: &StyledDom,
2378 dom_id: NodeId,
2379 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2380 viewport_size: LogicalSize,
2381) -> CollectedBoxProps {
2382 use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
2383 use crate::solver3::getters::*;
2384 // M12.7 diag: collect_box_props sub-step markers (0xC0_0N). The last one set
2385 // before create_node step A is the diverging call.
2386 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000001u32); } // entered
2387
2388 let node_data = &styled_dom.node_data.as_container()[dom_id];
2389
2390 // Get styled node state
2391 let node_state = styled_dom
2392 .styled_nodes
2393 .as_container()
2394 .get(dom_id)
2395 .map(|n| &n.styled_node_state)
2396 .cloned()
2397 .unwrap_or_default();
2398 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000002u32); } // after node_state (clone)
2399
2400 // Create resolution context for this element
2401 // Note: containing_block_size is None here because we don't have it yet
2402 // This is fine for initial resolution - will be re-resolved during layout
2403 let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
2404 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000003u32); } // after create_resolution_context
2405
2406 // Read margin values from styled_dom
2407 let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
2408 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000004u32); } // after get_css_margin_top
2409 let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
2410 let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
2411 let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
2412
2413 // Convert MultiValue to UnresolvedMargin
2414 let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
2415 match mv {
2416 MultiValue::Auto => UnresolvedMargin::Auto,
2417 MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
2418 _ => UnresolvedMargin::Zero,
2419 }
2420 };
2421
2422 // Build unresolved margins
2423 let unresolved_margin = UnresolvedEdge {
2424 top: to_unresolved_margin(&margin_top_mv),
2425 right: to_unresolved_margin(&margin_right_mv),
2426 bottom: to_unresolved_margin(&margin_bottom_mv),
2427 left: to_unresolved_margin(&margin_left_mv),
2428 };
2429 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000005u32); } // after margin block
2430
2431 // Read padding values
2432 let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
2433 let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
2434 let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
2435 let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
2436
2437 // Convert MultiValue to PixelValue (default to 0px)
2438 let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
2439 match mv {
2440 MultiValue::Exact(pv) => pv,
2441 _ => PixelValue::const_px(0),
2442 }
2443 };
2444
2445 // Build unresolved padding
2446 let unresolved_padding = UnresolvedEdge {
2447 top: to_pixel_value(padding_top_mv),
2448 right: to_pixel_value(padding_right_mv),
2449 bottom: to_pixel_value(padding_bottom_mv),
2450 left: to_pixel_value(padding_left_mv),
2451 };
2452 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000056u32); } // after padding getters+values, before get_display_type
2453
2454 // +spec:table-layout:038f9d - padding does not apply to table-row-group, table-header-group, table-footer-group, table-row, table-column-group, table-column
2455 // Non-cell internal table elements (rows, row groups, columns, column groups) do not have padding.
2456 // M12.7 diag: capture get_display_type's return BEFORE the match. If 0x400D0 reads
2457 // 0xC0_57<dt> the CALL returned (dt = LayoutDisplay discriminant) and the MATCH below
2458 // diverges; if it stays 0x56, get_display_type (the enum extraction) itself diverges.
2459 // M12.7 NOTE: get_display_type RETURNS a valid dt here (captured =2), but the code
2460 // immediately after diverges — and replacing the `match` below with a branchless
2461 // bitmask test did NOT help (so it's NOT the multi-way-branch codegen). So the
2462 // get_display_type CALL corrupts the caller frame / control flow (same class as
2463 // create_node's return 0→48704), specific to ENUM-returning getters (pixel getters
2464 // like get_css_margin_* lift fine). Remill-level. The match is kept (original).
2465 let unresolved_padding = match get_display_type(styled_dom, dom_id) {
2466 LayoutDisplay::TableRow
2467 | LayoutDisplay::TableRowGroup
2468 | LayoutDisplay::TableHeaderGroup
2469 | LayoutDisplay::TableFooterGroup
2470 | LayoutDisplay::TableColumn
2471 | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2472 top: PixelValue::const_px(0),
2473 right: PixelValue::const_px(0),
2474 bottom: PixelValue::const_px(0),
2475 left: PixelValue::const_px(0),
2476 },
2477 _ => unresolved_padding,
2478 };
2479 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000006u32); } // after padding block
2480
2481 // Read border values
2482 let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
2483 let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
2484 let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
2485 let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
2486
2487 // +spec:box-model:17c0e0 - computed border-width is 0 if border-style is none or hidden
2488 // +spec:box-model:5d2b66 - border-style none/hidden means no border
2489 // CSS 2.2 §8.5.1: "Computed value: absolute length; '0' if the border style is 'none' or 'hidden'"
2490 use azul_css::props::style::border::BorderStyle;
2491
2492 let style_zeroes_width = |s: BorderStyle| matches!(s, BorderStyle::None | BorderStyle::Hidden);
2493
2494 // Read border styles to check if widths should be zeroed.
2495 // FAST PATH: compact cache returns styles directly for normal state — no
2496 // cascade walks. Prior code here did 4 cascade walks × 586 nodes.
2497 let (bs_top, bs_right, bs_bottom, bs_left) = {
2498 let cache_ptr = &styled_dom.css_property_cache.ptr;
2499 if node_state.is_normal() {
2500 if let Some(ref cc) = cache_ptr.compact_cache {
2501 let idx = dom_id.index();
2502 (cc.get_border_top_style(idx), cc.get_border_right_style(idx),
2503 cc.get_border_bottom_style(idx), cc.get_border_left_style(idx))
2504 } else {
2505 (
2506 cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2507 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2508 cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2509 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2510 cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2511 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2512 cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2513 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2514 )
2515 }
2516 } else {
2517 (
2518 cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2519 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2520 cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2521 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2522 cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2523 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2524 cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2525 .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2526 )
2527 }
2528 };
2529
2530 // Build unresolved border, zeroing width when style is none or hidden
2531 let unresolved_border = UnresolvedEdge {
2532 top: if style_zeroes_width(bs_top) { PixelValue::const_px(0) } else { to_pixel_value(border_top_mv) },
2533 right: if style_zeroes_width(bs_right) { PixelValue::const_px(0) } else { to_pixel_value(border_right_mv) },
2534 bottom: if style_zeroes_width(bs_bottom) { PixelValue::const_px(0) } else { to_pixel_value(border_bottom_mv) },
2535 left: if style_zeroes_width(bs_left) { PixelValue::const_px(0) } else { to_pixel_value(border_left_mv) },
2536 };
2537 unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000007u32); } // after border block (incl is_normal/compact_cache fast-path)
2538
2539 // +spec:box-model:8538a9 - Internal table elements do not have margins (CSS 2.2 §17.5)
2540 // "These boxes have content and borders and cells have padding as well.
2541 // Internal table elements do not have margins."
2542 // +spec:box-model:b4923a - Internal table elements do not have margins (CSS 2.2 § 17.5)
2543 // +spec:box-model:0a9f8e - Internal table elements do not have margins (CSS 2.2 § 17.5)
2544 let display_type = get_display_type(styled_dom, dom_id);
2545 let unresolved_margin = match display_type {
2546 LayoutDisplay::TableRow
2547 | LayoutDisplay::TableRowGroup
2548 | LayoutDisplay::TableHeaderGroup
2549 | LayoutDisplay::TableFooterGroup
2550 | LayoutDisplay::TableCell
2551 | LayoutDisplay::TableColumn
2552 | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2553 top: UnresolvedMargin::Zero,
2554 right: UnresolvedMargin::Zero,
2555 bottom: UnresolvedMargin::Zero,
2556 left: UnresolvedMargin::Zero,
2557 },
2558 // +spec:box-model:1197a5 - height property does not apply to non-replaced inline elements; vertical margins zeroed
2559 // +spec:replaced-elements:f07118 - non-replaced elements have rendering dictated by CSS model
2560 // "These properties apply to all elements, but vertical margins will not have
2561 // any effect on non-replaced inline elements."
2562 LayoutDisplay::Inline => {
2563 let is_replaced = matches!(
2564 node_data.get_node_type(),
2565 NodeType::Image(_) | NodeType::VirtualView
2566 );
2567 if is_replaced {
2568 unresolved_margin
2569 } else {
2570 UnresolvedEdge {
2571 top: UnresolvedMargin::Zero,
2572 bottom: UnresolvedMargin::Zero,
2573 ..unresolved_margin
2574 }
2575 }
2576 },
2577 _ => unresolved_margin,
2578 };
2579
2580 // Build the UnresolvedBoxProps
2581 let unresolved = UnresolvedBoxProps {
2582 margin: unresolved_margin,
2583 padding: unresolved_padding,
2584 border: unresolved_border,
2585 };
2586
2587 // Create initial resolution params (with viewport as containing block for now)
2588 let params = crate::solver3::geometry::ResolutionParams {
2589 containing_block: viewport_size,
2590 viewport_size,
2591 element_font_size: context.parent_font_size,
2592 root_font_size: context.root_font_size,
2593 };
2594
2595 // Resolve to get initial box_props
2596 let resolved = unresolved.resolve(¶ms);
2597
2598 // Debug ALL node box props (padding, margin, border) for cascade debugging
2599 if let Some(msgs) = debug_messages.as_mut() {
2600 msgs.push(LayoutDebugMessage::box_props(format!(
2601 "[BOX] node[{}] {:?} pad=[{:.1} {:.1} {:.1} {:.1}] mar=[{:.1} {:.1} {:.1} {:.1}] bor=[{:.1} {:.1} {:.1} {:.1}]",
2602 dom_id.index(), node_data.node_type,
2603 resolved.padding.top, resolved.padding.right, resolved.padding.bottom, resolved.padding.left,
2604 resolved.margin.top, resolved.margin.right, resolved.margin.bottom, resolved.margin.left,
2605 resolved.border.top, resolved.border.right, resolved.border.bottom, resolved.border.left,
2606 )));
2607 }
2608
2609 // Debug nodes with non-zero margins or vh units
2610 if let Some(msgs) = debug_messages.as_mut() {
2611 // Check if any margin uses vh
2612 let has_vh = match &unresolved_margin.top {
2613 UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
2614 _ => false,
2615 };
2616 if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
2617 msgs.push(LayoutDebugMessage::box_props(format!(
2618 "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
2619 dom_id, node_data.node_type,
2620 unresolved_margin.top,
2621 resolved.margin.top,
2622 viewport_size
2623 )));
2624 }
2625 }
2626
2627 // Debug margin_auto detection
2628 if let Some(msgs) = debug_messages.as_mut() {
2629 msgs.push(LayoutDebugMessage::box_props(format!(
2630 "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
2631 dom_id, node_data.node_type,
2632 resolved.margin_auto.left, resolved.margin_auto.right,
2633 resolved.margin_auto.top, resolved.margin_auto.bottom,
2634 unresolved_margin.left
2635 )));
2636 }
2637
2638 // Debug for Body nodes
2639 if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
2640 if let Some(msgs) = debug_messages.as_mut() {
2641 msgs.push(LayoutDebugMessage::box_props(format!(
2642 "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
2643 resolved.margin.top, resolved.margin.right,
2644 resolved.margin.bottom, resolved.margin.left
2645 )));
2646 }
2647 }
2648
2649 CollectedBoxProps { unresolved, resolved }
2650}
2651
2652/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
2653/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
2654/// and do not themselves have 'display' set to a table-related value. In this context,
2655/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
2656///
2657/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
2658/// Returns true if the node is a text node containing only whitespace characters
2659/// that would be collapsed away by the white-space property.
2660// according to the 'white-space' property does not generate any anonymous inline boxes (CSS2§9.2.2.1)
2661pub fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2662 let binding = styled_dom.node_data.as_container();
2663 let node_data = binding.get(node_id);
2664 if let Some(data) = node_data {
2665 if let NodeType::Text(text) = data.get_node_type() {
2666 // Check if the text contains only CSS document white space characters
2667 // Per CSS Text 3 §4.1: document white space = U+0020, U+0009, segment breaks
2668 if !text.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
2669 return false;
2670 }
2671 // Per CSS2§9.2.2.1: "White space content that would subsequently be
2672 // collapsed away according to the 'white-space' property does not
2673 // generate any anonymous inline boxes."
2674 // For white-space: pre / pre-wrap / break-spaces, whitespace is preserved
2675 // and should NOT be treated as collapsible.
2676 let white_space = styled_dom
2677 .styled_nodes
2678 .as_container()
2679 .get(node_id)
2680 .map(|n| {
2681 match get_white_space_property(styled_dom, node_id, &n.styled_node_state) {
2682 MultiValue::Exact(ws) => ws,
2683 _ => StyleWhiteSpace::Normal,
2684 }
2685 })
2686 .unwrap_or(StyleWhiteSpace::Normal);
2687 return match white_space {
2688 // These values collapse whitespace — whitespace-only text is collapsible
2689 StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap | StyleWhiteSpace::PreLine => true,
2690 // These values preserve whitespace — whitespace-only text is NOT collapsible
2691 StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => false,
2692 };
2693 }
2694 }
2695
2696 false
2697}
2698
2699/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
2700/// Determines if a node should be skipped in table structure generation.
2701/// Whitespace-only text nodes are "irrelevant" and should not generate boxes
2702/// when they appear between table-related elements.
2703///
2704/// Returns true if the node should be skipped (i.e., it's whitespace-only text
2705/// and the parent is a table structural element).
2706fn should_skip_for_table_structure(
2707 styled_dom: &StyledDom,
2708 node_id: NodeId,
2709 parent_display: LayoutDisplay,
2710) -> bool {
2711 // CSS 2.2 Section 17.2.1: Only skip whitespace text nodes when parent is
2712 // a table structural element (table, row group, row)
2713 matches!(
2714 parent_display,
2715 LayoutDisplay::Table
2716 | LayoutDisplay::InlineTable
2717 | LayoutDisplay::TableRowGroup
2718 | LayoutDisplay::TableHeaderGroup
2719 | LayoutDisplay::TableFooterGroup
2720 | LayoutDisplay::TableRow
2721 ) && is_whitespace_only_text(styled_dom, node_id)
2722}
2723
2724/// Returns true if the given display type is a "proper table child" of a table/inline-table box.
2725/// Per CSS 2.2 §17.2.1, proper table children are: table-row-group, table-header-group,
2726/// table-footer-group, table-row, table-column-group, table-column, table-caption.
2727fn is_proper_table_child(display: LayoutDisplay) -> bool {
2728 matches!(
2729 display,
2730 LayoutDisplay::TableRowGroup
2731 | LayoutDisplay::TableHeaderGroup
2732 | LayoutDisplay::TableFooterGroup
2733 | LayoutDisplay::TableRow
2734 | LayoutDisplay::TableColumnGroup
2735 | LayoutDisplay::TableColumn
2736 | LayoutDisplay::TableCaption
2737 )
2738}
2739
2740// Determines the display type of a node based on its tag and CSS properties.
2741// Delegates to getters::get_display_property which uses the compact cache fast path.
2742// M12.7 ROOT: get_display_type (and every layout enum getter) mis-lifts to wasm via the
2743// remill enum-return/decode path — the geometry-chain blocker. FOUR Rust workarounds all
2744// FAILED to advance (none reached collect_box_props past get_display_type):
2745// 1. skip the get_css_property! enum compact-cache fast path → no change
2746// 2. replace the LayoutDisplay `match` with a branchless bitmask → no change
2747// 3. #[inline(never)] (wrap the call w/ enforce_sp_preservation) → made it diverge earlier
2748// 4. bypass MultiValue<LayoutDisplay> by reading cc.get_display() directly → diverges earlier
2749// So it is NOT the match codegen, NOT the MultiValue wrapper, NOT a frame/SP issue — it is
2750// the lift of a fn RETURNING a small fieldless enum (LayoutDisplay) corrupting control flow
2751// (pixel/i16-returning getters lift fine). Needs the remill m12-q-reg-x8-sret fork's
2752// enum-return handling — not fixable in Rust. (Original kept.)
2753pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
2754 use crate::solver3::getters::get_display_property;
2755 get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
2756}
2757
2758// +spec:display-contents:95faa5 - blockification has no effect on none/contents (other => other)
2759// +spec:display-property:f68848 - Automatic box type transformations: blockification of computed display values
2760/// Blockify a display type per CSS Display 3 §2.7.
2761// +spec:display-property:760c5f - blockification sets computed outer display type to block
2762/// +spec:display-property:d50f70 - blockification affects computed values, determining principal box type only
2763/// // +spec:inline-block:692e44 - blockification of inline-block per CSS2 compatibility
2764// +spec:display-property:c3aca2 - inline-block blockifies to block, not flow-root
2765// +spec:display-property:ee2d65 - blockification of inline-level display types (CSS Display 3 §2.7)
2766// +spec:display-property:e4a8b7 - layout-internal boxes blockified to flow (block container)
2767/// CSS Flexbox §3: flex items with table-internal display values
2768/// (table-cell, table-row, table-row-group, table-header-group, table-footer-group,
2769/// table-column, table-column-group, table-caption) are blockified to display:block
2770/// before anonymous table box generation can occur. E.g. two consecutive
2771/// display:table-cell flex items become two separate display:block flex items.
2772fn blockify_flex_item_if_table_internal(nodes: &mut Vec<LayoutNode>, node_idx: usize) {
2773 if let Some(node) = nodes.get_mut(node_idx) {
2774 let is_table_internal = matches!(
2775 node.formatting_context,
2776 FormattingContext::TableCell
2777 | FormattingContext::TableRow
2778 | FormattingContext::TableRowGroup
2779 | FormattingContext::TableColumnGroup
2780 | FormattingContext::TableCaption
2781 | FormattingContext::Table
2782 );
2783 if is_table_internal {
2784 node.formatting_context = FormattingContext::Block {
2785 establishes_new_context: true,
2786 };
2787 }
2788 }
2789}
2790
2791/// Returns true if the node is a replaced element per CSS Display 3 Appendix B.
2792/// Replaced elements (img, canvas, embed, object, audio, video, input, textarea,
2793/// select, br, wbr, meter, progress, virtual views) cannot be un-boxed by
2794/// `display: contents` and always establish an independent formatting context.
2795fn is_replaced_element(node_data: &NodeData) -> bool {
2796 matches!(
2797 node_data.get_node_type(),
2798 NodeType::Image(_)
2799 | NodeType::VirtualView
2800 | NodeType::Br
2801 | NodeType::Wbr
2802 | NodeType::Meter
2803 | NodeType::Progress
2804 | NodeType::Canvas
2805 | NodeType::Embed
2806 | NodeType::Object
2807 | NodeType::Audio
2808 | NodeType::Video
2809 | NodeType::Input
2810 | NodeType::TextArea
2811 | NodeType::Select
2812 )
2813}
2814
2815// +spec:display-property:285fe7 - block box establishing a BFC (block-level block container with new BFC)
2816/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
2817/// A BFC contains floats and prevents margin collapse.
2818fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2819 let display = get_display_type(styled_dom, node_id);
2820 if matches!(
2821 display,
2822 LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption | LayoutDisplay::FlowRoot
2823 ) {
2824 return true;
2825 }
2826
2827 if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2828 let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
2829 if !overflow_x.is_visible_or_clip() {
2830 return true;
2831 }
2832
2833 let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
2834 if !overflow_y.is_visible_or_clip() {
2835 return true;
2836 }
2837
2838 let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
2839 if position.is_absolute_or_fixed() {
2840 return true;
2841 }
2842
2843 let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
2844 if !float.is_none() {
2845 return true;
2846 }
2847 }
2848
2849 // CSS Writing Modes 4 § 3.2: block container with different writing-mode than parent establishes BFC
2850 if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2851 let hierarchy = styled_dom.node_hierarchy.as_container();
2852 if let Some(parent_dom_id) = hierarchy[node_id].parent_id() {
2853 let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
2854 let child_wm = get_writing_mode(styled_dom, node_id, &styled_node.styled_node_state).unwrap_or_default();
2855 let parent_wm = get_writing_mode(styled_dom, parent_dom_id, parent_state).unwrap_or_default();
2856 if child_wm != parent_wm {
2857 return true;
2858 }
2859 }
2860 }
2861
2862 // +spec:replaced-elements:4f494d - replaced elements always establish an independent formatting context
2863 let node_data = &styled_dom.node_data.as_container()[node_id];
2864 if is_replaced_element(node_data) {
2865 return true;
2866 }
2867
2868 // The root element (<html>) also establishes a BFC.
2869 if styled_dom.root.into_crate_internal() == Some(node_id) {
2870 return true;
2871 }
2872
2873 false
2874}
2875
2876// +spec:display-property:0d93f1 - maps display value to box generation (principal box, none, or contents)
2877/// Like `determine_formatting_context`, but uses an explicit (possibly blockified) display type
2878/// instead of reading it from the DOM. Used when blockification changes the display.
2879// +spec:display-property:80f43f - inner display type defines formatting context for non-replaced elements
2880// +spec:display-property:46e71c - Maps outer display (block/inline) and inner display (flow/flow-root/table/flex/grid) to FormattingContext
2881// +spec:display-property:aa582d - maps display types to formatting contexts (inline-level, block-level, atomic inline, block container)
2882fn determine_formatting_context_for_display(
2883 styled_dom: &StyledDom,
2884 node_id: NodeId,
2885 display_type: LayoutDisplay,
2886) -> FormattingContext {
2887 let node_data = &styled_dom.node_data.as_container()[node_id];
2888 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2889 return FormattingContext::Inline;
2890 }
2891 // +spec:display-property:2a8d62 - block containers with inline-level content establish an IFC
2892 match display_type {
2893 // +spec:display-property:37bcf3 - inline outer display type generates an inline box
2894 // +spec:display-property:30a935 - outer display without inner defaults to flow (block/inline both use flow context)
2895 LayoutDisplay::Inline => FormattingContext::Inline,
2896 // +spec:block-formatting-context:97b03b - flow-root always establishes a new BFC; block/list-item may establish one based on other conditions
2897 // +spec:display-property:0bac26 - list-item limited to flow layout inner types (block/flow-root)
2898 // +spec:display-property:0beffc - block container with only inline children establishes IFC
2899 // +spec:display-property:7c49c1 - block container with only inline children establishes an IFC
2900 // +spec:display-property:90ba2a - flow-root always establishes a new BFC
2901 LayoutDisplay::FlowRoot => FormattingContext::Block {
2902 establishes_new_context: true,
2903 },
2904 LayoutDisplay::Block | LayoutDisplay::ListItem => {
2905 if has_only_inline_children(styled_dom, node_id) {
2906 FormattingContext::Inline
2907 } else {
2908 FormattingContext::Block {
2909 establishes_new_context: establishes_new_block_formatting_context(
2910 styled_dom, node_id,
2911 ),
2912 }
2913 }
2914 }
2915 LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
2916 // +spec:display-property:723fe8 - CSS 2.2 §17.2 table model: display types map to formatting contexts, table-column/column-group not rendered, anonymous table objects generated
2917 // +spec:table-layout:023714 - map display values to table formatting contexts per CSS 2.2 §17.2
2918 // +spec:table-layout:6c5039 - row-primary table model: rows/cells/captions/columns mapped here
2919 // +spec:table-layout:75eea9 - display property values for table elements (table, tr, td, etc.)
2920 // +spec:table-layout:3ee121 - layout-internal display types map to table formatting context
2921 // +spec:display-property:b02b7f - table display types map to table formatting contexts;
2922 // table-column/table-column-group not rendered (treated as display:none for box generation)
2923 LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
2924 LayoutDisplay::TableRowGroup
2925 | LayoutDisplay::TableHeaderGroup
2926 | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
2927 LayoutDisplay::TableRow => FormattingContext::TableRow,
2928 LayoutDisplay::TableCell => FormattingContext::TableCell,
2929 // +spec:display-property:da3fc7 - display:none/contents generate no boxes (no inner/outer display types)
2930 // +spec:display-property:e370af - display:none generates no boxes or text sequences
2931 LayoutDisplay::None => FormattingContext::None,
2932 LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
2933 LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
2934 LayoutDisplay::TableCaption => FormattingContext::TableCaption,
2935 LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
2936 // table-column elements are used only for column styling, not for generating boxes
2937 LayoutDisplay::TableColumn => FormattingContext::None,
2938 // +spec:display-contents:584072 - no special behavior for legend/HTML elements; contents handled normally
2939 // display:contents - element generates no box, children are promoted to parent
2940 LayoutDisplay::Contents => FormattingContext::Contents,
2941 // +spec:display-property:b89b80 - run-in box falls back to block (merging into next block not implemented)
2942 // +spec:display-property:ccd4e6 - run-in falls back to block; reparenting not implemented
2943 // These less common display types default to block behavior
2944 // +spec:display-property:7d77f5 - run-in treated as block (run-in sequencing fixup not yet implemented)
2945 // +spec:display-property:0c30c4 - run-in boxes fall back to block (run-in reparenting not implemented, matches browser behavior)
2946 // +spec:display-property:2f5c52 - run-in treated as block (full run-in merging not implemented)
2947 LayoutDisplay::RunIn | LayoutDisplay::Marker => {
2948 FormattingContext::Block {
2949 establishes_new_context: true,
2950 }
2951 }
2952 }
2953}
2954
2955/// The logic now correctly identifies all BFC roots.
2956fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
2957 let node_data = &styled_dom.node_data.as_container()[node_id];
2958 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2959 return FormattingContext::Inline;
2960 }
2961 let display_type = get_display_type(styled_dom, node_id);
2962 determine_formatting_context_for_display(styled_dom, node_id, display_type)
2963}