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