azul_layout/solver3/layout_tree.rs
1//! solver3/layout_tree.rs
2//!
3//! Layout tree generation and anonymous box handling
4use std::{
5 collections::BTreeMap,
6 hash::{Hash, Hasher},
7 sync::{
8 atomic::{AtomicU32, Ordering},
9 Arc,
10 },
11};
12
13use crate::text3::cache::UnifiedConstraints;
14
15/// Global counter for IFC IDs. Resets to 0 when layout() callback is invoked.
16static IFC_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
17
18/// Unique identifier for an Inline Formatting Context (IFC).
19///
20/// An IFC represents a region where inline content (text, inline-blocks, images)
21/// is laid out together. One IFC can contain content from multiple DOM nodes
22/// (e.g., `<p>Hello <span>world</span>!</p>` is one IFC with 3 text runs).
23///
24/// The ID is generated using a global atomic counter that resets at the start
25/// of each layout pass. This ensures:
26/// - IDs are unique within a layout pass
27/// - The same logical IFC gets the same ID across frames (for selection stability)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub struct IfcId(pub u32);
30
31impl IfcId {
32 /// Generate a new unique IFC ID.
33 pub fn unique() -> Self {
34 Self(IFC_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
35 }
36
37 /// Reset the IFC ID counter. Called at the start of each layout pass.
38 pub fn reset_counter() {
39 IFC_ID_COUNTER.store(0, Ordering::Relaxed);
40 }
41}
42
43/// Tracks a layout node's membership in an Inline Formatting Context.
44///
45/// Text nodes don't store their own `inline_layout_result` - instead, they
46/// participate in their parent's IFC. This struct provides the link from
47/// a text node back to its IFC's layout data.
48///
49/// # Architecture
50///
51/// ```text
52/// DOM: <p>Hello <span>world</span>!</p>
53///
54/// Layout Tree:
55/// ├── LayoutNode (p) - IFC root
56/// │ └── inline_layout_result: Some(UnifiedLayout)
57/// │ └── ifc_id: IfcId(5)
58/// │
59/// ├── LayoutNode (::text "Hello ")
60/// │ └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 0 })
61/// │
62/// ├── LayoutNode (span)
63/// │ └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
64/// │ └── LayoutNode (::text "world")
65/// │ └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
66/// │
67/// └── LayoutNode (::text "!")
68/// └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 2 })
69/// ```
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct IfcMembership {
72 /// The IFC ID this node's content was laid out in.
73 pub ifc_id: IfcId,
74 /// The index of the IFC root LayoutNode in the layout tree.
75 /// Used to quickly find the node with `inline_layout_result`.
76 pub ifc_root_layout_index: usize,
77 /// Which run index within the IFC corresponds to this node's text.
78 /// Maps to `ContentIndex::run_index` in the shaped items.
79 pub run_index: u32,
80}
81
82use azul_core::{
83 dom::{FormattingContext, NodeId, NodeType},
84 geom::{LogicalPosition, LogicalRect, LogicalSize},
85 styled_dom::StyledDom,
86};
87use azul_css::{
88 corety::LayoutDebugMessage,
89 css::CssPropertyValue,
90 format_rust_code::GetHash,
91 props::{
92 basic::{
93 pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
94 },
95 layout::{LayoutDisplay, LayoutFloat, LayoutOverflow, LayoutPosition},
96 property::{CssProperty, CssPropertyType},
97 },
98};
99use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
100
101#[cfg(feature = "text_layout")]
102use crate::text3;
103use crate::{
104 debug_log,
105 font::parsed::ParsedFont,
106 font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
107 solver3::{
108 geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
109 getters::{get_float, get_overflow_x, get_overflow_y, get_position},
110 scrollbar::ScrollbarRequirements,
111 LayoutContext, Result,
112 },
113 text3::cache::AvailableSpace,
114};
115
116/// Represents the invalidation state of a layout node.
117///
118/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
119/// A node marked for `Layout` does not also need to be marked for `Paint`.
120///
121/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
122///
123/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
124/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
125/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
127pub enum DirtyFlag {
128 /// The node's layout is valid and no repaint is needed. This is the "clean" state.
129 #[default]
130 None,
131 /// The node's geometry is valid, but its appearance (e.g., color) has changed.
132 /// Requires a display list update only.
133 Paint,
134 /// The node's geometry (size or position) is invalid.
135 /// Requires a full layout pass and a display list update.
136 Layout,
137}
138
139/// A hash that represents the content and style of a node PLUS all of its descendants.
140/// If two SubtreeHashes are equal, their entire subtrees are considered identical for layout
141/// purposes.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
143pub struct SubtreeHash(pub u64);
144
145/// Cached inline layout result with the constraints used to compute it.
146///
147/// This structure solves a fundamental architectural problem: inline layouts
148/// (text wrapping, inline-block positioning) depend on the available width.
149/// Different layout phases may compute the layout with different widths:
150///
151/// 1. **Min-content measurement**: width = MinContent (effectively 0)
152/// 2. **Max-content measurement**: width = MaxContent (effectively infinite)
153/// 3. **Final layout**: width = Definite(actual_column_width)
154///
155/// Without tracking which constraints were used, a cached result from phase 1
156/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
157/// positions (the root cause of table cell width bugs).
158///
159/// By storing the constraints alongside the result, we can:
160/// - Invalidate the cache when constraints change
161/// - Keep multiple cached results for different constraint types if needed
162/// - Ensure the final render always uses a layout computed with correct widths
163#[derive(Debug, Clone)]
164pub struct CachedInlineLayout {
165 /// The computed inline layout
166 pub layout: Arc<UnifiedLayout>,
167 /// The available width constraint used to compute this layout.
168 /// This is the key for cache validity checking.
169 pub available_width: AvailableSpace,
170 /// Whether this layout was computed with float exclusions.
171 /// Float-aware layouts should not be overwritten by non-float layouts.
172 pub has_floats: bool,
173 /// The full constraints used to compute this layout.
174 /// Used for quick relayout after text edits without rebuilding from CSS.
175 pub constraints: Option<UnifiedConstraints>,
176}
177
178impl CachedInlineLayout {
179 /// Creates a new cached inline layout.
180 pub fn new(
181 layout: Arc<UnifiedLayout>,
182 available_width: AvailableSpace,
183 has_floats: bool,
184 ) -> Self {
185 Self {
186 layout,
187 available_width,
188 has_floats,
189 constraints: None,
190 }
191 }
192
193 /// Creates a new cached inline layout with full constraints.
194 pub fn new_with_constraints(
195 layout: Arc<UnifiedLayout>,
196 available_width: AvailableSpace,
197 has_floats: bool,
198 constraints: UnifiedConstraints,
199 ) -> Self {
200 Self {
201 layout,
202 available_width,
203 has_floats,
204 constraints: Some(constraints),
205 }
206 }
207
208 /// Checks if this cached layout is valid for the given constraints.
209 ///
210 /// A cached layout is valid if:
211 /// 1. The available width matches (definite widths must be equal, or both are the same
212 /// indefinite type)
213 /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
214 ///
215 /// The second condition preserves float-aware layouts, which are more "correct" than
216 /// non-float layouts and shouldn't be overwritten.
217 pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
218 // If we have a float-aware layout and the new request doesn't have floats,
219 // keep the float-aware layout (it's more accurate)
220 if self.has_floats && !new_has_floats {
221 // But only if the width constraint type matches
222 return self.width_constraint_matches(new_width);
223 }
224
225 // Otherwise, require exact width match
226 self.width_constraint_matches(new_width)
227 }
228
229 /// Checks if the width constraint matches.
230 fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
231 match (self.available_width, new_width) {
232 // Definite widths must match within a small epsilon
233 (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
234 (old - new).abs() < 0.1
235 }
236 // MinContent matches MinContent
237 (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
238 // MaxContent matches MaxContent
239 (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
240 // Different constraint types don't match
241 _ => false,
242 }
243 }
244
245 /// Determines if this cached layout should be replaced by a new layout.
246 ///
247 /// Returns true if the new layout should replace this one.
248 pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
249 // Always replace if we gain float information
250 if new_has_floats && !self.has_floats {
251 return true;
252 }
253
254 // Replace if width constraint changed
255 !self.width_constraint_matches(new_width)
256 }
257
258 /// Returns a reference to the inner UnifiedLayout.
259 ///
260 /// This is a convenience method for code that only needs the layout data
261 /// and doesn't care about the caching metadata.
262 #[inline]
263 pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
264 &self.layout
265 }
266
267 /// Returns a clone of the inner Arc<UnifiedLayout>.
268 ///
269 /// This is useful for APIs that need to return an owned reference
270 /// to the layout without exposing the caching metadata.
271 #[inline]
272 pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
273 self.layout.clone()
274 }
275}
276
277/// A layout tree node representing the CSS box model
278///
279/// Note: An absolute position is a final paint-time value and shouldn't be
280/// cached on the node itself, as it can change even if the node's
281/// layout is clean (e.g., if a sibling changes size). We will calculate
282/// it in a separate map.
283#[derive(Debug, Clone)]
284pub struct LayoutNode {
285 /// Reference back to the original DOM node (None for anonymous boxes)
286 pub dom_node_id: Option<NodeId>,
287 /// Pseudo-element type (::marker, ::before, ::after) if this node is a pseudo-element
288 pub pseudo_element: Option<PseudoElement>,
289 /// Whether this is an anonymous box generated by the layout engine
290 pub is_anonymous: bool,
291 /// Type of anonymous box (if applicable)
292 pub anonymous_type: Option<AnonymousBoxType>,
293 /// Children indices in the layout tree
294 pub children: Vec<usize>,
295 /// Parent index (None for root)
296 pub parent: Option<usize>,
297 /// Dirty flags to track what needs recalculation.
298 pub dirty_flag: DirtyFlag,
299 /// The resolved box model properties (margin, border, padding)
300 /// in logical pixels.
301 pub box_props: BoxProps,
302 /// Cache for Taffy layout computations for this node.
303 pub taffy_cache: TaffyCache, // NEW FIELD
304 /// A hash of this node's data (style, text content, etc.) used for
305 /// fast reconciliation.
306 pub node_data_hash: u64,
307 /// A hash of this node's data and all of its descendants. Used for
308 /// fast reconciliation.
309 pub subtree_hash: SubtreeHash,
310 /// The formatting context this node establishes or participates in.
311 pub formatting_context: FormattingContext,
312 /// Parent's formatting context (needed to determine if stretch applies)
313 pub parent_formatting_context: Option<FormattingContext>,
314 /// Cached intrinsic sizes (min-content, max-content, etc.)
315 pub intrinsic_sizes: Option<IntrinsicSizes>,
316 /// The size used during the last layout pass.
317 pub used_size: Option<LogicalSize>,
318 /// The position of this node *relative to its parent's content box*.
319 pub relative_position: Option<LogicalPosition>,
320 /// The baseline of this box, if applicable, measured from its content-box top edge.
321 pub baseline: Option<f32>,
322 /// Cached inline layout result with the constraints used to compute it.
323 ///
324 /// This field stores both the computed layout AND the constraints (available width,
325 /// float state) under which it was computed. This is essential for correctness:
326 /// - Table cells are measured multiple times with different widths
327 /// - Min-content/max-content intrinsic sizing uses special constraint values
328 /// - The final layout must use the actual available width, not a measurement width
329 ///
330 /// By tracking the constraints, we avoid the bug where a min-content measurement
331 /// (with width=0) would be incorrectly reused for final rendering.
332 pub inline_layout_result: Option<CachedInlineLayout>,
333 /// Escaped top margin (CSS 2.1 margin collapsing)
334 /// If this BFC's first child's top margin "escaped" the BFC, this contains
335 /// the collapsed margin that should be applied by the parent.
336 pub escaped_top_margin: Option<f32>,
337 /// Escaped bottom margin (CSS 2.1 margin collapsing)
338 /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
339 /// the collapsed margin that should be applied by the parent.
340 pub escaped_bottom_margin: Option<f32>,
341 /// Cached scrollbar information (calculated during layout)
342 /// Used to determine if scrollbars appeared/disappeared requiring reflow
343 pub scrollbar_info: Option<ScrollbarRequirements>,
344 /// The actual content size (children overflow size) for scrollable containers.
345 /// This is the size of all content that might need to be scrolled, which can
346 /// be larger than `used_size` when content overflows the container.
347 pub overflow_content_size: Option<LogicalSize>,
348 /// If this node is an IFC root, stores the IFC ID.
349 /// Used to identify which IFC this node's `inline_layout_result` belongs to.
350 pub ifc_id: Option<IfcId>,
351 /// If this node participates in an IFC (is inline content like text),
352 /// stores the reference back to the IFC root and the run index.
353 /// This allows text nodes to find their layout data in the parent's IFC.
354 pub ifc_membership: Option<IfcMembership>,
355}
356
357impl LayoutNode {
358 /// Calculates the actual content size of this node, including all children and text.
359 /// This is used to determine if scrollbars should appear for overflow: auto.
360 pub fn get_content_size(&self) -> LogicalSize {
361 // First, check if we have overflow_content_size from layout computation
362 if let Some(content_size) = self.overflow_content_size {
363 return content_size;
364 }
365
366 // Fall back to computing from used_size and text layout
367 let mut content_size = self.used_size.unwrap_or_default();
368
369 // If this node has text layout, calculate the bounds of all text items
370 if let Some(ref cached_layout) = self.inline_layout_result {
371 let text_layout = &cached_layout.layout;
372 // Find the maximum extent of all positioned items
373 let mut max_x: f32 = 0.0;
374 let mut max_y: f32 = 0.0;
375
376 for positioned_item in &text_layout.items {
377 let item_bounds = positioned_item.item.bounds();
378 let item_right = positioned_item.position.x + item_bounds.width;
379 let item_bottom = positioned_item.position.y + item_bounds.height;
380
381 max_x = max_x.max(item_right);
382 max_y = max_y.max(item_bottom);
383 }
384
385 // Use the maximum extent as content size if it's larger
386 content_size.width = content_size.width.max(max_x);
387 content_size.height = content_size.height.max(max_y);
388 }
389
390 // TODO: Also check children positions to get max content bounds
391 // For now, this handles the most common case (text overflowing)
392
393 content_size
394 }
395}
396
397/// CSS pseudo-elements that can be generated
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum PseudoElement {
400 /// ::marker pseudo-element for list items
401 Marker,
402 /// ::before pseudo-element
403 Before,
404 /// ::after pseudo-element
405 After,
406}
407
408/// Types of anonymous boxes that can be generated
409#[derive(Debug, Clone, Copy, PartialEq)]
410pub enum AnonymousBoxType {
411 /// Anonymous block box wrapping inline content
412 InlineWrapper,
413 /// Anonymous box for a list item marker (bullet or number)
414 /// DEPRECATED: Use PseudoElement::Marker instead
415 ListItemMarker,
416 /// Anonymous table wrapper
417 TableWrapper,
418 /// Anonymous table row group (tbody)
419 TableRowGroup,
420 /// Anonymous table row
421 TableRow,
422 /// Anonymous table cell
423 TableCell,
424}
425
426/// The complete layout tree structure
427#[derive(Debug, Clone)]
428pub struct LayoutTree {
429 /// Arena-style storage for layout nodes
430 pub nodes: Vec<LayoutNode>,
431 /// Root node index
432 pub root: usize,
433 /// Mapping from DOM node IDs to layout node indices
434 pub dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
435}
436
437impl LayoutTree {
438 pub fn get(&self, index: usize) -> Option<&LayoutNode> {
439 self.nodes.get(index)
440 }
441
442 pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
443 self.nodes.get_mut(index)
444 }
445
446 pub fn root_node(&self) -> &LayoutNode {
447 &self.nodes[self.root]
448 }
449
450 /// Marks a node and its ancestors as dirty with the given flag.
451 ///
452 /// The dirty state is "upgraded" if the new flag is more severe than the
453 /// existing one (e.g., upgrading from `Paint` to `Layout`). Propagation stops
454 /// if an ancestor is already marked with an equal or more severe flag.
455 pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
456 // A "None" flag is a no-op for marking dirty.
457 if flag == DirtyFlag::None {
458 return;
459 }
460
461 let mut current_index = Some(start_index);
462 while let Some(index) = current_index {
463 if let Some(node) = self.get_mut(index) {
464 // If the node's current flag is already as dirty or dirtier,
465 // then all ancestors are also sufficiently marked, so we can stop.
466 if node.dirty_flag >= flag {
467 break;
468 }
469
470 // Upgrade the flag to the new, more severe state.
471 node.dirty_flag = flag;
472 current_index = node.parent;
473 } else {
474 break;
475 }
476 }
477 }
478
479 /// Marks a node and its entire subtree of descendants with the given dirty flag.
480 ///
481 /// This is used for inherited CSS properties. Each node in the subtree
482 /// will be upgraded to at least the new flag's severity.
483 pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
484 // A "None" flag is a no-op.
485 if flag == DirtyFlag::None {
486 return;
487 }
488
489 // Using a stack for an iterative traversal to avoid deep recursion
490 // on large subtrees.
491 let mut stack = vec![start_index];
492 while let Some(index) = stack.pop() {
493 if let Some(node) = self.get_mut(index) {
494 // Only update if the new flag is an upgrade.
495 if node.dirty_flag < flag {
496 node.dirty_flag = flag;
497 }
498 // Add all children to be processed.
499 stack.extend_from_slice(&node.children);
500 }
501 }
502 }
503
504 /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
505 pub fn clear_all_dirty_flags(&mut self) {
506 for node in &mut self.nodes {
507 node.dirty_flag = DirtyFlag::None;
508 }
509 }
510
511 /// Get inline layout for a node, navigating through IFC membership if needed.
512 ///
513 /// For text nodes that participate in an IFC (Inline Formatting Context),
514 /// the actual `inline_layout_result` is stored on the IFC root node (the block
515 /// container), not on the text node itself. This method handles both cases:
516 ///
517 /// 1. If the node has its own `inline_layout_result`, return it directly
518 /// 2. If the node has `ifc_membership`, navigate to the IFC root and return its layout
519 ///
520 /// This mirrors the W3C Selection model where:
521 /// - Selection.focusNode points to the TEXT node
522 /// - But the layout data is owned by the containing block
523 ///
524 /// # Arguments
525 /// * `layout_index` - The index of the layout node in the tree
526 ///
527 /// # Returns
528 /// The inline layout for the node's IFC, or `None` if no layout is available
529 pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
530 let layout_node = self.nodes.get(layout_index)?;
531
532 // First, check if this node has its own inline_layout_result (it's an IFC root)
533 if let Some(cached) = &layout_node.inline_layout_result {
534 return Some(cached.get_layout());
535 }
536
537 // For text nodes, check if they have ifc_membership pointing to the IFC root
538 if let Some(ifc_membership) = &layout_node.ifc_membership {
539 let ifc_root_node = self.nodes.get(ifc_membership.ifc_root_layout_index)?;
540 if let Some(cached) = &ifc_root_node.inline_layout_result {
541 return Some(cached.get_layout());
542 }
543 }
544
545 None
546 }
547}
548
549/// Generate layout tree from styled DOM with proper anonymous box generation
550pub fn generate_layout_tree<T: ParsedFontTrait>(
551 ctx: &mut LayoutContext<'_, T>,
552) -> Result<LayoutTree> {
553 let mut builder = LayoutTreeBuilder::new();
554 let root_id = ctx
555 .styled_dom
556 .root
557 .into_crate_internal()
558 .unwrap_or(NodeId::ZERO);
559 let root_index =
560 builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
561 let layout_tree = builder.build(root_index);
562
563 debug_log!(
564 ctx,
565 "Generated layout tree with {} nodes (incl. anonymous)",
566 layout_tree.nodes.len()
567 );
568
569 Ok(layout_tree)
570}
571
572pub struct LayoutTreeBuilder {
573 nodes: Vec<LayoutNode>,
574 dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
575}
576
577impl LayoutTreeBuilder {
578 pub fn new() -> Self {
579 Self {
580 nodes: Vec::new(),
581 dom_to_layout: BTreeMap::new(),
582 }
583 }
584
585 pub fn get(&self, index: usize) -> Option<&LayoutNode> {
586 self.nodes.get(index)
587 }
588
589 pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
590 self.nodes.get_mut(index)
591 }
592
593 /// Main entry point for recursively building the layout tree.
594 /// This function dispatches to specialized handlers based on the node's
595 /// `display` property to correctly generate anonymous boxes.
596 pub fn process_node(
597 &mut self,
598 styled_dom: &StyledDom,
599 dom_id: NodeId,
600 parent_idx: Option<usize>,
601 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
602 ) -> Result<usize> {
603 let node_data = &styled_dom.node_data.as_container()[dom_id];
604 let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages)?;
605 let display_type = get_display_type(styled_dom, dom_id);
606
607 // If this is a list-item, inject a ::marker pseudo-element as its first child
608 // Per CSS spec, the ::marker is generated as the first child of the list-item
609 if display_type == LayoutDisplay::ListItem {
610 self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
611 }
612
613 match display_type {
614 LayoutDisplay::Block
615 | LayoutDisplay::InlineBlock
616 | LayoutDisplay::FlowRoot
617 | LayoutDisplay::ListItem => {
618 self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?
619 }
620 LayoutDisplay::Table => {
621 self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?
622 }
623 LayoutDisplay::TableRowGroup => {
624 self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?
625 }
626 LayoutDisplay::TableRow => {
627 self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?
628 }
629 // Inline, TableCell, etc., have their children processed as part of their
630 // formatting context layout and don't require anonymous box generation at this stage.
631 _ => {
632 let children: Vec<NodeId> = dom_id
633 .az_children(&styled_dom.node_hierarchy.as_container())
634 .collect();
635
636 for child_dom_id in children {
637 self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
638 }
639 }
640 }
641 Ok(node_idx)
642 }
643
644 /// Handles children of a block-level element, creating anonymous block
645 /// wrappers for consecutive runs of inline-level children if necessary.
646 fn process_block_children(
647 &mut self,
648 styled_dom: &StyledDom,
649 parent_dom_id: NodeId,
650 parent_idx: usize,
651 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
652 ) -> Result<()> {
653 let children: Vec<NodeId> = parent_dom_id
654 .az_children(&styled_dom.node_hierarchy.as_container())
655 .collect();
656
657 // Debug: log which children we found
658 if let Some(msgs) = debug_messages.as_mut() {
659 msgs.push(LayoutDebugMessage::info(format!(
660 "[process_block_children] DOM node {} has {} children: {:?}",
661 parent_dom_id.index(),
662 children.len(),
663 children.iter().map(|c| c.index()).collect::<Vec<_>>()
664 )));
665 }
666
667 let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
668
669 if let Some(msgs) = debug_messages.as_mut() {
670 msgs.push(LayoutDebugMessage::info(format!(
671 "[process_block_children] has_block_child={}, children display types: {:?}",
672 has_block_child,
673 children
674 .iter()
675 .map(|c| {
676 let dt = get_display_type(styled_dom, *c);
677 let is_block = is_block_level(styled_dom, *c);
678 format!("{}:{:?}(block={})", c.index(), dt, is_block)
679 })
680 .collect::<Vec<_>>()
681 )));
682 }
683
684 if !has_block_child {
685 // All children are inline, no anonymous boxes needed.
686 if let Some(msgs) = debug_messages.as_mut() {
687 msgs.push(LayoutDebugMessage::info(format!(
688 "[process_block_children] All inline, processing {} children directly",
689 children.len()
690 )));
691 }
692 for child_id in children {
693 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
694 }
695 return Ok(());
696 }
697
698 // Mixed block and inline content requires anonymous wrappers.
699 let mut inline_run = Vec::new();
700
701 for child_id in children {
702 if is_block_level(styled_dom, child_id) {
703 // End the current inline run
704 if !inline_run.is_empty() {
705 if let Some(msgs) = debug_messages.as_mut() {
706 msgs.push(LayoutDebugMessage::info(format!(
707 "[process_block_children] Creating anon wrapper for inline run: {:?}",
708 inline_run
709 .iter()
710 .map(|c: &NodeId| c.index())
711 .collect::<Vec<_>>()
712 )));
713 }
714 let anon_idx = self.create_anonymous_node(
715 parent_idx,
716 AnonymousBoxType::InlineWrapper,
717 FormattingContext::Block {
718 // Anonymous wrappers are BFC roots
719 establishes_new_context: true,
720 },
721 );
722 for inline_child_id in inline_run.drain(..) {
723 self.process_node(
724 styled_dom,
725 inline_child_id,
726 Some(anon_idx),
727 debug_messages,
728 )?;
729 }
730 }
731 // Process the block-level child directly
732 if let Some(msgs) = debug_messages.as_mut() {
733 msgs.push(LayoutDebugMessage::info(format!(
734 "[process_block_children] Processing block child DOM {}",
735 child_id.index()
736 )));
737 }
738 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
739 } else {
740 inline_run.push(child_id);
741 }
742 }
743 // Process any remaining inline children at the end
744 if !inline_run.is_empty() {
745 if let Some(msgs) = debug_messages.as_mut() {
746 msgs.push(LayoutDebugMessage::info(format!(
747 "[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
748 inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
749 )));
750 }
751 let anon_idx = self.create_anonymous_node(
752 parent_idx,
753 AnonymousBoxType::InlineWrapper,
754 FormattingContext::Block {
755 establishes_new_context: true, // Anonymous wrappers are BFC roots
756 },
757 );
758 for inline_child_id in inline_run {
759 self.process_node(styled_dom, inline_child_id, Some(anon_idx), debug_messages)?;
760 }
761 }
762
763 Ok(())
764 }
765
766 /// CSS 2.2 Section 17.2.1 - Anonymous box generation for tables:
767 /// "Generate missing child wrappers. If a child C of a table-row parent P is not a
768 /// table-cell, then generate an anonymous table-cell box around C and all consecutive
769 /// siblings of C that are not table-cells."
770 ///
771 /// Handles children of a `display: table`, inserting anonymous `table-row`
772 /// wrappers for any direct `table-cell` children.
773 ///
774 /// Per CSS 2.2 Section 17.2.1, Stage 2 & 3:
775 /// - Stage 2: Wrap consecutive table-cell children in anonymous table-rows
776 /// - Stage 1 (implemented here): Skip whitespace-only text nodes
777 fn process_table_children(
778 &mut self,
779 styled_dom: &StyledDom,
780 parent_dom_id: NodeId,
781 parent_idx: usize,
782 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
783 ) -> Result<()> {
784 let parent_display = get_display_type(styled_dom, parent_dom_id);
785 let mut row_children = Vec::new();
786
787 for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
788 // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
789 // "Remove all irrelevant boxes. These are boxes that do not contain table-related
790 // boxes and do not themselves have 'display' set to a table-related value."
791 if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
792 continue;
793 }
794
795 let child_display = get_display_type(styled_dom, child_id);
796
797 // CSS 2.2 Section 17.2.1, Stage 2:
798 // "Generate missing child wrappers"
799 if child_display == LayoutDisplay::TableCell {
800 // Accumulate consecutive table-cell children
801 row_children.push(child_id);
802 } else {
803 // CSS 2.2 Section 17.2.1, Stage 2:
804 // If we have accumulated cells, wrap them in an anonymous table-row
805 if !row_children.is_empty() {
806 let anon_row_idx = self.create_anonymous_node(
807 parent_idx,
808 AnonymousBoxType::TableRow,
809 FormattingContext::TableRow,
810 );
811
812 for cell_id in row_children.drain(..) {
813 self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
814 }
815 }
816
817 // Process non-cell child (could be row, row-group, caption, etc.)
818 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
819 }
820 }
821
822 // CSS 2.2 Section 17.2.1, Stage 2:
823 // Flush any remaining accumulated cells
824 if !row_children.is_empty() {
825 let anon_row_idx = self.create_anonymous_node(
826 parent_idx,
827 AnonymousBoxType::TableRow,
828 FormattingContext::TableRow,
829 );
830
831 for cell_id in row_children {
832 self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
833 }
834 }
835
836 Ok(())
837 }
838
839 /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
840 /// Handles children of a `display: table-row-group`, `table-header-group`,
841 /// or `table-footer-group`, inserting anonymous `table-row` wrappers as needed.
842 ///
843 /// The logic is identical to process_table_children per CSS 2.2 Section 17.2.1:
844 /// "If a child C of a table-row-group parent P is not a table-row, then generate
845 /// an anonymous table-row box around C and all consecutive siblings of C that are
846 /// not table-rows."
847 fn process_table_row_group_children(
848 &mut self,
849 styled_dom: &StyledDom,
850 parent_dom_id: NodeId,
851 parent_idx: usize,
852 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
853 ) -> Result<()> {
854 // CSS 2.2 Section 17.2.1: Row groups need the same anonymous box generation
855 // as tables (wrapping consecutive non-row children in anonymous rows)
856 self.process_table_children(styled_dom, parent_dom_id, parent_idx, debug_messages)
857 }
858
859 /// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 2:
860 /// "Generate missing child wrappers. If a child C of a table-row parent P is not a
861 /// table-cell, then generate an anonymous table-cell box around C and all consecutive
862 /// siblings of C that are not table-cells."
863 ///
864 /// Handles children of a `display: table-row`, inserting anonymous `table-cell` wrappers
865 /// for any non-cell children.
866 fn process_table_row_children(
867 &mut self,
868 styled_dom: &StyledDom,
869 parent_dom_id: NodeId,
870 parent_idx: usize,
871 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
872 ) -> Result<()> {
873 let parent_display = get_display_type(styled_dom, parent_dom_id);
874
875 for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
876 // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
877 if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
878 continue;
879 }
880
881 let child_display = get_display_type(styled_dom, child_id);
882
883 // CSS 2.2 Section 17.2.1, Stage 2:
884 // "If a child C of a table-row parent P is not a table-cell, then generate
885 // an anonymous table-cell box around C"
886 if child_display == LayoutDisplay::TableCell {
887 // Normal table cell - process directly
888 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
889 } else {
890 // CSS 2.2 Section 17.2.1, Stage 2:
891 // Non-cell child must be wrapped in an anonymous table-cell
892 let anon_cell_idx = self.create_anonymous_node(
893 parent_idx,
894 AnonymousBoxType::TableCell,
895 FormattingContext::Block {
896 establishes_new_context: true,
897 },
898 );
899
900 self.process_node(styled_dom, child_id, Some(anon_cell_idx), debug_messages)?;
901 }
902 }
903
904 Ok(())
905 }
906 /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
907 /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
908 /// to satisfy the constraints of the table model."
909 ///
910 /// Helper to create an anonymous node in the tree.
911 /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
912 /// the CSS box model structure (e.g., wrapping inline content in blocks,
913 /// or creating missing table structural elements).
914 pub fn create_anonymous_node(
915 &mut self,
916 parent: usize,
917 anon_type: AnonymousBoxType,
918 fc: FormattingContext,
919 ) -> usize {
920 let index = self.nodes.len();
921
922 // CSS 2.2 Section 17.2.1: Anonymous boxes inherit properties from their
923 // enclosing non-anonymous box
924 let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
925
926 self.nodes.push(LayoutNode {
927 // Anonymous boxes have no DOM correspondence
928 dom_node_id: None,
929 pseudo_element: None,
930 parent: Some(parent),
931 formatting_context: fc,
932 parent_formatting_context: parent_fc,
933 // Anonymous boxes inherit from parent
934 box_props: BoxProps::default(),
935 taffy_cache: TaffyCache::new(),
936 is_anonymous: true,
937 anonymous_type: Some(anon_type),
938 children: Vec::new(),
939 dirty_flag: DirtyFlag::Layout,
940 // Anonymous boxes don't have style/data
941 node_data_hash: 0,
942 subtree_hash: SubtreeHash(0),
943 intrinsic_sizes: None,
944 used_size: None,
945 relative_position: None,
946 baseline: None,
947 inline_layout_result: None,
948 escaped_top_margin: None,
949 escaped_bottom_margin: None,
950 scrollbar_info: None,
951 overflow_content_size: None,
952 ifc_id: None,
953 ifc_membership: None,
954 });
955
956 self.nodes[parent].children.push(index);
957 index
958 }
959
960 /// Creates a ::marker pseudo-element as the first child of a list-item.
961 ///
962 /// Per CSS Lists Module Level 3, Section 3.1:
963 /// "For elements with display: list-item, user agents must generate a
964 /// ::marker pseudo-element as the first child of the principal box."
965 ///
966 /// The ::marker references the same DOM node as its parent list-item,
967 /// but is marked as a pseudo-element for proper counter resolution and styling.
968 pub fn create_marker_pseudo_element(
969 &mut self,
970 styled_dom: &StyledDom,
971 list_item_dom_id: NodeId,
972 list_item_idx: usize,
973 ) -> usize {
974 let index = self.nodes.len();
975
976 // The marker references the same DOM node as the list-item
977 // This is important for style resolution (the marker inherits from the list-item)
978 let parent_fc = self
979 .nodes
980 .get(list_item_idx)
981 .map(|n| n.formatting_context.clone());
982 self.nodes.push(LayoutNode {
983 dom_node_id: Some(list_item_dom_id),
984 pseudo_element: Some(PseudoElement::Marker),
985 parent: Some(list_item_idx),
986 // Markers contain inline text
987 formatting_context: FormattingContext::Inline,
988 parent_formatting_context: parent_fc,
989 // Will be resolved from ::marker styles
990 box_props: BoxProps::default(),
991 taffy_cache: TaffyCache::new(),
992 // Pseudo-elements are not anonymous boxes
993 is_anonymous: false,
994 anonymous_type: None,
995 children: Vec::new(),
996 dirty_flag: DirtyFlag::Layout,
997 // Pseudo-elements don't have separate style in current impl
998 node_data_hash: 0,
999 subtree_hash: SubtreeHash(0),
1000 intrinsic_sizes: None,
1001 used_size: None,
1002 relative_position: None,
1003 baseline: None,
1004 inline_layout_result: None,
1005 escaped_top_margin: None,
1006 escaped_bottom_margin: None,
1007 scrollbar_info: None,
1008 overflow_content_size: None,
1009 ifc_id: None,
1010 ifc_membership: None,
1011 });
1012
1013 // Insert as FIRST child (per spec)
1014 self.nodes[list_item_idx].children.insert(0, index);
1015
1016 // Register with DOM mapping for counter resolution
1017 self.dom_to_layout
1018 .entry(list_item_dom_id)
1019 .or_default()
1020 .push(index);
1021
1022 index
1023 }
1024
1025 pub fn create_node_from_dom(
1026 &mut self,
1027 styled_dom: &StyledDom,
1028 dom_id: NodeId,
1029 parent: Option<usize>,
1030 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1031 ) -> Result<usize> {
1032 let index = self.nodes.len();
1033 let parent_fc =
1034 parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1035 self.nodes.push(LayoutNode {
1036 dom_node_id: Some(dom_id),
1037 pseudo_element: None,
1038 parent,
1039 formatting_context: determine_formatting_context(styled_dom, dom_id),
1040 parent_formatting_context: parent_fc,
1041 box_props: resolve_box_props(styled_dom, dom_id, debug_messages),
1042 taffy_cache: TaffyCache::new(),
1043 is_anonymous: false,
1044 anonymous_type: None,
1045 children: Vec::new(),
1046 dirty_flag: DirtyFlag::Layout,
1047 node_data_hash: hash_node_data(styled_dom, dom_id),
1048 subtree_hash: SubtreeHash(0),
1049 intrinsic_sizes: None,
1050 used_size: None,
1051 relative_position: None,
1052 baseline: None,
1053 inline_layout_result: None,
1054 escaped_top_margin: None,
1055 escaped_bottom_margin: None,
1056 scrollbar_info: None,
1057 overflow_content_size: None,
1058 ifc_id: None,
1059 ifc_membership: None,
1060 });
1061 if let Some(p) = parent {
1062 self.nodes[p].children.push(index);
1063 }
1064 self.dom_to_layout.entry(dom_id).or_default().push(index);
1065 Ok(index)
1066 }
1067
1068 pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
1069 let index = self.nodes.len();
1070 let mut new_node = old_node.clone();
1071 new_node.parent = parent;
1072 new_node.parent_formatting_context =
1073 parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1074 new_node.children = Vec::new();
1075 new_node.dirty_flag = DirtyFlag::None;
1076 self.nodes.push(new_node);
1077 if let Some(p) = parent {
1078 self.nodes[p].children.push(index);
1079 }
1080 if let Some(dom_id) = old_node.dom_node_id {
1081 self.dom_to_layout.entry(dom_id).or_default().push(index);
1082 }
1083 index
1084 }
1085
1086 pub fn build(self, root_idx: usize) -> LayoutTree {
1087 LayoutTree {
1088 nodes: self.nodes,
1089 root: root_idx,
1090 dom_to_layout: self.dom_to_layout,
1091 }
1092 }
1093}
1094
1095pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1096 matches!(
1097 get_display_type(styled_dom, node_id),
1098 LayoutDisplay::Block
1099 | LayoutDisplay::FlowRoot
1100 | LayoutDisplay::Table
1101 | LayoutDisplay::TableRow
1102 | LayoutDisplay::TableRowGroup
1103 | LayoutDisplay::ListItem
1104 )
1105}
1106
1107/// Checks if a node is inline-level (including text nodes).
1108/// According to CSS spec, inline-level content includes:
1109///
1110/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
1111/// - Text nodes
1112/// - Generated content
1113fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1114 // Text nodes are always inline-level
1115 let node_data = &styled_dom.node_data.as_container()[node_id];
1116 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
1117 return true;
1118 }
1119
1120 // Check the display property
1121 matches!(
1122 get_display_type(styled_dom, node_id),
1123 LayoutDisplay::Inline
1124 | LayoutDisplay::InlineBlock
1125 | LayoutDisplay::InlineTable
1126 | LayoutDisplay::InlineFlex
1127 | LayoutDisplay::InlineGrid
1128 )
1129}
1130
1131/// Checks if a block container has only inline-level children.
1132/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
1133/// by a block container box that contains no block-level boxes."
1134fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1135 let hierarchy = styled_dom.node_hierarchy.as_container();
1136 let node_hier = match hierarchy.get(node_id) {
1137 Some(n) => n,
1138 None => {
1139 return false;
1140 }
1141 };
1142
1143 // Get the first child
1144 let mut current_child = node_hier.first_child_id(node_id);
1145
1146 // If there are no children, it's not an IFC (it's empty)
1147 if current_child.is_none() {
1148 return false;
1149 }
1150
1151 // Check all children
1152 while let Some(child_id) = current_child {
1153 let is_inline = is_inline_level(styled_dom, child_id);
1154
1155 if !is_inline {
1156 // Found a block-level child
1157 return false;
1158 }
1159
1160 // Move to next sibling
1161 if let Some(child_hier) = hierarchy.get(child_id) {
1162 current_child = child_hier.next_sibling_id();
1163 } else {
1164 break;
1165 }
1166 }
1167
1168 // All children are inline-level
1169 true
1170}
1171
1172fn hash_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
1173 let mut hasher = std::hash::DefaultHasher::new();
1174 // Use node_state flags and node_type as a reasonable surrogate for now.
1175 if let Some(styled_node) = dom.node_data.as_container().get(node_id) {
1176 styled_node.get_hash().hash(&mut hasher);
1177 }
1178 hasher.finish()
1179}
1180
1181/// Helper function to get element's computed font-size
1182fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
1183 use crate::solver3::getters::*;
1184
1185 let node_data = &styled_dom.node_data.as_container()[dom_id];
1186 let node_state = styled_dom
1187 .styled_nodes
1188 .as_container()
1189 .get(dom_id)
1190 .map(|n| &n.styled_node_state)
1191 .cloned()
1192 .unwrap_or_default();
1193
1194 let cache = &styled_dom.css_property_cache.ptr;
1195
1196 // Try to get from dependency chain first (proper resolution)
1197 if let Some(node_chains) = cache.dependency_chains.get(&dom_id) {
1198 if let Some(chain) = node_chains.get(&CssPropertyType::FontSize) {
1199 if let Some(cached) = chain.cached_pixels {
1200 return cached;
1201 }
1202 }
1203 }
1204
1205 // Fallback: get from property cache
1206 cache
1207 .get_font_size(node_data, &dom_id, &node_state)
1208 .and_then(|v| v.get_property().cloned())
1209 .map(|v| {
1210 // Fallback using hardcoded 16px base
1211 v.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE)
1212 })
1213 .unwrap_or(DEFAULT_FONT_SIZE)
1214}
1215
1216/// Helper function to get parent's computed font-size
1217fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
1218 styled_dom
1219 .node_hierarchy
1220 .as_container()
1221 .get(dom_id)
1222 .and_then(|node| node.parent_id())
1223 .map(|parent_id| get_element_font_size(styled_dom, parent_id))
1224 .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
1225}
1226
1227/// Helper function to get root element's font-size
1228fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
1229 // Root is always NodeId(0) in Azul
1230 get_element_font_size(styled_dom, NodeId::new(0))
1231}
1232
1233/// Create a ResolutionContext for a given node
1234fn create_resolution_context(
1235 styled_dom: &StyledDom,
1236 dom_id: NodeId,
1237 containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
1238) -> azul_css::props::basic::ResolutionContext {
1239 let element_font_size = get_element_font_size(styled_dom, dom_id);
1240 let parent_font_size = get_parent_font_size(styled_dom, dom_id);
1241 let root_font_size = get_root_font_size(styled_dom);
1242
1243 ResolutionContext {
1244 element_font_size,
1245 parent_font_size,
1246 root_font_size,
1247 containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
1248 element_size: None, // Not yet laid out
1249 viewport_size: PhysicalSize::new(0.0, 0.0),
1250 }
1251}
1252
1253fn resolve_box_props(
1254 styled_dom: &StyledDom,
1255 dom_id: NodeId,
1256 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1257) -> BoxProps {
1258 use crate::solver3::getters::*;
1259
1260 let node_data = &styled_dom.node_data.as_container()[dom_id];
1261
1262 // Get styled node state
1263 let node_state = styled_dom
1264 .styled_nodes
1265 .as_container()
1266 .get(dom_id)
1267 .map(|n| &n.styled_node_state)
1268 .cloned()
1269 .unwrap_or_default();
1270
1271 // Create resolution context for this element
1272 // Note: containing_block_size is None here because we don't have it yet
1273 // This is fine - margins/padding use containing block width, but we'll handle that later
1274 let context = create_resolution_context(styled_dom, dom_id, None);
1275
1276 // Helper to extract and resolve pixel value from MultiValue<PixelValue>
1277 let resolve_value = |mv: MultiValue<PixelValue>, prop_context: PropertyContext| -> f32 {
1278 match mv {
1279 MultiValue::Exact(pv) => pv.resolve_with_context(&context, prop_context),
1280 _ => 0.0,
1281 }
1282 };
1283
1284 // Read margin, padding, border from styled_dom
1285 let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
1286 let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
1287 let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
1288 let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
1289
1290 let margin = crate::solver3::geometry::EdgeSizes {
1291 top: resolve_value(margin_top_mv, PropertyContext::Margin),
1292 right: resolve_value(margin_right_mv, PropertyContext::Margin),
1293 bottom: resolve_value(margin_bottom_mv, PropertyContext::Margin),
1294 left: resolve_value(margin_left_mv, PropertyContext::Margin),
1295 };
1296
1297 // Debug for Body nodes
1298 if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
1299 if let Some(msgs) = debug_messages.as_mut() {
1300 msgs.push(LayoutDebugMessage::box_props(format!(
1301 "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
1302 margin.top, margin.right, margin.bottom, margin.left
1303 )));
1304 }
1305 }
1306
1307 let padding = crate::solver3::geometry::EdgeSizes {
1308 top: resolve_value(
1309 get_css_padding_top(styled_dom, dom_id, &node_state),
1310 PropertyContext::Padding,
1311 ),
1312 right: resolve_value(
1313 get_css_padding_right(styled_dom, dom_id, &node_state),
1314 PropertyContext::Padding,
1315 ),
1316 bottom: resolve_value(
1317 get_css_padding_bottom(styled_dom, dom_id, &node_state),
1318 PropertyContext::Padding,
1319 ),
1320 left: resolve_value(
1321 get_css_padding_left(styled_dom, dom_id, &node_state),
1322 PropertyContext::Padding,
1323 ),
1324 };
1325
1326 let border = crate::solver3::geometry::EdgeSizes {
1327 top: resolve_value(
1328 get_css_border_top_width(styled_dom, dom_id, &node_state),
1329 PropertyContext::Other,
1330 ),
1331 right: resolve_value(
1332 get_css_border_right_width(styled_dom, dom_id, &node_state),
1333 PropertyContext::Other,
1334 ),
1335 bottom: resolve_value(
1336 get_css_border_bottom_width(styled_dom, dom_id, &node_state),
1337 PropertyContext::Other,
1338 ),
1339 left: resolve_value(
1340 get_css_border_left_width(styled_dom, dom_id, &node_state),
1341 PropertyContext::Other,
1342 ),
1343 };
1344
1345 BoxProps {
1346 margin,
1347 padding,
1348 border,
1349 }
1350}
1351
1352/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
1353/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
1354/// and do not themselves have 'display' set to a table-related value. In this context,
1355/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
1356///
1357/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
1358/// Returns true if the node is a text node containing only whitespace characters.
1359fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1360 let binding = styled_dom.node_data.as_container();
1361 let node_data = binding.get(node_id);
1362 if let Some(data) = node_data {
1363 if let NodeType::Text(text) = data.get_node_type() {
1364 // Check if the text contains only whitespace characters
1365 // Per CSS 2.2 Section 17.2.1: whitespace-only anonymous boxes are irrelevant
1366 return text.chars().all(|c| c.is_whitespace());
1367 }
1368 }
1369
1370 false
1371}
1372
1373/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
1374/// Determines if a node should be skipped in table structure generation.
1375/// Whitespace-only text nodes are "irrelevant" and should not generate boxes
1376/// when they appear between table-related elements.
1377///
1378/// Returns true if the node should be skipped (i.e., it's whitespace-only text
1379/// and the parent is a table structural element).
1380fn should_skip_for_table_structure(
1381 styled_dom: &StyledDom,
1382 node_id: NodeId,
1383 parent_display: LayoutDisplay,
1384) -> bool {
1385 // CSS 2.2 Section 17.2.1: Only skip whitespace text nodes when parent is
1386 // a table structural element (table, row group, row)
1387 matches!(
1388 parent_display,
1389 LayoutDisplay::Table
1390 | LayoutDisplay::TableRowGroup
1391 | LayoutDisplay::TableHeaderGroup
1392 | LayoutDisplay::TableFooterGroup
1393 | LayoutDisplay::TableRow
1394 ) && is_whitespace_only_text(styled_dom, node_id)
1395}
1396
1397/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 3:
1398/// "Generate missing parents. For each table-cell box C in a sequence of consecutive
1399/// table-cell boxes (that are not part of a table-row), an anonymous table-row box
1400/// is generated around C and its consecutive table-cell siblings.
1401///
1402/// For each proper table child C in a sequence of consecutive proper table children
1403/// that are misparented (i.e., their parent is not a table element), an anonymous
1404/// table box is generated around C and its consecutive siblings."
1405///
1406/// This function checks if a node needs a parent wrapper and returns the appropriate
1407/// anonymous box type, or None if no wrapper is needed.
1408fn needs_table_parent_wrapper(
1409 styled_dom: &StyledDom,
1410 node_id: NodeId,
1411 parent_display: LayoutDisplay,
1412) -> Option<AnonymousBoxType> {
1413 let child_display = get_display_type(styled_dom, node_id);
1414
1415 // CSS 2.2 Section 17.2.1, Stage 3:
1416 // If we have a table-cell but parent is not a table-row, need anonymous row
1417 if child_display == LayoutDisplay::TableCell {
1418 match parent_display {
1419 LayoutDisplay::TableRow
1420 | LayoutDisplay::TableRowGroup
1421 | LayoutDisplay::TableHeaderGroup
1422 | LayoutDisplay::TableFooterGroup => {
1423 // Parent can contain cells directly or via rows - no wrapper needed
1424 None
1425 }
1426 _ => Some(AnonymousBoxType::TableRow),
1427 }
1428 }
1429 // If we have a table-row but parent is not a table/row-group, need anonymous table
1430 else if matches!(child_display, LayoutDisplay::TableRow) {
1431 match parent_display {
1432 LayoutDisplay::Table
1433 | LayoutDisplay::TableRowGroup
1434 | LayoutDisplay::TableHeaderGroup
1435 | LayoutDisplay::TableFooterGroup => {
1436 None // Parent is correct
1437 }
1438 _ => Some(AnonymousBoxType::TableWrapper),
1439 }
1440 }
1441 // If we have a row-group but parent is not a table, need anonymous table
1442 else if matches!(
1443 child_display,
1444 LayoutDisplay::TableRowGroup
1445 | LayoutDisplay::TableHeaderGroup
1446 | LayoutDisplay::TableFooterGroup
1447 ) {
1448 match parent_display {
1449 LayoutDisplay::Table => None,
1450 _ => Some(AnonymousBoxType::TableWrapper),
1451 }
1452 } else {
1453 None
1454 }
1455}
1456
1457// Determines the display type of a node based on its tag and CSS properties.
1458pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
1459 if let Some(_styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
1460 let node_data = &styled_dom.node_data.as_container()[node_id];
1461 let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1462
1463 // 1. Check author CSS first
1464 if let Some(d) = styled_dom
1465 .css_property_cache
1466 .ptr
1467 .get_display(node_data, &node_id, node_state)
1468 .and_then(|v| v.get_property().copied())
1469 {
1470 return d;
1471 }
1472
1473 // 2. Check User Agent CSS (always returns a value for display)
1474 let node_type = &styled_dom.node_data.as_container()[node_id].node_type;
1475 if let Some(ua_prop) =
1476 azul_core::ua_css::get_ua_property(node_type, CssPropertyType::Display)
1477 {
1478 if let CssProperty::Display(azul_css::css::CssPropertyValue::Exact(d)) = ua_prop {
1479 return *d;
1480 }
1481 }
1482 }
1483
1484 // 3. Final fallback (should never be reached since UA CSS always provides display)
1485 // Inline is the safest default per CSS spec
1486 LayoutDisplay::Inline
1487}
1488
1489/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
1490/// A BFC contains floats and prevents margin collapse.
1491fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1492 let display = get_display_type(styled_dom, node_id);
1493 if matches!(
1494 display,
1495 LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::FlowRoot
1496 ) {
1497 return true;
1498 }
1499
1500 if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
1501 // `overflow` other than `visible`
1502
1503 let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
1504 if !overflow_x.is_visible_or_clip() {
1505 return true;
1506 }
1507
1508 let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
1509 if !overflow_y.is_visible_or_clip() {
1510 return true;
1511 }
1512
1513 // `position: absolute` or `position: fixed`
1514 let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
1515
1516 if position.is_absolute_or_fixed() {
1517 return true;
1518 }
1519
1520 // `float` is not `none`
1521 let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
1522 if !float.is_none() {
1523 return true;
1524 }
1525 }
1526
1527 // The root element (<html>) also establishes a BFC.
1528 if styled_dom.root.into_crate_internal() == Some(node_id) {
1529 return true;
1530 }
1531
1532 false
1533}
1534
1535/// The logic now correctly identifies all BFC roots.
1536fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
1537 // Special case: Text nodes should be treated as inline content.
1538 // They participate in their parent's inline formatting context.
1539 let node_data = &styled_dom.node_data.as_container()[node_id];
1540
1541 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
1542 // Text nodes are inline-level content within their parent's IFC
1543 return FormattingContext::Inline;
1544 }
1545
1546 let display_type = get_display_type(styled_dom, node_id);
1547
1548 match display_type {
1549 LayoutDisplay::Inline => FormattingContext::Inline,
1550
1551 // CSS 2.2 Section 9.4.2: "An inline formatting context is established by a
1552 // block container box that contains no block-level boxes."
1553 // Check if this block container has only inline-level children.
1554 LayoutDisplay::Block | LayoutDisplay::FlowRoot | LayoutDisplay::ListItem => {
1555 if has_only_inline_children(styled_dom, node_id) {
1556 // This block container should establish an IFC for its inline children
1557 FormattingContext::Inline
1558 } else {
1559 // Normal BFC
1560 FormattingContext::Block {
1561 establishes_new_context: establishes_new_block_formatting_context(
1562 styled_dom, node_id,
1563 ),
1564 }
1565 }
1566 }
1567 LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
1568 LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
1569 LayoutDisplay::TableRowGroup
1570 | LayoutDisplay::TableHeaderGroup
1571 | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
1572 LayoutDisplay::TableRow => FormattingContext::TableRow,
1573 LayoutDisplay::TableCell => FormattingContext::TableCell,
1574 LayoutDisplay::None => FormattingContext::None,
1575 LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
1576 LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
1577 LayoutDisplay::TableCaption => FormattingContext::TableCaption,
1578 LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
1579
1580 // These less common display types default to block behavior
1581 LayoutDisplay::TableColumn | LayoutDisplay::RunIn | LayoutDisplay::Marker => {
1582 FormattingContext::Block {
1583 establishes_new_context: true,
1584 }
1585 }
1586 }
1587}