azul_layout/solver3/sizing.rs
1//! Intrinsic and used size calculations for layout nodes
2
3use std::{
4 collections::BTreeSet,
5 sync::Arc,
6};
7
8use azul_core::{
9 dom::{FormattingContext, NodeId, NodeType},
10 geom::LogicalSize,
11 resources::RendererResources,
12 styled_dom::{StyledDom, StyledNodeState},
13};
14use azul_css::{
15 css::CssPropertyValue,
16 props::{
17 basic::PixelValue,
18 layout::{LayoutDisplay, LayoutFlexDirection, LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutPosition, LayoutWidth, LayoutWritingMode},
19 property::{CssProperty, CssPropertyType},
20 },
21 LayoutDebugMessage,
22};
23use rust_fontconfig::FcFontCache;
24
25#[cfg(feature = "text_layout")]
26use crate::text3;
27use crate::{
28 font::parsed::ParsedFont,
29 font_traits::{
30 AvailableSpace, FontLoaderTrait, FontManager, ImageSource, InlineContent, InlineImage,
31 InlineShape, LayoutCache, LayoutFragment, ObjectFit, ParsedFontTrait, ShapeDefinition,
32 StyleProperties, UnifiedConstraints,
33 },
34 solver3::{
35 fc::split_text_for_whitespace,
36 geometry::{BoxProps, BoxSizing, IntrinsicSizes, WritingModeContext},
37 getters::{
38 get_css_box_sizing, get_css_height, get_css_width, get_display_property,
39 get_direction_property, get_element_font_size, get_flex_direction, get_float,
40 get_style_properties, get_text_orientation_property, get_writing_mode, MultiValue,
41 },
42 layout_tree::{LayoutNodeHot, LayoutTree, get_display_type},
43 positioning::get_position_type,
44 LayoutContext, LayoutError, Result,
45 },
46};
47
48/// Resolves a percentage value against the containing block dimension.
49///
50/// Per CSS 2.1 Section 10.2, percentages resolve directly against the containing
51/// block's width or height. The margin/border/padding parameters are accepted for
52/// call-site convenience but are intentionally unused — percentage resolution does
53/// not subtract box-model extras in content-box sizing.
54///
55/// Returns `(containing_block_dimension * percentage).max(0.0)`.
56// +spec:containing-block:43c719 - percentages resolved against containing block width/height
57// +spec:containing-block:723eee - Percentages specify sizing with respect to the containing block
58// +spec:containing-block:8ad6f4 - Percentage resolution against containing block (editorial note: transferred percentages)
59// +spec:containing-block:257f3b - Block-axis percentages resolve against containing block size
60// +spec:containing-block:f1344e - percentage min/max-width resolved against containing block width; negative CB width yields zero
61pub fn resolve_percentage_with_box_model(
62 containing_block_dimension: f32,
63 percentage: f32,
64 _margins: (f32, f32),
65 _borders: (f32, f32),
66 _paddings: (f32, f32),
67) -> f32 {
68 // +spec:containing-block:b3388b - percentage resolved against containing block size without re-resolution (css-sizing-3 §5.2.1)
69 // CSS 2.1 Section 10.2: percentages resolve against containing block,
70 // not available space after margins/borders/padding
71 (containing_block_dimension * percentage).max(0.0)
72}
73
74/// Returns true if the DOM subtree rooted at `dom_id` contains any `NodeType::Text`.
75///
76/// Used when deciding whether a `FormattingContext::Inline` node should measure
77/// its inline content (it acts as an IFC root when nested inlines eventually
78/// hold text) versus returning zero (pure inline wrapper with no text reaches).
79fn subtree_contains_text(styled_dom: &StyledDom, dom_id: NodeId) -> bool {
80 let node_hierarchy = styled_dom.node_hierarchy.as_container();
81 let node_data = styled_dom.node_data.as_container();
82 if matches!(node_data[dom_id].get_node_type(), NodeType::Text(_)) {
83 return true;
84 }
85 dom_id
86 .az_children(&node_hierarchy)
87 .any(|child| subtree_contains_text(styled_dom, child))
88}
89
90/// Phase 2a: Calculate intrinsic sizes (bottom-up pass)
91/// // +spec:display-contents:f12d4e - intrinsic sizing: size determined by contents, not context
92pub fn calculate_intrinsic_sizes<T: ParsedFontTrait>(
93 ctx: &mut LayoutContext<'_, T>,
94 tree: &mut LayoutTree,
95 text_cache: &mut LayoutCache,
96 dirty_nodes: &BTreeSet<usize>,
97) -> Result<()> {
98 if dirty_nodes.is_empty() {
99 return Ok(());
100 }
101
102 ctx.debug_log("Starting intrinsic size calculation");
103 // Pre-compute the "ancestor closure" of dirty_nodes: every dirty
104 // node AND each of its ancestors up to root. A node not in this
105 // set (and whose `intrinsic_sizes` is already populated) can
106 // reuse its cached intrinsic — we skip its entire subtree walk.
107 // Before this, `calculate_intrinsic_recursive` walked the full
108 // tree from root regardless, costing ~2 ms per warm render on
109 // excel.html even when only 3 nodes were actually dirty.
110 let dirty_closure = compute_dirty_ancestor_closure(tree, dirty_nodes);
111
112 let mut calculator = IntrinsicSizeCalculator::new(ctx, text_cache);
113 calculator.dirty_closure = Some(dirty_closure);
114 // Fix C (re-enabled §58 Win #3): skip intrinsic computation for subtrees
115 // whose values will never be consumed. `tree.subtree_needs_intrinsic` is a
116 // static-DOM bitmap precomputed at tree-build time — true if this node or
117 // any descendant establishes a shrink-to-fit context. When both the
118 // caller and the subtree are non-STF, no one reads the intrinsic, so the
119 // whole descent is pure waste.
120 //
121 // The previous attempt (7667d13e, reverted in bd9ad36d) wrote default
122 // (zero) intrinsics and broke auto-height rendering because
123 // calculate_used_size_for_node read intrinsic.max_content_height as the
124 // height:auto fallback. 97c3d3db refactored that dependency away: for
125 // block-level auto-height, used_size.height is 0 pre-layout and
126 // apply_content_based_height fills it from the laid-out content size.
127 // With that gone, skipping intrinsic is safe.
128 calculator.calculate_intrinsic_recursive(tree, tree.root, false)?;
129 ctx.debug_log("Finished intrinsic size calculation");
130 Ok(())
131}
132
133fn compute_dirty_ancestor_closure(
134 tree: &LayoutTree,
135 dirty_nodes: &BTreeSet<usize>,
136) -> std::collections::HashSet<usize> {
137 let mut closure: std::collections::HashSet<usize> = std::collections::HashSet::new();
138 for &dirty in dirty_nodes {
139 let mut cur = Some(dirty);
140 while let Some(idx) = cur {
141 if !closure.insert(idx) {
142 break;
143 }
144 cur = tree.get(idx).and_then(|n| n.parent);
145 }
146 }
147 closure
148}
149
150struct IntrinsicSizeCalculator<'a, 'b, 'c, T: ParsedFontTrait> {
151 ctx: &'a mut LayoutContext<'b, T>,
152 /// Shared text shaping cache, threaded through from the caller so
153 /// stages 1–3 of the inline layout pipeline (logical / BiDi / shaping)
154 /// are cache-hits across the sizing pass's min/max-content measurements
155 /// AND the subsequent real layout pass. Previously each pass held its
156 /// own `LayoutCache`, so identical text was shaped three times per
157 /// root_layout_pass — once per min-content measurement, once per
158 /// max-content measurement, once at final layout.
159 text_cache: &'c mut LayoutCache,
160 /// If `Some`, only nodes in this set (the ancestor-closure of
161 /// dirty nodes) need recomputation. A clean node whose
162 /// `warm.intrinsic_sizes` is already populated reuses the
163 /// cached value and skips its entire subtree descent.
164 dirty_closure: Option<std::collections::HashSet<usize>>,
165}
166
167impl<'a, 'b, 'c, T: ParsedFontTrait> IntrinsicSizeCalculator<'a, 'b, 'c, T> {
168 fn new(ctx: &'a mut LayoutContext<'b, T>, text_cache: &'c mut LayoutCache) -> Self {
169 Self {
170 ctx,
171 text_cache,
172 dirty_closure: None,
173 }
174 }
175
176 fn calculate_intrinsic_recursive(
177 &mut self,
178 tree: &mut LayoutTree,
179 node_index: usize,
180 ancestor_is_stf: bool,
181 ) -> Result<IntrinsicSizes> {
182 // Fast path: if this subtree has no dirty nodes AND we
183 // already have a cached intrinsic, return the cached value
184 // and skip the whole descent. Caller is the ancestor-closure
185 // computation in `calculate_intrinsic_sizes` — anything not
186 // in that set is guaranteed clean through every descendant.
187 if let Some(closure) = self.dirty_closure.as_ref() {
188 if !closure.contains(&node_index) {
189 if let Some(cached) = tree
190 .warm(node_index)
191 .and_then(|w| w.intrinsic_sizes.clone())
192 {
193 return Ok(cached);
194 }
195 }
196 }
197
198 // Fix C static-DOM short-circuit: if no ancestor needs this intrinsic
199 // (none are STF) AND no descendant in this subtree is STF, nobody
200 // will ever read the value. Write a default and skip the recursion.
201 // `subtree_needs_intrinsic` is precomputed at tree-build time from
202 // the DOM's display/position/float properties, so this is a constant
203 // lookup with no per-pass work.
204 if !ancestor_is_stf
205 && tree
206 .subtree_needs_intrinsic
207 .get(node_index)
208 .copied()
209 .map(|v| !v)
210 .unwrap_or(false)
211 {
212 let default = IntrinsicSizes::default();
213 if let Some(n) = tree.warm_mut(node_index) {
214 n.intrinsic_sizes = Some(default);
215 }
216 return Ok(default);
217 }
218
219 // Previously cloned the full LayoutNode to sidestep borrow conflicts
220 // with the `&mut tree` recursive calls below, but we only need the
221 // DOM id here — a `Copy` scalar. The clone was allocating a
222 // Vec<usize> for children and a TaffyCache on every recursion
223 // (~300x on excel.html).
224 let dom_node_id = tree
225 .get(node_index)
226 .ok_or(LayoutError::InvalidTree)?
227 .dom_node_id;
228
229 // Out-of-flow elements do not contribute to their parent's intrinsic size.
230 let position = get_position_type(self.ctx.styled_dom, dom_node_id);
231 if position == LayoutPosition::Absolute || position == LayoutPosition::Fixed {
232 if let Some(n) = tree.warm_mut(node_index) {
233 n.intrinsic_sizes = Some(IntrinsicSizes::default());
234 }
235 return Ok(IntrinsicSizes::default());
236 }
237
238 // Copy child indices before recursive calls (which need &mut tree).
239 // Stack buffer for the common case (≤32 children); heap only for huge nodes.
240 let children_slice = tree.children(node_index);
241 let n = children_slice.len();
242 let mut stack_buf = [0usize; 32];
243 let heap_buf: Vec<usize>;
244 let children: &[usize] = if n <= 32 {
245 stack_buf[..n].copy_from_slice(children_slice);
246 &stack_buf[..n]
247 } else {
248 heap_buf = children_slice.to_vec();
249 &heap_buf
250 };
251 // Propagate STF flag: children inherit `ancestor_is_stf=true` if any
252 // ancestor up to and including self is STF.
253 let self_is_stf = tree
254 .get(node_index)
255 .map(|n| {
256 crate::solver3::layout_tree::is_shrink_to_fit_context(
257 self.ctx.styled_dom,
258 n.dom_node_id,
259 &n.formatting_context,
260 )
261 })
262 .unwrap_or(false);
263 let child_ancestor_is_stf = ancestor_is_stf || self_is_stf;
264
265 let mut child_intrinsics = Vec::with_capacity(n);
266 for &child_index in children {
267 let child_intrinsic =
268 self.calculate_intrinsic_recursive(tree, child_index, child_ancestor_is_stf)?;
269 child_intrinsics.push((child_index, child_intrinsic));
270 }
271
272 // Then calculate this node's intrinsic size based on its children
273 let mut intrinsic = self.calculate_node_intrinsic_sizes(tree, node_index, &child_intrinsics)?;
274
275 // +spec:min-max-sizing:970fef - if min-width/min-height is a <length>, use as floor for intrinsic sizes
276 if let Some(dom_id) = tree.get(node_index).and_then(|n| n.dom_node_id) {
277 use azul_css::props::basic::{pixel::{DEFAULT_FONT_SIZE, PT_TO_PX}, SizeMetric};
278 use crate::solver3::getters::{get_css_min_width, get_css_min_height, MultiValue};
279
280 let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
281
282 if let MultiValue::Exact(mw) = get_css_min_width(self.ctx.styled_dom, dom_id, node_state) {
283 let px = &mw.inner;
284 let resolved = match px.metric {
285 SizeMetric::Px => Some(px.number.get()),
286 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
287 SizeMetric::In => Some(px.number.get() * 96.0),
288 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
289 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
290 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
291 _ => None, // percentages are not <length>
292 };
293 if let Some(min_w) = resolved {
294 intrinsic.min_content_width = intrinsic.min_content_width.max(min_w);
295 intrinsic.max_content_width = intrinsic.max_content_width.max(min_w);
296 }
297 }
298
299 if let MultiValue::Exact(mh) = get_css_min_height(self.ctx.styled_dom, dom_id, node_state) {
300 let px = &mh.inner;
301 let resolved = match px.metric {
302 SizeMetric::Px => Some(px.number.get()),
303 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
304 SizeMetric::In => Some(px.number.get() * 96.0),
305 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
306 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
307 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
308 _ => None,
309 };
310 if let Some(min_h) = resolved {
311 intrinsic.min_content_height = intrinsic.min_content_height.max(min_h);
312 intrinsic.max_content_height = intrinsic.max_content_height.max(min_h);
313 }
314 }
315 }
316
317 if let Some(n) = tree.warm_mut(node_index) {
318 n.intrinsic_sizes = Some(intrinsic);
319 }
320
321 Ok(intrinsic)
322 }
323
324 fn calculate_node_intrinsic_sizes(
325 &mut self,
326 tree: &LayoutTree,
327 node_index: usize,
328 child_intrinsics: &[(usize, IntrinsicSizes)],
329 ) -> Result<IntrinsicSizes> {
330 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
331
332 // +spec:block-formatting-context:30def2 - replaced elements use physical 300x150 default, not re-oriented by writing-mode
333 // +spec:display-property:015c41 - replaced elements default to 300x150 intrinsic size per css-sizing-3 §5.1
334 // +spec:display-property:2c6af3 - replaced elements with auto width/height use max-content size
335 // +spec:replaced-elements:6d6030 - Intrinsic sizes for replaced elements (images, virtual views)
336 // VirtualViews are replaced elements with a default intrinsic size of 300x150px
337 // (same as virtualized view elements)
338 if let Some(dom_id) = node.dom_node_id {
339 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
340 if node_data.is_virtual_view_node() {
341 return Ok(IntrinsicSizes {
342 min_content_width: 300.0,
343 max_content_width: 300.0,
344 preferred_width: None, // Will be determined by CSS or flex-grow
345 min_content_height: 150.0,
346 max_content_height: 150.0,
347 preferred_height: None, // Will be determined by CSS or flex-grow
348 });
349 }
350
351 // +spec:containing-block:bb5a12 - replaced element intrinsic sizes using initial containing block
352 // +spec:display-property:7127f9 - intrinsic sizes of replaced elements without natural sizes (300x150 fallback, aspect ratio)
353 // +spec:display-property:f9cede - replaced elements derive intrinsic size from natural dimensions
354 // +spec:writing-modes:b18121 - stretch fit inline size from available space, calculate block size via aspect ratio
355 if let NodeType::Image(image_ref) = node_data.get_node_type() {
356 let size = image_ref.get_size();
357 // +spec:containing-block:1da6dc - use initial CB inline size for replaced elements with aspect ratio but no intrinsic size
358 // Per css-sizing-3 §5.1: "use an inline size matching the corresponding dimension
359 // of the initial containing block and calculate the other dimension using the aspect ratio"
360 let (width, height) = if size.width > 0.0 && size.height > 0.0 {
361 (size.width, size.height)
362 } else if size.width > 0.0 {
363 (size.width, size.width / 2.0)
364 } else if size.height > 0.0 {
365 // Has intrinsic height but no width — use initial CB inline dimension
366 (self.ctx.viewport_size.width, size.height)
367 } else {
368 // +spec:replaced-elements:43376b - 300px fallback with 2:1 ratio for replaced elements
369 // No intrinsic dimensions — cap at 300x150 per CSS 2.2 §10.3.2
370 // +spec:width-calculation:3b0efe - auto width fallback: 300px capped to device width
371 // +spec:width-calculation:16c305 - auto height fallback: 2:1 ratio, max 150px
372 let w = self.ctx.viewport_size.width.min(300.0);
373 (w, w / 2.0)
374 };
375 return Ok(IntrinsicSizes {
376 min_content_width: width,
377 max_content_width: width,
378 preferred_width: Some(width),
379 min_content_height: height,
380 max_content_height: height,
381 preferred_height: Some(height),
382 });
383 }
384 }
385
386 match node.formatting_context {
387 FormattingContext::Block { .. } => {
388 // Check if this block establishes an Inline Formatting Context (IFC).
389 // Per CSS 2.2 §9.2.1.1: A block container with mixed block-level and
390 // inline-level children creates anonymous block boxes to wrap the inline
391 // content. So we only treat as IFC root if there are NO block-level children.
392 //
393 // We check the actual CSS display property, NOT formatting_context,
394 // because a display:block element with only inline children gets
395 // FormattingContext::Inline (meaning "establishes IFC for its children"),
396 // which is different from being an inline element itself.
397 let has_block_child = tree.children(node_index).iter().any(|&child_idx| {
398 tree.get(child_idx)
399 .and_then(|c| c.dom_node_id)
400 .map(|dom_id| {
401 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
402 // Text nodes are inline-level
403 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
404 return false;
405 }
406 let display = get_display_type(self.ctx.styled_dom, dom_id);
407 display.creates_block_context()
408 })
409 .unwrap_or(false)
410 });
411
412 let has_inline_child = tree.children(node_index).iter().any(|&child_idx| {
413 tree.get(child_idx)
414 .and_then(|c| c.dom_node_id)
415 .map(|dom_id| {
416 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
417 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
418 return true;
419 }
420 let display = get_display_type(self.ctx.styled_dom, dom_id);
421 matches!(display,
422 LayoutDisplay::Inline
423 | LayoutDisplay::InlineBlock
424 | LayoutDisplay::InlineFlex
425 | LayoutDisplay::InlineGrid
426 | LayoutDisplay::InlineTable
427 )
428 })
429 .unwrap_or(false)
430 });
431
432 // IFC root only if there are inline children and NO block children.
433 // If there are block children, text nodes get anonymous block wrappers.
434 let is_ifc_root = has_inline_child && !has_block_child;
435
436 // Also check if this block has direct text content (text nodes in DOM)
437 // but ONLY if there are no block-level layout children
438 let has_direct_text = if !has_block_child {
439 if let Some(dom_id) = node.dom_node_id {
440 let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
441 dom_id.az_children(node_hierarchy).any(|child_id| {
442 let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
443 matches!(child_node_data.get_node_type(), NodeType::Text(_))
444 })
445 } else {
446 false
447 }
448 } else {
449 false
450 };
451
452 if is_ifc_root || has_direct_text {
453 // This block is an IFC root - measure all inline content ONCE
454 self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
455 } else {
456 // This is a BFC root (only block children) - aggregate child sizes
457 self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
458 }
459 }
460 FormattingContext::Inline => {
461 // There are THREE cases for FormattingContext::Inline:
462 // 1. A Text node (NodeType::Text) - this IS the text content itself
463 // -> Needs to measure itself as an atomic inline unit
464 // 2. An IFC root - a block with only inline children (has text child nodes)
465 // -> Should measure its inline content
466 // 3. A true inline element (display: inline, e.g., <span>) with no text
467 // -> Returns default(0,0), measured by parent IFC root
468 //
469 // We distinguish by:
470 // - Checking if THIS node is a Text node (case 1)
471 // - Checking if this subtree contains any text (case 2)
472 //
473 // Why descendants, not just direct children: for `<span><a>text</a></span>`,
474 // the `<span>` is a layout-tree IFC root (layout_ifc is called on it), but
475 // its direct DOM children are inline elements, not text. Restricting the
476 // check to direct text children would zero out the span's intrinsic width
477 // even though the cell content width depends on it.
478 let is_text_node = if let Some(dom_id) = node.dom_node_id {
479 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
480 matches!(node_data.get_node_type(), NodeType::Text(_))
481 } else {
482 false
483 };
484
485 let has_text_in_subtree = if let Some(dom_id) = node.dom_node_id {
486 subtree_contains_text(self.ctx.styled_dom, dom_id)
487 } else {
488 false
489 };
490
491 if is_text_node || has_text_in_subtree {
492 // Case 1 or 2: Text node or IFC root - measure inline content
493 self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
494 } else {
495 // Case 3: True inline element - measured by parent IFC root
496 Ok(IntrinsicSizes::default())
497 }
498 }
499 FormattingContext::InlineBlock => {
500 // Inline-block IS an atomic inline - it needs its own intrinsic size.
501 // Check layout tree children AND direct DOM text children (text nodes
502 // are not in the layout tree, only in the DOM).
503 let has_inline_children = tree.children(node_index).iter().any(|&child_idx| {
504 tree.get(child_idx)
505 .map(|c| matches!(c.formatting_context, FormattingContext::Inline))
506 .unwrap_or(false)
507 });
508
509 let has_direct_text = if let Some(dom_id) = node.dom_node_id {
510 let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
511 dom_id.az_children(node_hierarchy).any(|child_id| {
512 let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
513 matches!(child_node_data.get_node_type(), NodeType::Text(_))
514 })
515 } else {
516 false
517 };
518
519 if has_inline_children || has_direct_text {
520 // InlineBlock with inline children - measure as IFC root.
521 // Returns content-level intrinsic sizes (no margin/padding/border).
522 // The parent adds box-model extras via calculate_block_intrinsic_sizes,
523 // and calculate_used_size_for_node adds padding+border for border-box.
524 let intrinsic = self.calculate_ifc_root_intrinsic_sizes(tree, node_index)?;
525
526 Ok(intrinsic)
527 } else {
528 // InlineBlock with block children - aggregate like block
529 self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
530 }
531 }
532 FormattingContext::Table => {
533 self.calculate_table_intrinsic_sizes(tree, node_index, child_intrinsics)
534 }
535 FormattingContext::Flex => {
536 self.calculate_flex_intrinsic_sizes(tree, node_index, child_intrinsics)
537 }
538 _ => self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics),
539 }
540 }
541
542 // +spec:intrinsic-sizing:ea2c2c - §5.1 min-content size = size as float with auto; max-content = no wrapping
543 /// Calculate intrinsic sizes for an IFC root (a block containing inline content).
544 /// This collects ALL inline descendants' text and measures it ONCE.
545 // +spec:intrinsic-sizing:8f3c0c - hanging glyphs must be excluded from intrinsic size measurement
546 fn calculate_ifc_root_intrinsic_sizes(
547 &mut self,
548 tree: &LayoutTree,
549 node_index: usize,
550 ) -> Result<IntrinsicSizes> {
551 // Collect all inline content from this IFC root and its inline descendants
552 let inline_content = collect_inline_content(&mut self.ctx, tree, node_index)?;
553
554
555
556 if inline_content.is_empty() {
557 return Ok(IntrinsicSizes::default());
558 }
559
560 // Get pre-loaded fonts from font manager
561 let loaded_fonts = self.ctx.font_manager.get_loaded_fonts();
562
563 // +spec:intrinsic-sizing:ae8beb - min-content = zero-width CB, max-content = infinite-width CB
564 // +spec:intrinsic-sizing:8c94e2 - min-content/max-content intrinsic size determination via constrained layout
565 // Use `measure_intrinsic_widths` instead of two `layout_flow` passes (fix B):
566 // it runs stages 1–4 of the pipeline once (logical → BiDi → shape → orient)
567 // and derives min/max-content by scanning the shaped items directly. This
568 // avoids the BreakCursor line-breaking loop entirely — that loop clones
569 // every ShapedCluster it inspects via `peek_next_unit` and accounted for
570 // 24% of total CPU on the text_2000 stress fixture. Shaping is cached
571 // at the per-item level (keyed on text+style), so the subsequent real
572 // layout_flow call for this content gets pure cache hits for stages 1–3.
573 let constraints = UnifiedConstraints::default();
574 let intrinsic_text = match self.text_cache.measure_intrinsic_widths(
575 &inline_content,
576 &[],
577 &constraints,
578 &self.ctx.font_manager.font_chain_cache,
579 &self.ctx.font_manager.fc_cache,
580 &loaded_fonts,
581 self.ctx.debug_messages,
582 ) {
583 Ok(r) => r,
584 Err(_) => {
585 return Ok(IntrinsicSizes {
586 min_content_width: 100.0,
587 max_content_width: 300.0,
588 preferred_width: None,
589 min_content_height: 20.0,
590 max_content_height: 20.0,
591 preferred_height: None,
592 });
593 }
594 };
595
596 let min_width = intrinsic_text.min_content_width;
597 let max_width = intrinsic_text.max_content_width;
598
599 // +spec:display-property:c587fd - min-content block size equals max-content block size for block containers, tables, inline boxes
600 // +spec:intrinsic-sizing:02eedc - min-content block size equals max-content block size for block containers
601 // For a single-line max-content layout the height is one line box;
602 // `measure_intrinsic_widths` returns exactly that.
603 let max_content_height = intrinsic_text.max_content_height;
604
605 // NOTE(writing-modes): min_content_width / max_content_width are named for
606 // the physical axis. In vertical writing modes the "inline" axis is vertical,
607 // so these are swapped by calculate_block_intrinsic_sizes when computing
608 // the parent's intrinsic sizes. The physical naming is intentional here.
609 Ok(IntrinsicSizes {
610 min_content_width: min_width,
611 max_content_width: max_width,
612 preferred_width: None,
613 min_content_height: max_content_height,
614 max_content_height,
615 preferred_height: None,
616 })
617 }
618
619 // +spec:containing-block:bb0658 - percentage block-sizes behave as auto during intrinsic computation (no CSS height resolution here)
620 // +spec:display-contents:84fe7f - cyclic percentage contributions: percentage-sized children use auto during intrinsic sizing
621 // +spec:min-max-sizing:411904 - percentage block-sizes treated as auto during intrinsic sizing (content-sized CB)
622 // +spec:min-max-sizing:737e62 - percentage heights don't resolve inside content-sized containing blocks
623 fn calculate_block_intrinsic_sizes(
624 &mut self,
625 tree: &LayoutTree,
626 node_index: usize,
627 child_intrinsics: &[(usize, IntrinsicSizes)],
628 ) -> Result<IntrinsicSizes> {
629 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
630 let writing_mode = if let Some(dom_id) = node.dom_node_id {
631 let node_state =
632 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
633 get_writing_mode(self.ctx.styled_dom, dom_id, node_state).unwrap_or_default()
634 } else {
635 LayoutWritingMode::default()
636 };
637
638 // NOTE: Text content detection is now handled in calculate_node_intrinsic_sizes
639 // which calls calculate_ifc_root_intrinsic_sizes for blocks with inline content.
640 // This function now only handles pure block containers (BFC roots).
641 // +spec:height-calculation:d9ca8d - cyclic percentage contributions: percentage min-height/max-height on children should behave as auto when computing intrinsic contributions (not yet implemented)
642
643 let mut max_child_min_cross = 0.0f32;
644 let mut max_child_max_cross = 0.0f32;
645 let mut total_main_size = 0.0;
646 // Track margins for CSS 2.2 §8.3.1 collapsing in the block direction.
647 // Block margins collapse between siblings (max instead of sum) and
648 // parent-child margins can escape (first/last child).
649 let mut last_margin_main_end = 0.0f32;
650 let mut is_first_child = true;
651
652 for &child_index in tree.children(node_index) {
653 if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
654 // +spec:intrinsic-sizing:ed72bb - intrinsic contributions based on outer size, auto margins as zero
655 let child_node = tree.get(child_index);
656 let (cross_extras, main_border_padding, main_margin_start, main_margin_end) =
657 if let Some(cn) = child_node {
658 let bp = cn.box_props.unpack();
659 let h = bp.margin.left + bp.margin.right
660 + bp.border.left + bp.border.right
661 + bp.padding.left + bp.padding.right;
662 let v_bp = bp.border.top + bp.border.bottom
663 + bp.padding.top + bp.padding.bottom;
664 match writing_mode {
665 LayoutWritingMode::HorizontalTb => (h, v_bp, bp.margin.top, bp.margin.bottom),
666 _ => (v_bp, h, bp.margin.left, bp.margin.right),
667 }
668 } else {
669 (0.0, 0.0, 0.0, 0.0)
670 };
671
672 let (child_min_cross, child_max_cross, child_border_box_main) = match writing_mode {
673 LayoutWritingMode::HorizontalTb => (
674 child_intrinsic.min_content_width + cross_extras,
675 child_intrinsic.max_content_width + cross_extras,
676 child_intrinsic.max_content_height + main_border_padding,
677 ),
678 _ => (
679 child_intrinsic.min_content_height + cross_extras,
680 child_intrinsic.max_content_height + cross_extras,
681 child_intrinsic.max_content_width + main_border_padding,
682 ),
683 };
684
685 max_child_min_cross = max_child_min_cross.max(child_min_cross);
686 max_child_max_cross = max_child_max_cross.max(child_max_cross);
687
688 // CSS 2.2 §8.3.1 margin collapsing for intrinsic sizing:
689 // - First child's margin-start can escape (don't add to total)
690 // - Between siblings: collapsed gap = max(prev_end, curr_start)
691 // - Last child's margin-end can escape (don't add to total)
692 if is_first_child {
693 is_first_child = false;
694 // First child: top margin may escape, don't add it
695 } else {
696 // Sibling gap: collapsed margin between prev bottom and current top
697 let collapsed_gap = crate::solver3::fc::collapse_margins(
698 last_margin_main_end, main_margin_start
699 );
700 total_main_size += collapsed_gap;
701 }
702
703 total_main_size += child_border_box_main;
704 last_margin_main_end = main_margin_end;
705 }
706 }
707 // Last child's margin-end may escape — don't add it to total_main_size
708
709 let (min_width, max_width, min_height, max_height) = match writing_mode {
710 LayoutWritingMode::HorizontalTb => (
711 max_child_min_cross,
712 max_child_max_cross,
713 total_main_size,
714 total_main_size,
715 ),
716 _ => (
717 total_main_size,
718 total_main_size,
719 max_child_min_cross,
720 max_child_max_cross,
721 ),
722 };
723
724 Ok(IntrinsicSizes {
725 min_content_width: min_width,
726 max_content_width: max_width,
727 preferred_width: None,
728 min_content_height: min_height,
729 max_content_height: max_height,
730 preferred_height: None,
731 })
732 }
733
734 // The max-content main size is the sum of items' max-content contributions.
735 // The min-content main size of a single-line flex container is the sum of items'
736 // min-content contributions. For multi-line, it is the largest min-content contribution.
737 // Auto margins on flex items are treated as 0 for this computation.
738 fn calculate_flex_intrinsic_sizes(
739 &mut self,
740 tree: &LayoutTree,
741 node_index: usize,
742 child_intrinsics: &[(usize, IntrinsicSizes)],
743 ) -> Result<IntrinsicSizes> {
744 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
745
746 // Determine flex-direction to know if main axis is horizontal or vertical
747 let is_row = if let Some(dom_id) = node.dom_node_id {
748 let node_state =
749 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
750 match get_flex_direction(self.ctx.styled_dom, dom_id, &node_state) {
751 MultiValue::Exact(dir) => matches!(dir, LayoutFlexDirection::Row | LayoutFlexDirection::RowReverse),
752 _ => true, // default is row
753 }
754 } else {
755 true // default flex-direction is row
756 };
757
758 let mut sum_main_min: f32 = 0.0;
759 let mut sum_main_max: f32 = 0.0;
760 let mut max_main_min: f32 = 0.0;
761 let mut max_cross_min: f32 = 0.0;
762 let mut max_cross_max: f32 = 0.0;
763
764 for &child_index in tree.children(node_index) {
765 if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
766 let (child_main_min, child_main_max, child_cross_min, child_cross_max) = if is_row {
767 (
768 child_intrinsic.min_content_width,
769 child_intrinsic.max_content_width,
770 child_intrinsic.min_content_height,
771 child_intrinsic.max_content_height,
772 )
773 } else {
774 (
775 child_intrinsic.min_content_height,
776 child_intrinsic.max_content_height,
777 child_intrinsic.min_content_width,
778 child_intrinsic.max_content_width,
779 )
780 };
781
782 sum_main_max += child_main_max;
783 sum_main_min += child_main_min;
784 // For multi-line min-content, track the largest single item
785 max_main_min = max_main_min.max(child_main_min);
786
787 // Cross axis: largest child determines the container's cross size
788 max_cross_min = max_cross_min.max(child_cross_min);
789 max_cross_max = max_cross_max.max(child_cross_max);
790 }
791 }
792
793 // For single-line (nowrap), min-content = sum; for multi-line (wrap), min-content = max
794 // Default flex-wrap is nowrap (single-line)
795 let is_single_line = if let Some(dom_id) = node.dom_node_id {
796 let node_state =
797 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
798 let wrap_prop = crate::solver3::getters::get_flex_wrap_prop(
799 self.ctx.styled_dom, dom_id, &node_state,
800 );
801 match wrap_prop {
802 Some(val) => matches!(
803 val.get_property_or_default().unwrap_or_default(),
804 LayoutFlexWrap::NoWrap
805 ),
806 None => true, // default is nowrap
807 }
808 } else {
809 true
810 };
811
812 let min_main = if is_single_line { sum_main_min } else { max_main_min };
813 let max_main = sum_main_max;
814
815 if is_row {
816 Ok(IntrinsicSizes {
817 min_content_width: min_main,
818 max_content_width: max_main,
819 preferred_width: None,
820 min_content_height: max_cross_min,
821 max_content_height: max_cross_max,
822 preferred_height: None,
823 })
824 } else {
825 Ok(IntrinsicSizes {
826 min_content_width: max_cross_min,
827 max_content_width: max_cross_max,
828 preferred_width: None,
829 min_content_height: min_main,
830 max_content_height: max_main,
831 preferred_height: None,
832 })
833 }
834 }
835
836 /// Calculate intrinsic sizes for a table element by aggregating cell content
837 /// widths per column and row heights.
838 /// +spec:table-layout:93b13c - shrink-to-fit for tables uses intrinsic sizing
839 fn calculate_table_intrinsic_sizes(
840 &mut self,
841 tree: &LayoutTree,
842 node_index: usize,
843 child_intrinsics: &[(usize, IntrinsicSizes)],
844 ) -> Result<IntrinsicSizes> {
845 // Collect per-column min/max widths and total row heights.
846 // Table structure: table > row-group? > row > cell
847 let mut col_min: Vec<f32> = Vec::new();
848 let mut col_max: Vec<f32> = Vec::new();
849 let mut total_height = 0.0f32;
850
851 // Iterate rows — children may be row groups (thead/tbody/tfoot) or direct rows
852 let mut rows: Vec<usize> = Vec::new();
853 for &child_idx in tree.children(node_index) {
854 let child = match tree.get(child_idx) { Some(c) => c, None => continue };
855 match child.formatting_context {
856 FormattingContext::TableRow => rows.push(child_idx),
857 FormattingContext::TableRowGroup => {
858 // Row group contains rows
859 for &row_idx in tree.children(child_idx) {
860 if let Some(row) = tree.get(row_idx) {
861 if matches!(row.formatting_context, FormattingContext::TableRow) {
862 rows.push(row_idx);
863 }
864 }
865 }
866 }
867 _ => {}
868 }
869 }
870
871 for &row_idx in &rows {
872 let mut row_height = 0.0f32;
873 let mut col = 0usize;
874 for &cell_idx in tree.children(row_idx) {
875 let cell_intrinsic = child_intrinsics.iter().find(|(k, _)| k == &cell_idx).map(|(_, v)| *v)
876 .unwrap_or_default();
877 // Also check if cell has IFC content we can measure
878 let cell_is = if cell_intrinsic.max_content_width > 0.0 {
879 cell_intrinsic
880 } else {
881 // Try to measure cell content via IFC
882 self.calculate_ifc_root_intrinsic_sizes(tree, cell_idx)
883 .unwrap_or_default()
884 };
885
886 // Add cell box-model extras
887 let cell_node = tree.get(cell_idx);
888 let (h_extras, v_extras) = if let Some(cn) = cell_node {
889 let bp = cn.box_props.unpack();
890 (bp.padding.left + bp.padding.right + bp.border.left + bp.border.right,
891 bp.padding.top + bp.padding.bottom + bp.border.top + bp.border.bottom)
892 } else { (0.0, 0.0) };
893
894 let cell_min = cell_is.min_content_width + h_extras;
895 let cell_max = cell_is.max_content_width + h_extras;
896 let cell_h = cell_is.max_content_height + v_extras;
897
898 if col >= col_min.len() {
899 col_min.push(cell_min);
900 col_max.push(cell_max);
901 } else {
902 col_min[col] = col_min[col].max(cell_min);
903 col_max[col] = col_max[col].max(cell_max);
904 }
905 row_height = row_height.max(cell_h);
906 col += 1;
907 }
908 total_height += row_height;
909 }
910
911 let min_width: f32 = col_min.iter().sum();
912 let max_width: f32 = col_max.iter().sum();
913
914 Ok(IntrinsicSizes {
915 min_content_width: min_width,
916 max_content_width: max_width,
917 min_content_height: total_height,
918 max_content_height: total_height,
919 preferred_width: None,
920 preferred_height: None,
921 })
922 }
923}
924
925/// Gathers all inline content for the intrinsic sizing pass.
926///
927/// This function recursively collects text and inline-level content according to
928/// CSS Sizing Level 3, Section 4.1: "Intrinsic Sizes"
929/// https://www.w3.org/TR/css-sizing-3/#intrinsic-sizes
930///
931/// For inline formatting contexts, we need to gather:
932/// 1. Text nodes (inline content)
933/// 2. Inline-level boxes (display: inline, inline-block, etc.)
934/// 3. Atomic inline-level elements (replaced elements like images)
935///
936/// The key difference from `collect_and_measure_inline_content` in fc.rs is that
937/// this version is used for intrinsic sizing (calculating min/max-content widths)
938/// before the actual layout pass, so it must recursively gather content from
939/// inline descendants without laying them out first.
940fn collect_inline_content_for_sizing<T: ParsedFontTrait>(
941 ctx: &mut LayoutContext<'_, T>,
942 tree: &LayoutTree,
943 ifc_root_index: usize,
944) -> Result<Vec<InlineContent>> {
945 ctx.debug_log(&format!(
946 "Collecting inline content from node {} for intrinsic sizing",
947 ifc_root_index
948 ));
949
950 let mut content = Vec::new();
951
952 // Recursively collect inline content from this node and its inline descendants
953 collect_inline_content_recursive(ctx, tree, ifc_root_index, &mut content)?;
954
955 ctx.debug_log(&format!(
956 "Collected {} inline content items from node {}",
957 content.len(),
958 ifc_root_index
959 ));
960
961 Ok(content)
962}
963
964/// Recursive helper for collecting inline content.
965///
966/// According to CSS Sizing Level 3, the intrinsic size of an inline formatting context
967/// is based on all inline-level content, including text in nested inline elements.
968///
969/// This function:
970/// - Collects text from the current node if it's a text node
971/// - Collects text from DOM children (text nodes may not be in layout tree)
972/// - Recursively collects from inline children (display: inline)
973/// - Treats non-inline children as atomic inline-level boxes
974fn collect_inline_content_recursive<T: ParsedFontTrait>(
975 ctx: &mut LayoutContext<'_, T>,
976 tree: &LayoutTree,
977 node_index: usize,
978 content: &mut Vec<InlineContent>,
979) -> Result<()> {
980 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
981
982 // CRITICAL FIX: Text nodes may exist in the DOM but not as separate layout nodes!
983 // We need to check the DOM children for text content.
984 let Some(dom_id) = node.dom_node_id else {
985 // No DOM ID means this is a synthetic node, skip text extraction
986 return process_layout_children(ctx, tree, node_index, content);
987 };
988
989 // First check if THIS node is a text node
990 if let Some(text) = extract_text_from_node(ctx.styled_dom, dom_id) {
991 let style_props = Arc::new(get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref(), azul_css::props::basic::PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
992 ctx.debug_log(&format!("Found text in node {}: '{}'", node_index, text));
993 // Use split_text_for_whitespace to correctly handle white-space: pre with \n
994 let text_items = split_text_for_whitespace(
995 ctx.styled_dom,
996 dom_id,
997 &text,
998 style_props,
999 );
1000 content.extend(text_items);
1001 }
1002
1003 // CRITICAL: Also check DOM children for text nodes!
1004 // Text nodes are often not represented as separate layout nodes.
1005 // However, we must SKIP children that already have a layout tree entry,
1006 // because those will be handled by process_layout_children() below.
1007 // Without this guard, text nodes present in both DOM and layout tree
1008 // get collected twice, causing inline-block containers to be ~2x too wide.
1009 let node_hierarchy = &ctx.styled_dom.node_hierarchy.as_container();
1010 for child_id in dom_id.az_children(node_hierarchy) {
1011 // Skip DOM children that have layout tree nodes - they will be
1012 // processed via process_layout_children -> collect_inline_content_recursive
1013 if tree.dom_to_layout.contains_key(&child_id) {
1014 continue;
1015 }
1016 // Check if this DOM child is a text node
1017 let child_dom_node = &ctx.styled_dom.node_data.as_container()[child_id];
1018 if let NodeType::Text(text_data) = child_dom_node.get_node_type() {
1019 let text = text_data.as_str().to_string();
1020 let style_props = Arc::new(get_style_properties(ctx.styled_dom, child_id, ctx.system_style.as_ref(), azul_css::props::basic::PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
1021 ctx.debug_log(&format!(
1022 "Found text in DOM child of node {}: '{}'",
1023 node_index, text
1024 ));
1025 // Use split_text_for_whitespace to correctly handle white-space: pre with \n
1026 let text_items = split_text_for_whitespace(
1027 ctx.styled_dom,
1028 child_id,
1029 &text,
1030 style_props,
1031 );
1032 content.extend(text_items);
1033 }
1034 }
1035
1036 process_layout_children(ctx, tree, node_index, content)
1037}
1038
1039/// Helper to process layout tree children for inline content collection
1040fn process_layout_children<T: ParsedFontTrait>(
1041 ctx: &mut LayoutContext<'_, T>,
1042 tree: &LayoutTree,
1043 node_index: usize,
1044 content: &mut Vec<InlineContent>,
1045) -> Result<()> {
1046 use azul_css::props::basic::SizeMetric;
1047 use azul_css::props::layout::{LayoutHeight, LayoutWidth};
1048
1049 // Process layout tree children (these are elements with layout properties)
1050 for &child_index in tree.children(node_index) {
1051 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1052 let Some(child_dom_id) = child_node.dom_node_id else {
1053 continue;
1054 };
1055
1056 let display = get_display_property(ctx.styled_dom, Some(child_dom_id));
1057
1058 // CSS Sizing Level 3: Inline-level boxes participate in the IFC
1059 if display.unwrap_or_default() == LayoutDisplay::Inline {
1060 // Recursively collect content from inline children
1061 // This is CRITICAL for proper intrinsic width calculation!
1062 ctx.debug_log(&format!(
1063 "Recursing into inline child at node {}",
1064 child_index
1065 ));
1066 collect_inline_content_recursive(ctx, tree, child_index, content)?;
1067 } else {
1068 // Non-inline children are treated as atomic inline-level boxes
1069 // (e.g., inline-block, images, floats)
1070 // Their intrinsic size must have been calculated in the bottom-up pass
1071 let intrinsic_sizes = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
1072
1073 // CSS 2.2 § 10.3.9: For inline-block elements with explicit CSS width/height,
1074 // use the CSS-defined values instead of intrinsic sizes.
1075 let node_state =
1076 &ctx.styled_dom.styled_nodes.as_container()[child_dom_id].styled_node_state;
1077 let css_width = get_css_width(ctx.styled_dom, child_dom_id, node_state);
1078 let css_height = get_css_height(ctx.styled_dom, child_dom_id, node_state);
1079
1080 // Resolve CSS width - use explicit value if set, otherwise fall back to intrinsic
1081 let used_width = match css_width {
1082 MultiValue::Exact(LayoutWidth::Px(px)) => {
1083 // Convert PixelValue to f32
1084 use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
1085 match px.metric {
1086 SizeMetric::Px => px.number.get(),
1087 SizeMetric::Pt => px.number.get() * PT_TO_PX,
1088 SizeMetric::In => px.number.get() * 96.0,
1089 SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
1090 SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
1091 SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
1092 // +spec:containing-block:495930 - percentages in intrinsic sizing fall back to intrinsic contribution (css-sizing-3 §5.2.1)
1093 // For percentages and viewport units, fall back to intrinsic
1094 // +spec:containing-block:5246c0 - cyclic percentage: when containing block size depends on this box's intrinsic contribution, percentages fall back to intrinsic size
1095 // +spec:containing-block:598124 - cyclic percentage contributions use intrinsic size
1096 // +spec:height-calculation:ca9f19 - percentage-sized boxes use intrinsic size as contribution during intrinsic sizing
1097 // +spec:width-calculation:7a384a - percentage-sized boxes behave as width:auto for intrinsic contributions (cyclic percentage)
1098 _ => intrinsic_sizes.max_content_width,
1099 }
1100 }
1101 MultiValue::Exact(LayoutWidth::MinContent) => intrinsic_sizes.min_content_width,
1102 MultiValue::Exact(LayoutWidth::MaxContent) => intrinsic_sizes.max_content_width,
1103 MultiValue::Exact(LayoutWidth::FitContent(_)) => {
1104 // During intrinsic sizing, fit-content resolves to max-content
1105 intrinsic_sizes.max_content_width
1106 }
1107 // For Auto or other values, use intrinsic size
1108 _ => intrinsic_sizes.max_content_width,
1109 };
1110
1111 // +spec:containing-block:5145c5 - percentage block-size ignored in content-sized containing blocks during intrinsic sizing
1112 // Resolve CSS height - use explicit value if set, otherwise fall back to intrinsic
1113 let used_height = match css_height {
1114 MultiValue::Exact(LayoutHeight::Px(px)) => {
1115 use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
1116 match px.metric {
1117 SizeMetric::Px => px.number.get(),
1118 SizeMetric::Pt => px.number.get() * PT_TO_PX,
1119 SizeMetric::In => px.number.get() * 96.0,
1120 SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
1121 SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
1122 SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
1123 // +spec:containing-block:7d5e79 - percentages behave as auto when containing block height is auto (cyclic percentage contribution)
1124 // +spec:height-calculation:7d807b - css-sizing-3 §5.2.1: percentage heights behave as auto during intrinsic sizing (cyclic percentage contribution)
1125 // Percentages and viewport units fall back to intrinsic (treated as auto)
1126 _ => intrinsic_sizes.max_content_height,
1127 }
1128 }
1129 // is equivalent to automatic size
1130 MultiValue::Exact(LayoutHeight::MinContent) => intrinsic_sizes.max_content_height,
1131 // is equivalent to automatic size
1132 MultiValue::Exact(LayoutHeight::MaxContent) => intrinsic_sizes.max_content_height,
1133 MultiValue::Exact(LayoutHeight::FitContent(_)) => intrinsic_sizes.max_content_height,
1134 _ => intrinsic_sizes.max_content_height,
1135 };
1136
1137 ctx.debug_log(&format!(
1138 "Found atomic inline child at node {}: display={:?}, intrinsic_width={}, used_width={}, css_width={:?}",
1139 child_index, display, intrinsic_sizes.max_content_width, used_width, css_width
1140 ));
1141
1142 // Represent as a rectangular shape with the resolved dimensions
1143 content.push(InlineContent::Shape(InlineShape {
1144 shape_def: ShapeDefinition::Rectangle {
1145 size: crate::text3::cache::Size {
1146 width: used_width,
1147 height: used_height,
1148 },
1149 corner_radius: None,
1150 },
1151 fill: None,
1152 stroke: None,
1153 baseline_offset: used_height,
1154 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
1155 source_node_id: Some(child_dom_id),
1156 }));
1157 }
1158 }
1159
1160 Ok(())
1161}
1162
1163// Keep old name as an alias for backward compatibility
1164pub fn collect_inline_content<T: ParsedFontTrait>(
1165 ctx: &mut LayoutContext<'_, T>,
1166 tree: &LayoutTree,
1167 ifc_root_index: usize,
1168) -> Result<Vec<InlineContent>> {
1169 collect_inline_content_for_sizing(ctx, tree, ifc_root_index)
1170}
1171
1172// +spec:height-calculation:1c899b - width and height properties specify the preferred size of the box
1173/// Calculates the used size of a single node based on its CSS properties and
1174/// the available space provided by its containing block.
1175///
1176/// // +spec:display-contents:71ccde - extrinsic sizing: size determined by context (containing block), not contents
1177///
1178/// This implementation correctly handles writing modes and percentage-based sizes
1179/// according to the CSS specification:
1180/// 1. `width` and `height` CSS properties are resolved to pixel values. Percentages are calculated
1181/// based on the containing block's PHYSICAL dimensions (`width` for `width`, `height` for
1182/// `height`), regardless of writing mode.
1183/// 2. The resolved physical `width` is then mapped to the node's logical CROSS size.
1184/// 3. The resolved physical `height` is then mapped to the node's logical MAIN size.
1185/// 4. A final `LogicalSize` is constructed from these logical dimensions.
1186// +spec:overflow:3c4f25 - auto box sizes: four auto-determined size types resolved here
1187// +spec:width-calculation:fb0629 - width/margin used values depend on box type, auto replaced by suitable value
1188pub fn calculate_used_size_for_node(
1189 styled_dom: &StyledDom,
1190 dom_id: Option<NodeId>,
1191 containing_block_size: LogicalSize,
1192 intrinsic: IntrinsicSizes,
1193 _box_props: &BoxProps,
1194 viewport_size: LogicalSize,
1195) -> Result<LogicalSize> {
1196 let Some(id) = dom_id else {
1197 // Anonymous boxes:
1198 // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit from their enclosing box.
1199 // The inline dimension fills the containing block's inline size,
1200 // and the block dimension is auto (content-based).
1201 // In horizontal-tb: inline=width, block=height.
1202 // In vertical modes: inline=height, block=width.
1203 //
1204 // Since anonymous boxes don't have a DOM node, we default to horizontal-tb.
1205 // The parent's writing mode is already reflected in containing_block_size.
1206 return Ok(LogicalSize::new(
1207 containing_block_size.width,
1208 if intrinsic.max_content_height > 0.0 {
1209 intrinsic.max_content_height
1210 } else {
1211 // Auto height - will be resolved from content
1212 0.0
1213 },
1214 ));
1215 };
1216
1217 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1218 let css_width = get_css_width(styled_dom, id, node_state);
1219 let css_height = get_css_height(styled_dom, id, node_state);
1220 let writing_mode = get_writing_mode(styled_dom, id, node_state);
1221 let display = get_display_property(styled_dom, Some(id));
1222 let position = get_position_type(styled_dom, dom_id);
1223
1224 // Construct the full WritingModeContext from resolved styles.
1225 // This determines how logical dimensions (inline/block) map to physical (width/height).
1226 let wm_ctx = WritingModeContext::new(
1227 writing_mode.unwrap_or_default(),
1228 get_direction_property(styled_dom, id, node_state).unwrap_or_default(),
1229 get_text_orientation_property(styled_dom, id, node_state).unwrap_or_default(),
1230 );
1231 let is_vertical = !wm_ctx.is_horizontal();
1232
1233 // +spec:display-property:06e0b1 - form controls (non-image) treated as non-replaced
1234 // Determine if this element is a replaced element (images, virtual views)
1235 let node_data = &styled_dom.node_data.as_container()[id];
1236 let is_replaced = matches!(node_data.get_node_type(), NodeType::Image(_))
1237 || node_data.is_virtual_view_node();
1238
1239 // +spec:width-calculation:79cdf8 - inline non-replaced: width property does not apply
1240 // +spec:width-calculation:972e86 - §10.3.1: width property does not apply to inline non-replaced elements
1241 // For inline non-replaced elements, override any explicit width to Auto.
1242 let css_width = if display.unwrap_or_default() == LayoutDisplay::Inline
1243 && !is_replaced
1244 {
1245 MultiValue::Exact(LayoutWidth::Auto)
1246 } else {
1247 css_width
1248 };
1249
1250 // +spec:box-model:1197a5 - height does not apply to non-replaced inline elements
1251 // +spec:display-property:9cb33d - height does not apply to inline boxes
1252 // +spec:height-calculation:c03717 - height does not apply to inline non-replaced elements
1253 // CSS 2.2 §10.6.1 / CSS Inline 3 §6.4: height property does not apply to
1254 // inline, non-replaced elements. Override any explicit height to Auto.
1255 let css_height = if display.unwrap_or_default() == LayoutDisplay::Inline
1256 && !is_replaced
1257 {
1258 MultiValue::Exact(LayoutHeight::Auto)
1259 } else {
1260 css_height
1261 };
1262
1263 // Remember if width/height were auto before consuming them
1264 let width_is_auto = css_width.is_auto() || matches!(&css_width, MultiValue::Exact(LayoutWidth::Auto));
1265 let height_is_auto = css_height.is_auto() || matches!(&css_height, MultiValue::Exact(LayoutHeight::Auto));
1266
1267 // +spec:intrinsic-sizing:9e1c9d - non-quantitative values (auto, min-content, max-content) are not influenced by box-sizing
1268 let width_is_quantitative = matches!(
1269 &css_width,
1270 MultiValue::Exact(LayoutWidth::Px(_) | LayoutWidth::FitContent(_) | LayoutWidth::Calc(_))
1271 );
1272 let height_is_quantitative = matches!(
1273 &css_height,
1274 MultiValue::Exact(LayoutHeight::Px(_) | LayoutHeight::FitContent(_) | LayoutHeight::Calc(_))
1275 );
1276
1277 // +spec:width-calculation:50d67a - automatic sizing concepts (width/height auto resolution)
1278 // +spec:width-calculation:564315 - §10.3 width calculation dispatch for all box types
1279 // Step 1: Resolve the CSS `width` property into a concrete pixel value.
1280 // CSS `width` always refers to the physical horizontal dimension, regardless of writing mode.
1281 // Percentage values resolve against the containing block's physical width.
1282 // In horizontal-tb: width = inline size. In vertical modes: width = block size.
1283 // The physical-to-logical mapping happens in Step 5 below.
1284 // Percentage values for `width` are resolved against the containing block's width.
1285 // +spec:width-calculation:febf0c - width/height "behaves as auto" when computed auto or percentage resolves against indefinite
1286 let resolved_width = match css_width.unwrap_or_default() {
1287 LayoutWidth::Auto => {
1288 // +spec:width-calculation:ed6a34 - auto width on replaced element uses intrinsic width
1289 // CSS 2.2 §10.3.2: If 'width' has a computed value of 'auto', and the element
1290 // has an intrinsic width, then that intrinsic width is the used value of 'width'.
1291 // +spec:replaced-elements:992ea5 - block-level replaced elements use inline replaced width rules
1292 // §10.3.4: "The used value of 'width' is determined as for inline replaced elements."
1293 // +spec:replaced-elements:36de3e - §10.3.2/§10.3.4: auto width for inline/block replaced elements uses intrinsic width
1294 // +spec:replaced-elements:b9a780 - §10.3.2: inline replaced auto width = intrinsic width (conditions resolved during intrinsic size calc)
1295 if is_replaced {
1296 // +spec:width-calculation:b41dbe - floating/inline replaced: auto width = intrinsic width
1297 // +spec:width-calculation:c62d35 - §10.3.2: auto width for replaced elements uses intrinsic width
1298 // +spec:width-calculation:d87ca4 - abs-replaced: auto width+height uses intrinsic width
1299 // For replaced elements (inline or block-level), auto width = intrinsic width.
1300 // The intrinsic sizes were already computed with the 300px fallback per §10.3.2.
1301 intrinsic.max_content_width
1302 }
1303 // +spec:intrinsic-sizing:560697 - shrink-to-fit = clamp(min-content, stretch-fit, max-content)
1304 else if get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None) != LayoutFloat::None {
1305 // +spec:width-calculation:8d7047 - shrink-to-fit width per CSS2.1§10.3.5
1306 // +spec:width-calculation:0bb038 - shrink-to-fit for floating non-replaced elements (§10.3.5)
1307 // shrink-to-fit = min(max(preferred minimum width, available width), preferred width)
1308 // +spec:table-layout:93b13c - shrink-to-fit for floats, inline-blocks, table-cells;
1309 // orthogonal flows would require child block size as input (not yet implemented)
1310 // +spec:width-calculation:a6fd29 - shrink-to-fit width for floats: min(max(preferred minimum, available), preferred)
1311 // CSS 2.2 §10.3.5: For floats, auto width = shrink-to-fit
1312 let available_width = (containing_block_size.width
1313 - _box_props.margin.left
1314 - _box_props.margin.right
1315 - _box_props.border.left
1316 - _box_props.border.right
1317 - _box_props.padding.left
1318 - _box_props.padding.right)
1319 .max(0.0);
1320 let preferred_minimum = intrinsic.min_content_width;
1321 let preferred = intrinsic.max_content_width;
1322 preferred_minimum.max(available_width).min(preferred).max(0.0)
1323 }
1324 else if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
1325 // +spec:intrinsic-sizing:12a531 - abspos auto size = fit-content (shrink-to-fit)
1326 // +spec:width-calculation:0bb038 - shrink-to-fit width for abs-pos non-replaced elements
1327 // §10.3.7: abs-pos elements with auto width use shrink-to-fit
1328 // +spec:intrinsic-sizing:087b57 - abspos automatic size is fit-content (shrink-to-fit)
1329 // +spec:width-calculation:1661b4 - abs-pos non-replaced auto width uses shrink-to-fit (§10.3.7)
1330 // shrink-to-fit = min(max(preferred_minimum, available), preferred)
1331 let available_width = (containing_block_size.width
1332 - _box_props.margin.left
1333 - _box_props.margin.right
1334 - _box_props.border.left
1335 - _box_props.border.right
1336 - _box_props.padding.left
1337 - _box_props.padding.right)
1338 .max(0.0);
1339 let preferred_minimum = intrinsic.min_content_width;
1340 let preferred = intrinsic.max_content_width;
1341 preferred_minimum.max(available_width).min(preferred).max(0.0)
1342 } else {
1343 // +spec:width-calculation:472065 - orthogonal flow auto inline size: if this block
1344 // container establishes an orthogonal flow (child writing mode axis differs from
1345 // parent), its auto inline size should use the parent's block-axis size as available
1346 // space, falling back to the initial containing block size. Currently not implemented;
1347 // auto width always resolves against the containing block's width.
1348 // 'auto' width resolution depends on the display type.
1349 match display.unwrap_or_default() {
1350 LayoutDisplay::Block
1351 | LayoutDisplay::FlowRoot
1352 | LayoutDisplay::ListItem
1353 | LayoutDisplay::Flex
1354 | LayoutDisplay::Grid => {
1355 // +spec:box-model:503ea3 - margin + border + padding + width = containing block width
1356 // +spec:box-model:5ed651 - stretch fit: size minus margins (auto=0), border, padding, floored at 0
1357 // +spec:box-model:33b951 - stretch-fit inline size: available space minus margins/border/padding, floored at zero
1358 // +spec:box-model:30b4d0 - stretch fit: available size minus margins (auto as zero), border, padding, floored at zero
1359 // +spec:width-calculation:e2c8f6 - auto width for non-replaced blocks in normal flow per CSS2.1§10.3.3
1360 // For block-level non-replaced elements,
1361 // 'auto' width fills the containing block (minus margins, borders, padding).
1362 // CSS 2.2 §10.3.3: width = containing_block_width - margin_left -
1363 // margin_right - border_left - border_right - padding_left - padding_right
1364 // +spec:width-calculation:aef2da - auto width: other auto values become 0, width follows from constraint equality
1365 let available_width = containing_block_size.width
1366 - _box_props.margin.left
1367 - _box_props.margin.right
1368 - _box_props.border.left
1369 - _box_props.border.right
1370 - _box_props.padding.left
1371 - _box_props.padding.right;
1372
1373 available_width.max(0.0)
1374 }
1375 LayoutDisplay::InlineBlock | LayoutDisplay::InlineGrid | LayoutDisplay::InlineFlex => {
1376 // +spec:width-calculation:c01de8 - inline-block auto width uses shrink-to-fit (§10.3.9)
1377 // shrink-to-fit = min(max(preferred_minimum, available), preferred)
1378 let available_width = (containing_block_size.width
1379 - _box_props.margin.left
1380 - _box_props.margin.right
1381 - _box_props.border.left
1382 - _box_props.border.right
1383 - _box_props.padding.left
1384 - _box_props.padding.right)
1385 .max(0.0);
1386 let preferred_minimum = intrinsic.min_content_width;
1387 let preferred = intrinsic.max_content_width;
1388 preferred_minimum.max(available_width).min(preferred).max(0.0)
1389 }
1390 LayoutDisplay::Inline => {
1391 // For inline elements, 'auto' width is the intrinsic/max-content width
1392 intrinsic.max_content_width
1393 }
1394 LayoutDisplay::Table | LayoutDisplay::InlineTable => intrinsic.max_content_width,
1395 // Table cells: during intrinsic measurement, intrinsic sizes
1396 // aren't known yet (0). Use containing block width so content
1397 // can expand and be measured. The table layout algorithm sets
1398 // the final cell width from computed column widths.
1399 LayoutDisplay::TableCell => {
1400 if intrinsic.max_content_width > 0.0 {
1401 intrinsic.max_content_width
1402 } else {
1403 (containing_block_size.width
1404 - _box_props.margin.left
1405 - _box_props.margin.right
1406 - _box_props.border.left
1407 - _box_props.border.right
1408 - _box_props.padding.left
1409 - _box_props.padding.right)
1410 .max(0.0)
1411 }
1412 }
1413 // Other display types use intrinsic sizing
1414 _ => intrinsic.max_content_width,
1415 }
1416 }
1417 }
1418 LayoutWidth::Px(px) => {
1419 // Resolve percentage or absolute pixel value
1420 use azul_css::props::basic::{
1421 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1422 SizeMetric,
1423 };
1424 let pixels_opt = match px.metric {
1425 SizeMetric::Px => Some(px.number.get()),
1426 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1427 SizeMetric::In => Some(px.number.get() * 96.0),
1428 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1429 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1430 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1431 SizeMetric::Vw => Some(px.number.get() / 100.0 * viewport_size.width),
1432 SizeMetric::Vh => Some(px.number.get() / 100.0 * viewport_size.height),
1433 SizeMetric::Vmin => Some(px.number.get() / 100.0 * viewport_size.width.min(viewport_size.height)),
1434 SizeMetric::Vmax => Some(px.number.get() / 100.0 * viewport_size.width.max(viewport_size.height)),
1435 SizeMetric::Percent => None,
1436 };
1437
1438 match pixels_opt {
1439 Some(pixels) => pixels,
1440 None => match px.to_percent() {
1441 Some(p) => {
1442 let result = resolve_percentage_with_box_model(
1443 containing_block_size.width,
1444 p.get(),
1445 (_box_props.margin.left, _box_props.margin.right),
1446 (_box_props.border.left, _box_props.border.right),
1447 (_box_props.padding.left, _box_props.padding.right),
1448 );
1449
1450 result
1451 }
1452 None => intrinsic.max_content_width,
1453 },
1454 }
1455 }
1456 // +spec:intrinsic-sizing:069c75 - min-content, max-content, fit-content() sizing value keywords
1457 // +spec:intrinsic-sizing:1ce4fa - §3.2 min-content/max-content/fit-content() sizing values
1458 LayoutWidth::MinContent => intrinsic.min_content_width,
1459 LayoutWidth::MaxContent => intrinsic.max_content_width,
1460 // +spec:width-calculation:7b2128 - fit-content formula and non-negative inner size flooring (css-sizing-3 §3.2)
1461 // +spec:width-calculation:bf694a - min-content, max-content, fit-content() sizing values
1462 // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
1463 LayoutWidth::FitContent(px) => {
1464 use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1465 let arg = super::calc::resolve_pixel_value_with_viewport(
1466 &px, containing_block_size.width, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
1467 viewport_size.width, viewport_size.height,
1468 );
1469 intrinsic.max_content_width.min(intrinsic.min_content_width.max(arg))
1470 }
1471 LayoutWidth::Calc(items) => {
1472 use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1473 let em = get_element_font_size(styled_dom, id, node_state);
1474 let calc_ctx = super::calc::CalcResolveContext {
1475 items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1476 };
1477 super::calc::evaluate_calc(&calc_ctx, containing_block_size.width)
1478 }
1479 };
1480 // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
1481 let resolved_width = resolved_width.max(0.0);
1482
1483 // +spec:height-calculation:7880e3 - Distinction between box types for height/margin calculation
1484 // +spec:height-calculation:753d8d - Height calculation for various box types (§10.6)
1485 // +spec:positioning:d5184e - percentage height resolved against containing block height
1486 // +spec:height-calculation:6a6cac - §10.5 content height resolution (auto, length, percentage)
1487 // +spec:height-calculation:d398e4 - §10.5/10.6 height property resolution for different box types
1488 // Step 2: Resolve the CSS `height` property into a concrete pixel value.
1489 // CSS `height` always refers to the physical vertical dimension, regardless of writing mode.
1490 // Percentage values resolve against the containing block's physical height.
1491 // In horizontal-tb: height = block size. In vertical modes: height = inline size.
1492 // The physical-to-logical mapping happens in Step 5 below.
1493 // Percentage values for `height` are resolved against the containing block's height.
1494 // +spec:height-calculation:0b5b0a - abs-pos replaced elements use intrinsic height for auto
1495 let resolved_height = match css_height.unwrap_or_default() {
1496 LayoutHeight::Auto => {
1497 // +spec:width-calculation:be5eb1 - auto height means available block space is infinite (unconstrained)
1498 // +spec:replaced-elements:994ac6 - §10.6.2: auto height for replaced elements uses intrinsic height or (used width)/ratio
1499 //
1500 // For block-level non-replaced containers in normal flow, CSS 2.2 §10.6.3
1501 // says auto height is resolved from children after layout. We return 0.0
1502 // as a placeholder; `apply_content_based_height` (cache.rs) overwrites it
1503 // with the laid-out content size. Reading `intrinsic.max_content_height`
1504 // here is unsafe: when the intrinsic pass short-circuits (e.g. a non-STF
1505 // subtree whose intrinsics are never consumed), that field is zero anyway
1506 // — so any caller that "trusts" the pre-layout value is depending on an
1507 // estimate that isn't guaranteed to exist.
1508 //
1509 // Shrink-to-fit contexts (inline-block, float, abspos, table/table-cell)
1510 // genuinely need intrinsic for width sizing; auto-height for those is
1511 // still driven by content, but we keep the intrinsic fallback for
1512 // backwards compatibility with the existing paths.
1513 match display.unwrap_or_default() {
1514 LayoutDisplay::Block
1515 | LayoutDisplay::FlowRoot
1516 | LayoutDisplay::ListItem
1517 | LayoutDisplay::Flex
1518 | LayoutDisplay::Grid => 0.0,
1519 // Inline: height property does not apply (§10.6.1), handled earlier
1520 // via css_height override, but be explicit anyway.
1521 LayoutDisplay::Inline => 0.0,
1522 // Shrink-to-fit and intrinsically-sized: keep using intrinsic pre-layout.
1523 _ => intrinsic.max_content_height,
1524 }
1525 }
1526 LayoutHeight::Px(px) => {
1527 // Resolve percentage or absolute pixel value
1528 use azul_css::props::basic::{
1529 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1530 SizeMetric,
1531 };
1532 let pixels_opt = match px.metric {
1533 SizeMetric::Px => Some(px.number.get()),
1534 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1535 SizeMetric::In => Some(px.number.get() * 96.0),
1536 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1537 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1538 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1539 SizeMetric::Vw => Some(px.number.get() / 100.0 * viewport_size.width),
1540 SizeMetric::Vh => Some(px.number.get() / 100.0 * viewport_size.height),
1541 SizeMetric::Vmin => Some(px.number.get() / 100.0 * viewport_size.width.min(viewport_size.height)),
1542 SizeMetric::Vmax => Some(px.number.get() / 100.0 * viewport_size.width.max(viewport_size.height)),
1543 SizeMetric::Percent => None,
1544 };
1545
1546 match pixels_opt {
1547 Some(pixels) => pixels,
1548 // +spec:height-calculation:37bc8c - percentage heights resolve against definite containing block height
1549 None => match px.to_percent() {
1550 Some(p) => resolve_percentage_with_box_model(
1551 containing_block_size.height,
1552 p.get(),
1553 (_box_props.margin.top, _box_props.margin.bottom),
1554 (_box_props.border.top, _box_props.border.bottom),
1555 (_box_props.padding.top, _box_props.padding.bottom),
1556 ),
1557 None => intrinsic.max_content_height,
1558 },
1559 }
1560 }
1561 // equivalent to automatic size (not min_content_height which is height at min-content width)
1562 LayoutHeight::MinContent => intrinsic.max_content_height,
1563 // equivalent to automatic size
1564 LayoutHeight::MaxContent => intrinsic.max_content_height,
1565 // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
1566 // For block axis, both min-content and max-content equal auto height
1567 LayoutHeight::FitContent(px) => {
1568 use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1569 let arg = super::calc::resolve_pixel_value_with_viewport(
1570 &px, containing_block_size.height, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
1571 viewport_size.width, viewport_size.height,
1572 );
1573 let auto_height = intrinsic.max_content_height;
1574 auto_height.min(auto_height.max(arg))
1575 }
1576 LayoutHeight::Calc(items) => {
1577 use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1578 let em = get_element_font_size(styled_dom, id, node_state);
1579 let calc_ctx = super::calc::CalcResolveContext {
1580 items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1581 };
1582 super::calc::evaluate_calc(&calc_ctx, containing_block_size.height)
1583 }
1584 };
1585 // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
1586 let resolved_height = resolved_height.max(0.0);
1587
1588 // +spec:replaced-elements:5a85ce - abs-pos replaced: derive auto width from height × intrinsic ratio
1589 // +spec:replaced-elements:aedb26 - abs-pos replaced: both auto, ratio but no intrinsic w/h → block constraint
1590 // CSS Position 3 §6.2 (abs-replaced-width): For absolutely positioned replaced elements,
1591 // if width is auto and the element has an intrinsic ratio, width may be derived from height.
1592 let (resolved_width, resolved_height) = if is_replaced
1593 && width_is_auto
1594 && matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed)
1595 {
1596 let has_intrinsic_width = intrinsic.preferred_width.map_or(false, |w| w > 0.0);
1597 let has_intrinsic_height = intrinsic.preferred_height.map_or(false, |h| h > 0.0);
1598 let intrinsic_ratio = match (intrinsic.preferred_width, intrinsic.preferred_height) {
1599 (Some(iw), Some(ih)) if ih > 0.0 => Some(iw / ih),
1600 _ => None,
1601 };
1602
1603 if let Some(ratio) = intrinsic_ratio {
1604 if height_is_auto && !has_intrinsic_width && has_intrinsic_height {
1605 // §6.2 case: both auto, no intrinsic width, has intrinsic height + ratio
1606 // → width = used height × ratio
1607 (resolved_height * ratio, resolved_height)
1608 } else if !height_is_auto {
1609 // §6.2 case: width auto, height not auto, has intrinsic ratio
1610 // → width = used height × ratio
1611 (resolved_height * ratio, resolved_height)
1612 } else if height_is_auto && !has_intrinsic_width && !has_intrinsic_height {
1613 // §6.2 case: both auto, has ratio but no intrinsic width or height
1614 // → use block-level non-replaced constraint equation for width
1615 let block_width = (containing_block_size.width
1616 - _box_props.margin.left
1617 - _box_props.margin.right
1618 - _box_props.border.left
1619 - _box_props.border.right
1620 - _box_props.padding.left
1621 - _box_props.padding.right)
1622 .max(0.0);
1623 (block_width, block_width / ratio)
1624 } else {
1625 (resolved_width, resolved_height)
1626 }
1627 } else {
1628 (resolved_width, resolved_height)
1629 }
1630 } else {
1631 (resolved_width, resolved_height)
1632 };
1633
1634 // +spec:min-max-sizing:58869e - sizing properties width/height/min-width/min-height/max-width/max-height applied here
1635 // +spec:min-max-sizing:2e2414 - max-width/max-height specify maximum box dimensions, applied here
1636 // +spec:min-max-sizing:73f51a - tentative width clamped by max-width then min-width per §10.4
1637 // +spec:min-max-sizing:e98c4e - preferred size clamped by min/max, box-sizing handled
1638 // Step 3: Apply min/max constraints (CSS 2.2 § 10.4 and § 10.7)
1639 // "The tentative used width is calculated (without 'min-width' and 'max-width')
1640 // ...If the tentative used width is greater than 'max-width', the rules above are
1641 // applied again using the computed value of 'max-width' as the computed value for 'width'.
1642 // If the resulting width is smaller than 'min-width', the rules above are applied again
1643 // using the value of 'min-width' as the computed value for 'width'."
1644
1645 // use the constraint violation table to coordinate width+height together;
1646 // for non-replaced elements, apply width and height constraints independently
1647 let has_intrinsic_ratio = intrinsic.preferred_width.is_some()
1648 && intrinsic.preferred_height.is_some()
1649 && intrinsic.preferred_width.unwrap_or(0.0) > 0.0
1650 && intrinsic.preferred_height.unwrap_or(0.0) > 0.0;
1651
1652 // +spec:margin-collapsing:840eb6 - aspect ratio transfers size constraints across dimensions
1653 let (constrained_width, constrained_height) = if has_intrinsic_ratio {
1654 // +spec:width-calculation:ef71c4 - replaced elements with both width/height auto use constraint violation table
1655 // Replaced element with intrinsic ratio: use §10.4 constraint violation table
1656 apply_constraint_violation_table(
1657 styled_dom,
1658 id,
1659 node_state,
1660 resolved_width,
1661 resolved_height,
1662 containing_block_size.width,
1663 containing_block_size.height,
1664 _box_props,
1665 )
1666 } else {
1667 // Non-replaced element: apply width and height constraints independently
1668 let cw = apply_width_constraints(
1669 styled_dom,
1670 id,
1671 node_state,
1672 resolved_width,
1673 containing_block_size.width,
1674 _box_props,
1675 );
1676
1677 let ch = apply_height_constraints(
1678 styled_dom,
1679 id,
1680 node_state,
1681 resolved_height,
1682 containing_block_size.height,
1683 _box_props,
1684 );
1685 (cw, ch)
1686 };
1687
1688 // +spec:box-model:cc170b - box-sizing: border-box includes padding+border in specified size; content-box adds them outside; content size floored at zero
1689 // +spec:box-model:d9d797 - box-sizing: content-box vs border-box dimension interpretation
1690 // +spec:box-model:e2a773 - box-sizing: border-box includes padding+border in width/height; content-box adds them outside
1691 // +spec:box-sizing:8159a8 - box-sizing property indicates whether content-box or border-box is measured
1692 // +spec:box-sizing:b0ff05 - border-box sets border-box to specified size, content-box calculated from it
1693 // +spec:box-sizing:aefeb2 - box-sizing: content-box vs border-box width/height interpretation
1694 // +spec:box-sizing:e2e28c - width/height refer to content-box size by default (content-box); box-sizing: border-box makes them refer to border-box size
1695 // Step 4: Convert to border-box dimensions, respecting box-sizing property
1696 // CSS box-sizing:
1697 // - content-box (default): width/height set content size, border+padding are added
1698 // - border-box: width/height set border-box size, border+padding are included
1699 let box_sizing = match get_css_box_sizing(styled_dom, id, node_state) {
1700 MultiValue::Exact(bs) => bs,
1701 MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
1702 azul_css::props::layout::LayoutBoxSizing::ContentBox
1703 }
1704 };
1705
1706 let (border_box_width, border_box_height) = match box_sizing {
1707 azul_css::props::layout::LayoutBoxSizing::BorderBox => {
1708 // +spec:box-sizing:cdfe09 - box-sizing: border-box makes width/height set the border box
1709 // +spec:box-sizing:3ba6d3 - content-box floors at 0px, so border-box can't be less than padding+border
1710 let min_border_box_w = _box_props.padding.left
1711 + _box_props.padding.right
1712 + _box_props.border.left
1713 + _box_props.border.right;
1714 let min_border_box_h = _box_props.padding.top
1715 + _box_props.padding.bottom
1716 + _box_props.border.top
1717 + _box_props.border.bottom;
1718 // +spec:box-model:4f423b - used values refer to the border box when box-sizing: border-box
1719 // border-box: The width/height values already include border and padding
1720 // CSS Box Sizing Level 3: "the specified width and height (and respective min/max
1721 // properties) on this element determine the border box of the element"
1722 // However, non-quantitative values (auto, min-content, max-content) are not
1723 // influenced by box-sizing, so they still need border+padding added.
1724 // Floor: content-box cannot go negative, so border-box >= padding+border
1725 let bw = if width_is_quantitative {
1726 constrained_width.max(min_border_box_w)
1727 } else {
1728 constrained_width
1729 + _box_props.padding.left
1730 + _box_props.padding.right
1731 + _box_props.border.left
1732 + _box_props.border.right
1733 };
1734 let bh = if height_is_quantitative {
1735 constrained_height.max(min_border_box_h)
1736 } else {
1737 constrained_height
1738 + _box_props.padding.top
1739 + _box_props.padding.bottom
1740 + _box_props.border.top
1741 + _box_props.border.bottom
1742 };
1743 (bw, bh)
1744 }
1745 azul_css::props::layout::LayoutBoxSizing::ContentBox => {
1746 // +spec:box-sizing:fead70 - content-box: width/height set content size, border+padding added outside
1747 let border_box_width = constrained_width
1748 + _box_props.padding.left
1749 + _box_props.padding.right
1750 + _box_props.border.left
1751 + _box_props.border.right;
1752 let border_box_height = constrained_height
1753 + _box_props.padding.top
1754 + _box_props.padding.bottom
1755 + _box_props.border.top
1756 + _box_props.border.bottom;
1757 (border_box_width, border_box_height)
1758 }
1759 };
1760
1761 // +spec:block-formatting-context:c6fb58 - vertical writing modes swap layout dimensions
1762 // +spec:min-max-sizing:d97870 - width/height/min/max refer to physical dimensions; layout rules are logical
1763 // Step 5: Map the resolved physical dimensions to logical dimensions.
1764 //
1765 // CSS Writing Modes Level 4:
1766 // - In horizontal-tb: width = inline (cross) size, height = block (main) size.
1767 // - In vertical-rl/lr: width = block (main) size, height = inline (cross) size.
1768 //
1769 // `from_main_cross` handles this mapping: given (main, cross) and writing mode,
1770 // it produces the correct LogicalSize with physical (width, height).
1771 let (main_size, cross_size) = if is_vertical {
1772 // Vertical writing mode: width is the block (main) dimension,
1773 // height is the inline (cross) dimension.
1774 (border_box_width, border_box_height)
1775 } else {
1776 // Horizontal writing mode (default): width is cross, height is main.
1777 (border_box_height, border_box_width)
1778 };
1779
1780 // Step 6: Construct the final LogicalSize from the logical dimensions.
1781 // +spec:min-max-sizing:2f66a6 - direction-dependent layout rules abstracted to logical start/end via writing mode
1782 let result =
1783 LogicalSize::from_main_cross(main_size, cross_size, writing_mode.unwrap_or_default());
1784
1785 Ok(result)
1786}
1787
1788// +spec:min-max-sizing:b02ebc - sizing properties min-width/max-width/min-height/max-height and preferred aspect ratio
1789// +spec:replaced-elements:740f3e - constraint violation table for replaced elements with intrinsic ratio and both width/height auto
1790// +spec:min-max-sizing:939f2c - use min-width/min-height <length> with aspect ratio for replaced elements
1791// with intrinsic ratios. Implements all 10 cases from the spec table, coordinating
1792// +spec:min-max-sizing:07620d - CSS 2.2 §10.4 constraint violation table for replaced elements with intrinsic ratios
1793// Implements all 11 cases from the spec table, coordinating
1794// width and height together to preserve the aspect ratio while respecting min/max constraints.
1795fn apply_constraint_violation_table(
1796 styled_dom: &StyledDom,
1797 id: NodeId,
1798 node_state: &StyledNodeState,
1799 w: f32, // tentative width (ignoring min/max)
1800 h: f32, // tentative height (ignoring min/max)
1801 containing_block_width: f32,
1802 containing_block_height: f32,
1803 box_props: &BoxProps,
1804) -> (f32, f32) {
1805 use azul_css::props::basic::{
1806 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1807 SizeMetric,
1808 };
1809 use crate::solver3::getters::{
1810 get_css_min_width, get_css_max_width, get_css_min_height, get_css_max_height, MultiValue,
1811 };
1812
1813 // Helper to resolve a pixel value to f32
1814 fn resolve_px(px: &azul_css::props::basic::pixel::PixelValue, containing: f32, box_props: &BoxProps, is_horizontal: bool) -> Option<f32> {
1815 let pixels_opt = match px.metric {
1816 SizeMetric::Px => Some(px.number.get()),
1817 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1818 SizeMetric::In => Some(px.number.get() * 96.0),
1819 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1820 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1821 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1822 SizeMetric::Percent => None,
1823 _ => None,
1824 };
1825 match pixels_opt {
1826 Some(v) => Some(v),
1827 None => {
1828 px.to_percent().map(|p| {
1829 let (m1, m2, b1, b2, p1, p2) = if is_horizontal {
1830 (box_props.margin.left, box_props.margin.right,
1831 box_props.border.left, box_props.border.right,
1832 box_props.padding.left, box_props.padding.right)
1833 } else {
1834 (box_props.margin.top, box_props.margin.bottom,
1835 box_props.border.top, box_props.border.bottom,
1836 box_props.padding.top, box_props.padding.bottom)
1837 };
1838 resolve_percentage_with_box_model(containing, p.get(), (m1, m2), (b1, b2), (p1, p2))
1839 })
1840 }
1841 }
1842 }
1843
1844 // +spec:min-max-sizing:92ab8d - constraint violation table for replaced elements with intrinsic ratio (cyclic percentage contributions use auto fallback)
1845 // +spec:min-max-sizing:ad8605 - min-height/max-height interact with percentage heights; percentages behave as auto in intrinsic contribution calc
1846
1847 // +spec:positioning:c0af55 - automatic minimum size of abspos box is always zero (default 0.0)
1848 // Resolve min-width (default 0)
1849 let min_w = match get_css_min_width(styled_dom, id, node_state) {
1850 MultiValue::Exact(mw) => resolve_px(&mw.inner, containing_block_width, box_props, true).unwrap_or(0.0),
1851 _ => 0.0,
1852 };
1853
1854 // Resolve max-width (default infinity)
1855 let max_w = match get_css_max_width(styled_dom, id, node_state) {
1856 MultiValue::Exact(mw) => {
1857 if mw.inner.number.get() >= core::f32::MAX - 1.0 {
1858 f32::MAX
1859 } else {
1860 resolve_px(&mw.inner, containing_block_width, box_props, true).unwrap_or(f32::MAX)
1861 }
1862 }
1863 _ => f32::MAX,
1864 };
1865
1866 // Resolve min-height (default 0)
1867 let min_h = match get_css_min_height(styled_dom, id, node_state) {
1868 MultiValue::Exact(mh) => resolve_px(&mh.inner, containing_block_height, box_props, false).unwrap_or(0.0),
1869 _ => 0.0,
1870 };
1871
1872 // Resolve max-height (default infinity)
1873 let max_h = match get_css_max_height(styled_dom, id, node_state) {
1874 MultiValue::Exact(mh) => {
1875 if mh.inner.number.get() >= core::f32::MAX - 1.0 {
1876 f32::MAX
1877 } else {
1878 resolve_px(&mh.inner, containing_block_height, box_props, false).unwrap_or(f32::MAX)
1879 }
1880 }
1881 _ => f32::MAX,
1882 };
1883
1884 // max(min, max) so that min ≤ max holds true."
1885 let max_w = max_w.max(min_w);
1886 let max_h = max_h.max(min_h);
1887
1888 // Guard against zero dimensions (avoid division by zero)
1889 if w <= 0.0 || h <= 0.0 {
1890 return (w.max(min_w).min(max_w), h.max(min_h).min(max_h));
1891 }
1892
1893 let w_over = w > max_w;
1894 let w_under = w < min_w;
1895 let h_over = h > max_h;
1896 let h_under = h < min_h;
1897
1898 // +spec:min-max-sizing:713560 - constraint violation table for replaced elements with intrinsic ratio
1899 match (w_over, w_under, h_over, h_under) {
1900 // Row 1: no constraint violation
1901 (false, false, false, false) => (w, h),
1902
1903 // Row 2: w > max-width only
1904 (true, false, false, false) => {
1905 (max_w, (max_w * h / w).max(min_h))
1906 }
1907
1908 // Row 3: w < min-width only
1909 (false, true, false, false) => {
1910 (min_w, (min_w * h / w).min(max_h))
1911 }
1912
1913 // Row 4: h > max-height only
1914 (false, false, true, false) => {
1915 ((max_h * w / h).max(min_w), max_h)
1916 }
1917
1918 // Row 5: h < min-height only
1919 (false, false, false, true) => {
1920 ((min_h * w / h).min(max_w), min_h)
1921 }
1922
1923 // Row 6+7: (w > max-width) and (h > max-height)
1924 (true, false, true, false) => {
1925 if max_w / w <= max_h / h {
1926 (max_w, (max_w * h / w).max(min_h))
1927 } else {
1928 ((max_h * w / h).max(min_w), max_h)
1929 }
1930 }
1931
1932 // Row 8+9: (w < min-width) and (h < min-height)
1933 (false, true, false, true) => {
1934 if min_w / w <= min_h / h {
1935 ((min_h * w / h).min(max_w), min_h)
1936 } else {
1937 (min_w, (min_w * h / w).min(max_h))
1938 }
1939 }
1940
1941 // Row 10: (w < min-width) and (h > max-height)
1942 (false, true, true, false) => (min_w, max_h),
1943
1944 // Row 11: (w > max-width) and (h < min-height)
1945 (true, false, false, true) => (max_w, min_h),
1946
1947 // Fallback (impossible combinations like w_over && w_under)
1948 _ => (w.max(min_w).min(max_w), h.max(min_h).min(max_h)),
1949 }
1950}
1951
1952// +spec:min-max-sizing:114b53 - min-width/max-width/min-height/max-height property definitions: initial values, percentage resolution against containing block, applies to elements accepting width/height
1953// +spec:min-max-sizing:12667d - width/height/min-width/min-height/max-width/max-height properties from CSS Sizing 3
1954/// +spec:min-max-sizing:205e9e - intrinsic size constraints (min/max-content contributions, min/max sizing properties)
1955// +spec:min-max-sizing:cac146 - min-width/min-height specify minimum box dimensions; max overridden by min
1956// +spec:width-calculation:e77d58 - min/max-width clamping algorithm per CSS 2.2 § 10.4
1957// +spec:width-calculation:1d63f0 - min-width/max-width property resolution and value meanings
1958/// Apply min-width and max-width constraints to tentative width
1959/// Per CSS 2.2 § 10.4: min-width overrides max-width if min > max
1960fn apply_width_constraints(
1961 styled_dom: &StyledDom,
1962 id: NodeId,
1963 node_state: &StyledNodeState,
1964 tentative_width: f32,
1965 containing_block_width: f32,
1966 box_props: &BoxProps,
1967) -> f32 {
1968 use azul_css::props::basic::{
1969 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1970 SizeMetric,
1971 };
1972
1973 use crate::solver3::getters::{get_css_max_width, get_css_min_width, MultiValue};
1974
1975 // +spec:display-property:0c55e5 - auto min-width resolves to 0 for CSS2 display types
1976 // Resolve min-width (default is 0)
1977 let min_width = match get_css_min_width(styled_dom, id, node_state) {
1978 MultiValue::Exact(mw) => {
1979 let px = &mw.inner;
1980 let pixels_opt = match px.metric {
1981 SizeMetric::Px => Some(px.number.get()),
1982 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1983 SizeMetric::In => Some(px.number.get() * 96.0),
1984 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1985 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1986 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1987 SizeMetric::Percent => None,
1988 _ => None,
1989 };
1990
1991 match pixels_opt {
1992 Some(pixels) => pixels,
1993 None => px
1994 .to_percent()
1995 .map(|p| {
1996 resolve_percentage_with_box_model(
1997 containing_block_width,
1998 p.get(),
1999 (box_props.margin.left, box_props.margin.right),
2000 (box_props.border.left, box_props.border.right),
2001 (box_props.padding.left, box_props.padding.right),
2002 )
2003 })
2004 .unwrap_or(0.0),
2005 }
2006 }
2007 _ => 0.0,
2008 };
2009
2010 // Resolve max-width (default is infinity/none)
2011 let max_width = match get_css_max_width(styled_dom, id, node_state) {
2012 MultiValue::Exact(mw) => {
2013 let px = &mw.inner;
2014 // Check if it's the default "max" value (f32::MAX)
2015 if px.number.get() >= core::f32::MAX - 1.0 {
2016 None
2017 } else {
2018 let pixels_opt = match px.metric {
2019 SizeMetric::Px => Some(px.number.get()),
2020 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
2021 SizeMetric::In => Some(px.number.get() * 96.0),
2022 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
2023 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
2024 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
2025 SizeMetric::Percent => None,
2026 _ => None,
2027 };
2028
2029 match pixels_opt {
2030 Some(pixels) => Some(pixels),
2031 None => px.to_percent().map(|p| {
2032 resolve_percentage_with_box_model(
2033 containing_block_width,
2034 p.get(),
2035 (box_props.margin.left, box_props.margin.right),
2036 (box_props.border.left, box_props.border.right),
2037 (box_props.padding.left, box_props.padding.right),
2038 )
2039 }),
2040 }
2041 }
2042 }
2043 _ => None,
2044 };
2045
2046 // Apply constraints: max(min_width, min(tentative, max_width))
2047 // If min > max, min wins per CSS spec
2048 let mut result = tentative_width;
2049
2050 if let Some(max) = max_width {
2051 result = result.min(max);
2052 }
2053
2054 result = result.max(min_width);
2055
2056 result
2057}
2058
2059/// Apply min-height and max-height constraints to tentative height
2060/// Per CSS 2.2 § 10.7: min-height overrides max-height if min > max
2061// +spec:height-calculation:22a77a - percentage min/max-height resolved against containing block; if CB height depends on content and element is not absolutely positioned, percentage treated as 0 (min-height) or none (max-height)
2062// +spec:height-calculation:982aaf - min-height/max-height constrain box heights to a range
2063// +spec:height-calculation:c6c33a - min-height and max-height property resolution and application
2064fn apply_height_constraints(
2065 styled_dom: &StyledDom,
2066 id: NodeId,
2067 node_state: &StyledNodeState,
2068 tentative_height: f32,
2069 containing_block_height: f32,
2070 box_props: &BoxProps,
2071) -> f32 {
2072 use azul_css::props::basic::{
2073 pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
2074 SizeMetric,
2075 };
2076
2077 use crate::solver3::getters::{get_css_max_height, get_css_min_height, MultiValue};
2078
2079 // for backwards-compat with CSS2 display types (block, inline, inline-block, table)
2080 // Resolve min-height (default is 0)
2081 let min_height = match get_css_min_height(styled_dom, id, node_state) {
2082 MultiValue::Exact(mh) => {
2083 let px = &mh.inner;
2084 let pixels_opt = match px.metric {
2085 SizeMetric::Px => Some(px.number.get()),
2086 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
2087 SizeMetric::In => Some(px.number.get() * 96.0),
2088 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
2089 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
2090 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
2091 SizeMetric::Percent => None,
2092 _ => None,
2093 };
2094
2095 match pixels_opt {
2096 Some(pixels) => pixels,
2097 None => px
2098 .to_percent()
2099 .map(|p| {
2100 resolve_percentage_with_box_model(
2101 containing_block_height,
2102 p.get(),
2103 (box_props.margin.top, box_props.margin.bottom),
2104 (box_props.border.top, box_props.border.bottom),
2105 (box_props.padding.top, box_props.padding.bottom),
2106 )
2107 })
2108 .unwrap_or(0.0),
2109 }
2110 }
2111 _ => 0.0,
2112 };
2113
2114 // Resolve max-height (default is infinity/none)
2115 let max_height = match get_css_max_height(styled_dom, id, node_state) {
2116 MultiValue::Exact(mh) => {
2117 let px = &mh.inner;
2118 // Check if it's the default "max" value (f32::MAX)
2119 if px.number.get() >= core::f32::MAX - 1.0 {
2120 None
2121 } else {
2122 let pixels_opt = match px.metric {
2123 SizeMetric::Px => Some(px.number.get()),
2124 SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
2125 SizeMetric::In => Some(px.number.get() * 96.0),
2126 SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
2127 SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
2128 SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
2129 SizeMetric::Percent => None,
2130 _ => None,
2131 };
2132
2133 match pixels_opt {
2134 Some(pixels) => Some(pixels),
2135 None => px.to_percent().map(|p| {
2136 resolve_percentage_with_box_model(
2137 containing_block_height,
2138 p.get(),
2139 (box_props.margin.top, box_props.margin.bottom),
2140 (box_props.border.top, box_props.border.bottom),
2141 (box_props.padding.top, box_props.padding.bottom),
2142 )
2143 }),
2144 }
2145 }
2146 }
2147 _ => None,
2148 };
2149
2150 // +spec:height-calculation:297001 - min/max height constraint algorithm per CSS 2.2 §10.7
2151 // Apply constraints: max(min_height, min(tentative, max_height))
2152 // If min > max, min wins per CSS spec
2153 let mut result = tentative_height;
2154
2155 if let Some(max) = max_height {
2156 result = result.min(max);
2157 }
2158
2159 result = result.max(min_height);
2160
2161 result
2162}
2163
2164pub fn extract_text_from_node(styled_dom: &StyledDom, node_id: NodeId) -> Option<String> {
2165 match &styled_dom.node_data.as_container()[node_id].get_node_type() {
2166 NodeType::Text(text_data) => Some(text_data.as_str().to_string()),
2167 _ => None,
2168 }
2169}