1use crate::debug_log;
4use std::{
5 collections::BTreeSet,
6 sync::Arc,
7};
8
9use azul_core::{
10 dom::{FormattingContext, NodeId, NodeType},
11 geom::LogicalSize,
12 resources::RendererResources,
13 styled_dom::{StyledDom, StyledNodeState},
14};
15use azul_css::{
16 css::CssPropertyValue,
17 props::{
18 basic::PixelValue,
19 layout::{LayoutDisplay, LayoutFlexDirection, LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutPosition, LayoutWidth, LayoutWritingMode},
20 property::{CssProperty, CssPropertyType},
21 },
22 LayoutDebugMessage,
23};
24use rust_fontconfig::FcFontCache;
25
26#[cfg(feature = "text_layout")]
27use crate::text3;
28use crate::{
29 font::parsed::ParsedFont,
30 font_traits::{
31 AvailableSpace, FontLoaderTrait, FontManager, ImageSource, InlineContent, InlineImage,
32 InlineShape, LayoutCache, LayoutFragment, ObjectFit, ParsedFontTrait, ShapeDefinition,
33 StyleProperties, UnifiedConstraints,
34 },
35 solver3::{
36 fc::split_text_for_whitespace,
37 geometry::{BoxProps, IntrinsicSizes, WritingModeContext},
38 getters::{
39 get_css_box_sizing, get_css_height, get_css_width, get_display_property,
40 get_direction_property, get_element_font_size, get_flex_direction, get_float,
41 get_style_properties, get_text_orientation_property, get_writing_mode, MultiValue,
42 },
43 layout_tree::{LayoutNodeHot, LayoutTree, get_display_type},
44 positioning::get_position_type,
45 LayoutContext, LayoutError, Result,
46 },
47};
48
49const FALLBACK_MIN_CONTENT_WIDTH: f32 = 100.0;
50const FALLBACK_MAX_CONTENT_WIDTH: f32 = 300.0;
51const FALLBACK_MIN_CONTENT_HEIGHT: f32 = 20.0;
52const FALLBACK_MAX_CONTENT_HEIGHT: f32 = 20.0;
53
54fn resolve_px_with_box_model(
61 px: &PixelValue,
62 containing: f32,
63 box_props: &BoxProps,
64 is_horizontal: bool,
65 em: f32,
66 rem: f32,
67) -> Option<f32> {
68 if let Some(v) = super::calc::resolve_pixel_value_no_percent(px, em, rem) {
69 return Some(v);
70 }
71
72 let percent = px.to_percent()?;
73 let (margin, border, padding) = if is_horizontal {
74 (
75 (box_props.margin.left, box_props.margin.right),
76 (box_props.border.left, box_props.border.right),
77 (box_props.padding.left, box_props.padding.right),
78 )
79 } else {
80 (
81 (box_props.margin.top, box_props.margin.bottom),
82 (box_props.border.top, box_props.border.bottom),
83 (box_props.padding.top, box_props.padding.bottom),
84 )
85 };
86 Some(resolve_percentage_with_box_model(
87 containing,
88 percent.get(),
89 margin,
90 border,
91 padding,
92 ))
93}
94
95#[must_use] pub fn resolve_percentage_with_box_model(
109 containing_block_dimension: f32,
110 percentage: f32,
111 _margins: (f32, f32),
112 _borders: (f32, f32),
113 _paddings: (f32, f32),
114) -> f32 {
115 (containing_block_dimension * percentage).max(0.0)
119}
120
121fn subtree_contains_text(styled_dom: &StyledDom, dom_id: NodeId) -> bool {
127 let node_hierarchy = styled_dom.node_hierarchy.as_container();
128 let node_data = styled_dom.node_data.as_container();
129 if matches!(node_data[dom_id].get_node_type(), NodeType::Text(_)) {
130 return true;
131 }
132 dom_id
133 .az_children(&node_hierarchy)
134 .any(|child| subtree_contains_text(styled_dom, child))
135}
136
137#[inline(never)]
145#[allow(clippy::cast_possible_truncation)] pub fn calculate_intrinsic_sizes<T: ParsedFontTrait>(
150 ctx: &mut LayoutContext<'_, T>,
151 tree: &mut LayoutTree,
152 text_cache: &mut LayoutCache,
153 dirty_nodes: &BTreeSet<usize>,
154) -> Result<()> {
155 unsafe { crate::az_mark(0x607B0_u32, (tree.nodes.len() as u32)); }
162 if dirty_nodes.is_empty() {
163 return Ok(());
164 }
165
166 debug_log!(ctx, "Starting intrinsic size calculation");
167 let dirty_closure = compute_dirty_ancestor_closure(tree, dirty_nodes);
175 unsafe { crate::az_mark(0x607B4_u32, (tree.nodes.len() as u32)); }
177
178 let mut calculator = IntrinsicSizeCalculator::new(ctx, text_cache);
179 calculator.dirty_closure = Some(dirty_closure);
180 unsafe {
198 crate::az_mark(0x60730_u32, (tree.root as u32));
199 crate::az_mark(0x60734_u32, (tree.nodes.len() as u32));
200 crate::az_mark(0x60738_u32, u32::from(tree.get(tree.root).is_some()));
201 crate::az_mark(0x6075C_u32, ((std::ptr::from_ref::<LayoutTree>(tree) as usize) as u32));
204 }
205 calculator.calculate_intrinsic_recursive(tree, tree.root, false)?;
206 debug_log!(ctx, "Finished intrinsic size calculation");
207 Ok(())
208}
209
210fn compute_dirty_ancestor_closure(
211 tree: &LayoutTree,
212 dirty_nodes: &BTreeSet<usize>,
213) -> std::collections::HashSet<usize> {
214 let mut closure: std::collections::HashSet<usize> = std::collections::HashSet::new();
215 for &dirty in dirty_nodes {
216 let mut cur = Some(dirty);
217 while let Some(idx) = cur {
218 if !closure.insert(idx) {
219 break;
220 }
221 cur = tree.get(idx).and_then(|n| n.parent);
222 }
223 }
224 closure
225}
226
227struct IntrinsicSizeCalculator<'a, 'b, 'c, T: ParsedFontTrait> {
228 ctx: &'a mut LayoutContext<'b, T>,
229 text_cache: &'c mut LayoutCache,
237 dirty_closure: Option<std::collections::HashSet<usize>>,
242}
243
244impl<'a, 'b, 'c, T: ParsedFontTrait> IntrinsicSizeCalculator<'a, 'b, 'c, T> {
245 const fn new(ctx: &'a mut LayoutContext<'b, T>, text_cache: &'c mut LayoutCache) -> Self {
246 Self {
247 ctx,
248 text_cache,
249 dirty_closure: None,
250 }
251 }
252
253 #[allow(clippy::cast_possible_truncation)] fn calculate_intrinsic_recursive(
255 &mut self,
256 tree: &mut LayoutTree,
257 node_index: usize,
258 ancestor_is_stf: bool,
259 ) -> Result<IntrinsicSizes> {
260 unsafe { crate::az_mark(0x60720_u32, (node_index as u32)); }
263 if let Some(closure) = self.dirty_closure.as_ref() {
269 if !closure.contains(&node_index) {
270 if let Some(cached) = tree
271 .warm(node_index)
272 .and_then(|w| w.intrinsic_sizes)
273 {
274 return Ok(cached);
275 }
276 }
277 }
278
279 if !ancestor_is_stf
286 && tree
287 .subtree_needs_intrinsic
288 .get(node_index)
289 .copied()
290 .is_some_and(|v| !v)
291 {
292 let default = IntrinsicSizes::default();
293 if let Some(n) = tree.warm_mut(node_index) {
294 n.intrinsic_sizes = Some(default);
295 }
296 return Ok(default);
297 }
298
299 let dom_node_id = tree
305 .get(node_index)
306 .ok_or(LayoutError::InvalidTree)?
307 .dom_node_id;
308
309 let is_out_of_flow = matches!(
317 get_position_type(self.ctx.styled_dom, dom_node_id),
318 LayoutPosition::Absolute | LayoutPosition::Fixed
319 );
320
321 let children_slice = tree.children(node_index);
324 let n = children_slice.len();
325 let mut stack_buf = [0usize; 32];
326 let heap_buf: Vec<usize>;
327 let children: &[usize] = if n <= 32 {
328 stack_buf[..n].copy_from_slice(children_slice);
329 &stack_buf[..n]
330 } else {
331 heap_buf = children_slice.to_vec();
332 &heap_buf
333 };
334 let self_is_stf = tree
337 .get(node_index)
338 .is_some_and(|n| {
339 crate::solver3::layout_tree::is_shrink_to_fit_context(
340 self.ctx.styled_dom,
341 n.dom_node_id,
342 n.formatting_context,
343 )
344 });
345 let child_ancestor_is_stf = ancestor_is_stf || self_is_stf;
346
347 let mut child_intrinsics = Vec::with_capacity(n);
348 for &child_index in children {
349 unsafe { crate::az_mark(0x60728_u32, (child_index as u32)); }
351 if tree.get(child_index).is_none() {
358 continue;
359 }
360 let child_intrinsic =
361 self.calculate_intrinsic_recursive(tree, child_index, child_ancestor_is_stf)?;
362 child_intrinsics.push((child_index, child_intrinsic));
363 }
364
365 let mut intrinsic = self.calculate_node_intrinsic_sizes(tree, node_index, &child_intrinsics)?;
367
368 if let Some(dom_id) = tree.get(node_index).and_then(|n| n.dom_node_id) {
370 use crate::solver3::getters::{get_css_min_width, get_css_min_height, MultiValue};
371
372 let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
373
374 let em = get_element_font_size(self.ctx.styled_dom, dom_id, node_state);
376 let rem = super::getters::get_root_font_size(self.ctx.styled_dom, node_state);
377
378 if let MultiValue::Exact(mw) = get_css_min_width(self.ctx.styled_dom, dom_id, node_state) {
379 if let Some(min_w) = super::calc::resolve_pixel_value_no_percent(&mw.inner, em, rem) {
380 intrinsic.min_content_width = intrinsic.min_content_width.max(min_w);
381 intrinsic.max_content_width = intrinsic.max_content_width.max(min_w);
382 }
383 }
384
385 if let MultiValue::Exact(mh) = get_css_min_height(self.ctx.styled_dom, dom_id, node_state) {
386 if let Some(min_h) = super::calc::resolve_pixel_value_no_percent(&mh.inner, em, rem) {
387 intrinsic.min_content_height = intrinsic.min_content_height.max(min_h);
388 intrinsic.max_content_height = intrinsic.max_content_height.max(min_h);
389 }
390 }
391 }
392
393 if let Some(n) = tree.warm_mut(node_index) {
394 n.intrinsic_sizes = Some(intrinsic);
395 }
396
397 if is_out_of_flow {
401 Ok(IntrinsicSizes::default())
402 } else {
403 Ok(intrinsic)
404 }
405 }
406
407 #[allow(clippy::too_many_lines)] fn calculate_node_intrinsic_sizes(
409 &mut self,
410 tree: &LayoutTree,
411 node_index: usize,
412 child_intrinsics: &[(usize, IntrinsicSizes)],
413 ) -> Result<IntrinsicSizes> {
414 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
415
416 if let Some(dom_id) = node.dom_node_id {
423 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
424 if node_data.is_virtual_view_node() {
425 return Ok(IntrinsicSizes {
426 min_content_width: 300.0,
427 max_content_width: 300.0,
428 preferred_width: None, min_content_height: 150.0,
430 max_content_height: 150.0,
431 preferred_height: None, preferred_aspect_ratio: None,
433 });
434 }
435
436 if let NodeType::Image(image_ref) = node_data.get_node_type() {
441 let size = image_ref.get_size();
442 let has_intrinsic = size.width > 0.0 || size.height > 0.0;
446 let (width, height) = if size.width > 0.0 && size.height > 0.0 {
447 (size.width, size.height)
448 } else if size.width > 0.0 {
449 (size.width, size.width / 2.0)
450 } else if size.height > 0.0 {
451 (self.ctx.viewport_size.width, size.height)
453 } else {
454 let w = self.ctx.viewport_size.width.min(300.0);
459 (w, w / 2.0)
460 };
461 let (pref_w, pref_h) = if has_intrinsic {
471 (Some(width), Some(height))
472 } else {
473 (None, None)
474 };
475 return Ok(IntrinsicSizes {
476 min_content_width: width,
477 max_content_width: width,
478 preferred_width: pref_w,
479 min_content_height: height,
480 max_content_height: height,
481 preferred_height: pref_h,
482 preferred_aspect_ratio: None,
483 });
484 }
485 }
486
487 match node.formatting_context {
488 FormattingContext::Block { .. } => {
489 let has_block_child = tree.children(node_index).iter().any(|&child_idx| {
499 tree.get(child_idx)
500 .and_then(|c| c.dom_node_id)
501 .is_some_and(|dom_id| {
502 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
503 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
505 return false;
506 }
507 let display = get_display_type(self.ctx.styled_dom, dom_id);
508 display.creates_block_context()
509 })
510 });
511
512 let has_inline_child = tree.children(node_index).iter().any(|&child_idx| {
513 tree.get(child_idx)
514 .and_then(|c| c.dom_node_id)
515 .is_some_and(|dom_id| {
516 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
517 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
518 return true;
519 }
520 let display = get_display_type(self.ctx.styled_dom, dom_id);
521 matches!(display,
522 LayoutDisplay::Inline
523 | LayoutDisplay::InlineBlock
524 | LayoutDisplay::InlineFlex
525 | LayoutDisplay::InlineGrid
526 | LayoutDisplay::InlineTable
527 )
528 })
529 });
530
531 let is_ifc_root = has_inline_child && !has_block_child;
534
535 let has_direct_text = if has_block_child {
538 false
539 } else if let Some(dom_id) = node.dom_node_id {
540 let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
541 dom_id.az_children(node_hierarchy).any(|child_id| {
542 let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
543 matches!(child_node_data.get_node_type(), NodeType::Text(_))
544 })
545 } else {
546 false
547 };
548
549 if is_ifc_root || has_direct_text {
550 self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
552 } else {
553 self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
555 }
556 }
557 FormattingContext::Inline => {
558 let is_text_node = if let Some(dom_id) = node.dom_node_id {
576 let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
577 matches!(node_data.get_node_type(), NodeType::Text(_))
578 } else {
579 false
580 };
581
582 let has_text_in_subtree = if let Some(dom_id) = node.dom_node_id {
583 subtree_contains_text(self.ctx.styled_dom, dom_id)
584 } else {
585 false
586 };
587
588 if is_text_node || has_text_in_subtree {
589 self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
591 } else {
592 Ok(IntrinsicSizes::default())
594 }
595 }
596 FormattingContext::InlineBlock => {
597 let has_inline_children = tree.children(node_index).iter().any(|&child_idx| {
601 tree.get(child_idx)
602 .is_some_and(|c| matches!(c.formatting_context, FormattingContext::Inline))
603 });
604
605 let has_direct_text = if let Some(dom_id) = node.dom_node_id {
606 let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
607 dom_id.az_children(node_hierarchy).any(|child_id| {
608 let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
609 matches!(child_node_data.get_node_type(), NodeType::Text(_))
610 })
611 } else {
612 false
613 };
614
615 if has_inline_children || has_direct_text {
616 let intrinsic = self.calculate_ifc_root_intrinsic_sizes(tree, node_index)?;
621
622 Ok(intrinsic)
623 } else {
624 self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
626 }
627 }
628 FormattingContext::Table => {
629 Ok(self.calculate_table_intrinsic_sizes(tree, node_index, child_intrinsics))
630 }
631 FormattingContext::Flex => {
632 self.calculate_flex_intrinsic_sizes(tree, node_index, child_intrinsics)
633 }
634 _ => self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics),
635 }
636 }
637
638 #[allow(clippy::cast_possible_truncation)] fn calculate_ifc_root_intrinsic_sizes(
644 &mut self,
645 tree: &LayoutTree,
646 node_index: usize,
647 ) -> Result<IntrinsicSizes> {
648 unsafe {
650 let c = crate::az_mark_read(0x60758).wrapping_add(1);
651 crate::az_mark(0x60758_u32, (c));
652 crate::az_mark(0x6075C_u32, (node_index as u32));
653 }
654 let collect_result = collect_inline_content(self.ctx, tree, node_index);
671 #[cfg(feature = "web_lift")]
672 unsafe { crate::az_mark((0x60760) as u32, (if collect_result.is_ok() { 0x00000001u32 } else { 0x000000EEu32 }) as u32); }
673 let inline_content: Vec<InlineContent> = collect_result?;
674
675 if inline_content.is_empty() {
676 return Ok(IntrinsicSizes::default());
677 }
678
679 let loaded_fonts = self.ctx.font_manager.get_loaded_fonts();
681
682 let mut constraints = UnifiedConstraints::default();
698 if let Some(dom_id) = tree.get(node_index).and_then(|n| n.dom_node_id) {
699 use crate::solver3::getters::{get_white_space_property, MultiValue};
700 use azul_css::props::style::text::StyleWhiteSpace;
701 let node_state =
702 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
703 let ws = match get_white_space_property(self.ctx.styled_dom, dom_id, node_state) {
704 MultiValue::Exact(v) => v,
705 _ => StyleWhiteSpace::Normal,
706 };
707 constraints.white_space_mode = match ws {
708 StyleWhiteSpace::Normal => crate::text3::cache::WhiteSpaceMode::Normal,
709 StyleWhiteSpace::Nowrap => crate::text3::cache::WhiteSpaceMode::Nowrap,
710 StyleWhiteSpace::Pre => crate::text3::cache::WhiteSpaceMode::Pre,
711 StyleWhiteSpace::PreWrap => crate::text3::cache::WhiteSpaceMode::PreWrap,
712 StyleWhiteSpace::PreLine => crate::text3::cache::WhiteSpaceMode::PreLine,
713 StyleWhiteSpace::BreakSpaces => crate::text3::cache::WhiteSpaceMode::BreakSpaces,
714 };
715 }
716 #[cfg(feature = "web_lift")]
727 {
728 let cl = self.ctx.font_manager.font_chain_cache.len();
729 unsafe {
730 crate::az_mark((0x60768) as u32, (cl as u32) as u32);
731 crate::az_mark((0x6076C) as u32, (loaded_fonts.len() as u32) as u32);
732 crate::az_mark((0x60704) as u32, (0xA15u32) as u32);
733 }
734 let _ = (cl, loaded_fonts.len());
742 }
743 let Ok(intrinsic_text) = self.text_cache.measure_intrinsic_widths(
744 &inline_content,
745 &[],
746 &constraints,
747 &self.ctx.font_manager.font_chain_cache,
748 &self.ctx.font_manager.fc_cache,
749 &loaded_fonts,
750 self.ctx.debug_messages,
751 ) else {
752 return Ok(IntrinsicSizes {
753 min_content_width: FALLBACK_MIN_CONTENT_WIDTH,
754 max_content_width: FALLBACK_MAX_CONTENT_WIDTH,
755 preferred_width: None,
756 min_content_height: FALLBACK_MIN_CONTENT_HEIGHT,
757 max_content_height: FALLBACK_MAX_CONTENT_HEIGHT,
758 preferred_height: None,
759 preferred_aspect_ratio: None,
760 });
761 };
762
763 let min_width = intrinsic_text.min_content_width;
764 let max_width = intrinsic_text.max_content_width;
765
766 let max_content_height = intrinsic_text.max_content_height;
771
772 Ok(IntrinsicSizes {
777 min_content_width: min_width,
778 max_content_width: max_width,
779 preferred_width: None,
780 min_content_height: max_content_height,
781 max_content_height,
782 preferred_height: None,
783 preferred_aspect_ratio: None,
784 })
785 }
786
787 fn calculate_block_intrinsic_sizes(
792 &self,
793 tree: &LayoutTree,
794 node_index: usize,
795 child_intrinsics: &[(usize, IntrinsicSizes)],
796 ) -> Result<IntrinsicSizes> {
797 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
798 let writing_mode = node.dom_node_id.map_or_else(LayoutWritingMode::default, |dom_id| {
799 let node_state =
800 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
801 get_writing_mode(self.ctx.styled_dom, dom_id, node_state).unwrap_or_default()
802 });
803
804 let mut max_child_min_cross = 0.0f32;
810 let mut max_child_max_cross = 0.0f32;
811 let mut total_main_size = 0.0;
812 let mut last_margin_main_end = 0.0f32;
816 let mut is_first_child = true;
817
818 for &child_index in tree.children(node_index) {
819 if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
820 let child_node = tree.get(child_index);
822 let (cross_extras, main_border_padding, main_margin_start, main_margin_end) =
823 child_node.map_or((0.0, 0.0, 0.0, 0.0), |cn| {
824 let bp = cn.box_props.unpack();
825 let h = bp.margin.left + bp.margin.right
826 + bp.border.left + bp.border.right
827 + bp.padding.left + bp.padding.right;
828 let v_bp = bp.border.top + bp.border.bottom
829 + bp.padding.top + bp.padding.bottom;
830 match writing_mode {
831 LayoutWritingMode::HorizontalTb => (h, v_bp, bp.margin.top, bp.margin.bottom),
832 _ => (v_bp, h, bp.margin.left, bp.margin.right),
833 }
834 });
835
836 let (child_min_cross, child_max_cross, child_border_box_main) = match writing_mode {
837 LayoutWritingMode::HorizontalTb => (
838 child_intrinsic.min_content_width + cross_extras,
839 child_intrinsic.max_content_width + cross_extras,
840 child_intrinsic.max_content_height + main_border_padding,
841 ),
842 _ => (
843 child_intrinsic.min_content_height + cross_extras,
844 child_intrinsic.max_content_height + cross_extras,
845 child_intrinsic.max_content_width + main_border_padding,
846 ),
847 };
848
849 max_child_min_cross = max_child_min_cross.max(child_min_cross);
850 max_child_max_cross = max_child_max_cross.max(child_max_cross);
851
852 if is_first_child {
857 is_first_child = false;
858 } else {
860 let collapsed_gap = crate::solver3::fc::collapse_margins(
862 last_margin_main_end, main_margin_start
863 );
864 total_main_size += collapsed_gap;
865 }
866
867 total_main_size += child_border_box_main;
868 last_margin_main_end = main_margin_end;
869 }
870 }
871 let (min_width, max_width, min_height, max_height) = match writing_mode {
874 LayoutWritingMode::HorizontalTb => (
875 max_child_min_cross,
876 max_child_max_cross,
877 total_main_size,
878 total_main_size,
879 ),
880 _ => (
881 total_main_size,
882 total_main_size,
883 max_child_min_cross,
884 max_child_max_cross,
885 ),
886 };
887
888 Ok(IntrinsicSizes {
889 min_content_width: min_width,
890 max_content_width: max_width,
891 preferred_width: None,
892 min_content_height: min_height,
893 max_content_height: max_height,
894 preferred_height: None,
895 preferred_aspect_ratio: None,
896 })
897 }
898
899 fn calculate_flex_intrinsic_sizes(
904 &self,
905 tree: &LayoutTree,
906 node_index: usize,
907 child_intrinsics: &[(usize, IntrinsicSizes)],
908 ) -> Result<IntrinsicSizes> {
909 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
910
911 let is_row = node.dom_node_id.is_none_or(|dom_id| {
913 let node_state =
914 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
915 match get_flex_direction(self.ctx.styled_dom, dom_id, node_state) {
916 MultiValue::Exact(dir) => matches!(dir, LayoutFlexDirection::Row | LayoutFlexDirection::RowReverse),
917 _ => true, }
919 });
920
921 let mut sum_main_min: f32 = 0.0;
922 let mut sum_main_max: f32 = 0.0;
923 let mut max_main_min: f32 = 0.0;
924 let mut max_cross_min: f32 = 0.0;
925 let mut max_cross_max: f32 = 0.0;
926
927 for &child_index in tree.children(node_index) {
928 if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
929 let (child_main_min, child_main_max, child_cross_min, child_cross_max) = if is_row {
930 (
931 child_intrinsic.min_content_width,
932 child_intrinsic.max_content_width,
933 child_intrinsic.min_content_height,
934 child_intrinsic.max_content_height,
935 )
936 } else {
937 (
938 child_intrinsic.min_content_height,
939 child_intrinsic.max_content_height,
940 child_intrinsic.min_content_width,
941 child_intrinsic.max_content_width,
942 )
943 };
944
945 sum_main_max += child_main_max;
946 sum_main_min += child_main_min;
947 max_main_min = max_main_min.max(child_main_min);
949
950 max_cross_min = max_cross_min.max(child_cross_min);
952 max_cross_max = max_cross_max.max(child_cross_max);
953 }
954 }
955
956 let is_single_line = node.dom_node_id.is_none_or(|dom_id| {
959 let node_state =
960 &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
961 let wrap_prop = crate::solver3::getters::get_flex_wrap_prop(
962 self.ctx.styled_dom, dom_id, node_state,
963 );
964 wrap_prop.is_none_or(|val| matches!(
965 val.get_property_or_default().unwrap_or_default(),
966 LayoutFlexWrap::NoWrap
967 ))
968 });
969
970 let min_main = if is_single_line { sum_main_min } else { max_main_min };
971 let max_main = sum_main_max;
972
973 if is_row {
974 Ok(IntrinsicSizes {
975 min_content_width: min_main,
976 max_content_width: max_main,
977 preferred_width: None,
978 min_content_height: max_cross_min,
979 max_content_height: max_cross_max,
980 preferred_height: None,
981 preferred_aspect_ratio: None,
982 })
983 } else {
984 Ok(IntrinsicSizes {
985 min_content_width: max_cross_min,
986 max_content_width: max_cross_max,
987 preferred_width: None,
988 min_content_height: min_main,
989 max_content_height: max_main,
990 preferred_height: None,
991 preferred_aspect_ratio: None,
992 })
993 }
994 }
995
996 fn calculate_table_intrinsic_sizes(
1000 &mut self,
1001 tree: &LayoutTree,
1002 node_index: usize,
1003 child_intrinsics: &[(usize, IntrinsicSizes)],
1004 ) -> IntrinsicSizes {
1005 let mut col_min: Vec<f32> = Vec::new();
1008 let mut col_max: Vec<f32> = Vec::new();
1009 let mut total_height = 0.0f32;
1010
1011 let mut rows: Vec<usize> = Vec::new();
1013 for &child_idx in tree.children(node_index) {
1014 let Some(child) = tree.get(child_idx) else { continue };
1015 match child.formatting_context {
1016 FormattingContext::TableRow => rows.push(child_idx),
1017 FormattingContext::TableRowGroup => {
1018 for &row_idx in tree.children(child_idx) {
1020 if let Some(row) = tree.get(row_idx) {
1021 if matches!(row.formatting_context, FormattingContext::TableRow) {
1022 rows.push(row_idx);
1023 }
1024 }
1025 }
1026 }
1027 _ => {}
1028 }
1029 }
1030
1031 for &row_idx in &rows {
1032 let mut row_height = 0.0f32;
1033 for (col, &cell_idx) in tree.children(row_idx).iter().enumerate() {
1034 let cell_intrinsic = child_intrinsics.iter().find(|(k, _)| k == &cell_idx).map(|(_, v)| *v)
1035 .unwrap_or_default();
1036 let cell_is = if cell_intrinsic.max_content_width > 0.0 {
1038 cell_intrinsic
1039 } else {
1040 self.calculate_ifc_root_intrinsic_sizes(tree, cell_idx)
1042 .unwrap_or_default()
1043 };
1044
1045 let cell_node = tree.get(cell_idx);
1047 let (h_extras, v_extras) = cell_node.map_or((0.0, 0.0), |cn| {
1048 let bp = cn.box_props.unpack();
1049 (bp.padding.left + bp.padding.right + bp.border.left + bp.border.right,
1050 bp.padding.top + bp.padding.bottom + bp.border.top + bp.border.bottom)
1051 });
1052
1053 let cell_min = cell_is.min_content_width + h_extras;
1054 let cell_max = cell_is.max_content_width + h_extras;
1055 let cell_h = cell_is.max_content_height + v_extras;
1056
1057 if col >= col_min.len() {
1058 col_min.push(cell_min);
1059 col_max.push(cell_max);
1060 } else {
1061 col_min[col] = col_min[col].max(cell_min);
1062 col_max[col] = col_max[col].max(cell_max);
1063 }
1064 row_height = row_height.max(cell_h);
1065 }
1066 total_height += row_height;
1067 }
1068
1069 let min_width: f32 = col_min.iter().sum();
1070 let max_width: f32 = col_max.iter().sum();
1071
1072 IntrinsicSizes {
1073 min_content_width: min_width,
1074 max_content_width: max_width,
1075 min_content_height: total_height,
1076 max_content_height: total_height,
1077 preferred_width: None,
1078 preferred_height: None,
1079 preferred_aspect_ratio: None,
1080 }
1081 }
1082}
1083
1084fn collect_inline_content_for_sizing<T: ParsedFontTrait>(
1100 ctx: &mut LayoutContext<'_, T>,
1101 tree: &LayoutTree,
1102 ifc_root_index: usize,
1103 out: &mut Vec<InlineContent>,
1104) -> Result<()> {
1105 debug_log!(ctx, "Collecting inline content from node {} for intrinsic sizing", ifc_root_index);
1106
1107 collect_inline_content_recursive(ctx, tree, ifc_root_index, out)?;
1110 unsafe { crate::az_mark(0x6071C_u32, (0xB8u32)); }
1112 debug_log!(ctx, "Collected {} inline content items from node {}", out.len(), ifc_root_index);
1113
1114 Ok(())
1115}
1116
1117#[allow(clippy::cast_possible_truncation)] fn collect_inline_content_recursive<T: ParsedFontTrait>(
1129 ctx: &mut LayoutContext<'_, T>,
1130 tree: &LayoutTree,
1131 node_index: usize,
1132 content: &mut Vec<InlineContent>,
1133) -> Result<()> {
1134 unsafe { crate::az_mark(0x60754_u32, (node_index as u32)); }
1139 let Some(node) = tree.get(node_index) else {
1140 unsafe { crate::az_mark(0x6071C_u32, (0xBADu32)); }
1141 return Err(LayoutError::InvalidTree);
1142 };
1143
1144 let Some(dom_id) = node.dom_node_id else {
1147 return process_layout_children(ctx, tree, node_index, content);
1149 };
1150
1151 if let Some(text) = extract_text_from_node(ctx.styled_dom, dom_id) {
1153 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)));
1154 debug_log!(ctx, "Found text in node {}: '{}'", node_index, text);
1155 let text_items = split_text_for_whitespace(
1157 ctx.styled_dom,
1158 dom_id,
1159 &text,
1160 &style_props,
1161 );
1162 content.extend(text_items);
1163 }
1164
1165 let node_hierarchy = &ctx.styled_dom.node_hierarchy.as_container();
1172 for child_id in dom_id.az_children(node_hierarchy) {
1173 if tree.dom_to_layout.contains_key(&child_id) {
1176 continue;
1177 }
1178 let child_dom_node = &ctx.styled_dom.node_data.as_container()[child_id];
1180 if let NodeType::Text(text_data) = child_dom_node.get_node_type() {
1181 let text = text_data.as_str().to_string();
1182 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)));
1183 debug_log!(ctx, "Found text in DOM child of node {}: '{}'", node_index, text);
1184 let text_items = split_text_for_whitespace(
1186 ctx.styled_dom,
1187 child_id,
1188 &text,
1189 &style_props,
1190 );
1191 content.extend(text_items);
1192 }
1193 }
1194 unsafe { crate::az_mark(0x6071C_u32, (0xB6u32)); }
1196
1197 process_layout_children(ctx, tree, node_index, content)
1198}
1199
1200#[allow(clippy::cast_possible_truncation)] #[allow(clippy::match_same_arms)] fn process_layout_children<T: ParsedFontTrait>(
1204 ctx: &mut LayoutContext<'_, T>,
1205 tree: &LayoutTree,
1206 node_index: usize,
1207 content: &mut Vec<InlineContent>,
1208) -> Result<()> {
1209 use azul_css::props::layout::{LayoutHeight, LayoutWidth};
1210
1211 unsafe { crate::az_mark(0x60708_u32, (0xC000_0000_u32 | (node_index as u32 & 0x00FF_FFFF))); }
1213 for &child_index in tree.children(node_index) {
1215 unsafe { crate::az_mark(0x6070C_u32, (child_index as u32)); }
1217 let Some(child_node) = tree.get(child_index) else { continue; };
1224 let Some(child_dom_id) = child_node.dom_node_id else {
1225 continue;
1226 };
1227
1228 let display = get_display_property(ctx.styled_dom, Some(child_dom_id));
1229
1230 if display.unwrap_or_default() == LayoutDisplay::Inline {
1232 debug_log!(ctx, "Recursing into inline child at node {}", child_index);
1235 collect_inline_content_recursive(ctx, tree, child_index, content)?;
1236 } else {
1237 let intrinsic_sizes = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
1241
1242 let node_state =
1245 &ctx.styled_dom.styled_nodes.as_container()[child_dom_id].styled_node_state;
1246 let css_width = get_css_width(ctx.styled_dom, child_dom_id, node_state);
1247 let css_height = get_css_height(ctx.styled_dom, child_dom_id, node_state);
1248
1249 let used_width = match css_width {
1251 MultiValue::Exact(LayoutWidth::Px(px)) => {
1252 let em = get_element_font_size(ctx.styled_dom, child_dom_id, node_state);
1261 let rem = super::getters::get_root_font_size(ctx.styled_dom, node_state);
1262 super::calc::resolve_pixel_value_no_percent(&px, em, rem)
1263 .unwrap_or(intrinsic_sizes.max_content_width)
1264 }
1265 MultiValue::Exact(LayoutWidth::MinContent) => intrinsic_sizes.min_content_width,
1266 MultiValue::Exact(LayoutWidth::MaxContent) => intrinsic_sizes.max_content_width,
1267 MultiValue::Exact(LayoutWidth::FitContent(_)) => {
1268 intrinsic_sizes.max_content_width
1270 }
1271 _ => intrinsic_sizes.max_content_width,
1273 };
1274
1275 let used_height = match css_height {
1278 MultiValue::Exact(LayoutHeight::Px(px)) => {
1279 let em = get_element_font_size(ctx.styled_dom, child_dom_id, node_state);
1283 let rem = super::getters::get_root_font_size(ctx.styled_dom, node_state);
1284 super::calc::resolve_pixel_value_no_percent(&px, em, rem)
1285 .unwrap_or(intrinsic_sizes.max_content_height)
1286 }
1287 MultiValue::Exact(LayoutHeight::MinContent) => intrinsic_sizes.max_content_height,
1289 MultiValue::Exact(LayoutHeight::MaxContent) => intrinsic_sizes.max_content_height,
1291 MultiValue::Exact(LayoutHeight::FitContent(_)) => intrinsic_sizes.max_content_height,
1292 _ => intrinsic_sizes.max_content_height,
1293 };
1294
1295 debug_log!(ctx, "Found atomic inline child at node {}: display={:?}, intrinsic_width={}, used_width={}, css_width={:?}",
1296 child_index, display, intrinsic_sizes.max_content_width, used_width, css_width);
1297
1298 content.push(InlineContent::Shape(InlineShape {
1300 shape_def: ShapeDefinition::Rectangle {
1301 size: crate::text3::cache::Size {
1302 width: used_width,
1303 height: used_height,
1304 },
1305 corner_radius: None,
1306 },
1307 fill: None,
1308 stroke: None,
1309 baseline_offset: used_height,
1310 alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
1311 source_node_id: Some(child_dom_id),
1312 }));
1313 }
1314 }
1315
1316 Ok(())
1317}
1318
1319pub fn collect_inline_content<T: ParsedFontTrait>(
1324 ctx: &mut LayoutContext<'_, T>,
1325 tree: &LayoutTree,
1326 ifc_root_index: usize,
1327) -> Result<Vec<InlineContent>> {
1328 let mut out = Vec::new();
1329 collect_inline_content_for_sizing(ctx, tree, ifc_root_index, &mut out)?;
1330 Ok(out)
1331}
1332
1333#[inline(never)]
1360#[allow(clippy::trivially_copy_pass_by_ref)] fn auto_block_inline_size(cb: &LogicalSize, bp: &BoxProps) -> f32 {
1362 let aw = cb.width
1363 - bp.margin.left
1364 - bp.margin.right
1365 - bp.border.left
1366 - bp.border.right
1367 - bp.padding.left
1368 - bp.padding.right;
1369 aw.max(0.0)
1370}
1371
1372#[allow(clippy::match_same_arms)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn calculate_used_size_for_node(
1378 styled_dom: &StyledDom,
1379 dom_id: Option<NodeId>,
1380 containing_block_size: &LogicalSize,
1386 intrinsic: IntrinsicSizes,
1387 box_props: &BoxProps,
1388 viewport_size: &LogicalSize,
1389) -> Result<LogicalSize> {
1390 let Some(id) = dom_id else {
1391 return Ok(LogicalSize::new(
1401 containing_block_size.width,
1402 if intrinsic.max_content_height > 0.0 {
1403 intrinsic.max_content_height
1404 } else {
1405 0.0
1407 },
1408 ));
1409 };
1410
1411 let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1412 let css_width = get_css_width(styled_dom, id, node_state);
1413 let css_height = get_css_height(styled_dom, id, node_state);
1414 let writing_mode = get_writing_mode(styled_dom, id, node_state);
1415 let display = get_display_property(styled_dom, Some(id));
1416 let position = get_position_type(styled_dom, dom_id);
1417
1418 let wm_ctx = WritingModeContext::new(
1421 writing_mode.unwrap_or_default(),
1422 get_direction_property(styled_dom, id, node_state).unwrap_or_default(),
1423 get_text_orientation_property(styled_dom, id, node_state).unwrap_or_default(),
1424 );
1425 let is_vertical = !wm_ctx.is_horizontal();
1426
1427 let node_data = &styled_dom.node_data.as_container()[id];
1430 let is_replaced = matches!(node_data.get_node_type(), NodeType::Image(_))
1431 || node_data.is_virtual_view_node();
1432
1433 let css_width = if display.unwrap_or_default() == LayoutDisplay::Inline
1437 && !is_replaced
1438 {
1439 MultiValue::Exact(LayoutWidth::Auto)
1440 } else {
1441 css_width
1442 };
1443
1444 let css_height = if display.unwrap_or_default() == LayoutDisplay::Inline
1450 && !is_replaced
1451 {
1452 MultiValue::Exact(LayoutHeight::Auto)
1453 } else {
1454 css_height
1455 };
1456
1457 let width_is_auto = css_width.is_auto() || matches!(&css_width, MultiValue::Exact(LayoutWidth::Auto));
1459 let height_is_auto = css_height.is_auto() || matches!(&css_height, MultiValue::Exact(LayoutHeight::Auto));
1460
1461 let width_is_quantitative = matches!(
1463 &css_width,
1464 MultiValue::Exact(LayoutWidth::Px(_) | LayoutWidth::FitContent(_) | LayoutWidth::Calc(_))
1465 );
1466 let height_is_quantitative = matches!(
1467 &css_height,
1468 MultiValue::Exact(LayoutHeight::Px(_) | LayoutHeight::FitContent(_) | LayoutHeight::Calc(_))
1469 );
1470
1471 let resolved_width = match css_width.unwrap_or_default() {
1481 LayoutWidth::Auto => {
1482 if is_replaced {
1490 intrinsic.max_content_width
1496 }
1497 else if get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None) != LayoutFloat::None {
1499 let available_width = (containing_block_size.width
1507 - box_props.margin.left
1508 - box_props.margin.right
1509 - box_props.border.left
1510 - box_props.border.right
1511 - box_props.padding.left
1512 - box_props.padding.right)
1513 .max(0.0);
1514 let preferred_minimum = intrinsic.min_content_width;
1515 let preferred = intrinsic.max_content_width;
1516 preferred_minimum.max(available_width).min(preferred).max(0.0)
1517 }
1518 else if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
1519 let available_width = (containing_block_size.width
1526 - box_props.margin.left
1527 - box_props.margin.right
1528 - box_props.border.left
1529 - box_props.border.right
1530 - box_props.padding.left
1531 - box_props.padding.right)
1532 .max(0.0);
1533 let preferred_minimum = intrinsic.min_content_width;
1534 let preferred = intrinsic.max_content_width;
1535 preferred_minimum.max(available_width).min(preferred).max(0.0)
1536 } else {
1537 match display.unwrap_or_default() {
1544 LayoutDisplay::Block
1545 | LayoutDisplay::FlowRoot
1546 | LayoutDisplay::ListItem
1547 | LayoutDisplay::Flex
1548 | LayoutDisplay::Grid => {
1549 auto_block_inline_size(containing_block_size, box_props)
1572 }
1573 LayoutDisplay::InlineBlock | LayoutDisplay::InlineGrid | LayoutDisplay::InlineFlex => {
1574 let available_width = (containing_block_size.width
1577 - box_props.margin.left
1578 - box_props.margin.right
1579 - box_props.border.left
1580 - box_props.border.right
1581 - box_props.padding.left
1582 - box_props.padding.right)
1583 .max(0.0);
1584 let preferred_minimum = intrinsic.min_content_width;
1585 let preferred = intrinsic.max_content_width;
1586 preferred_minimum.max(available_width).min(preferred).max(0.0)
1587 }
1588 LayoutDisplay::Inline => {
1589 intrinsic.max_content_width
1591 }
1592 LayoutDisplay::Table | LayoutDisplay::InlineTable => intrinsic.max_content_width,
1593 LayoutDisplay::TableCell => {
1598 if intrinsic.max_content_width > 0.0 {
1599 intrinsic.max_content_width
1600 } else {
1601 (containing_block_size.width
1602 - box_props.margin.left
1603 - box_props.margin.right
1604 - box_props.border.left
1605 - box_props.border.right
1606 - box_props.padding.left
1607 - box_props.padding.right)
1608 .max(0.0)
1609 }
1610 }
1611 _ => intrinsic.max_content_width,
1613 }
1614 }
1615 }
1616 LayoutWidth::Px(px) => {
1617 let em = get_element_font_size(styled_dom, id, node_state);
1618 let rem = super::getters::get_root_font_size(styled_dom, node_state);
1619 let pixels_opt = super::calc::resolve_pixel_value_no_percent_with_viewport(
1620 &px, em, rem,
1621 viewport_size.width, viewport_size.height,
1622 );
1623
1624 pixels_opt.unwrap_or_else(|| {
1625 px.to_percent().map_or(intrinsic.max_content_width, |p| {
1626 resolve_percentage_with_box_model(
1627 containing_block_size.width,
1628 p.get(),
1629 (box_props.margin.left, box_props.margin.right),
1630 (box_props.border.left, box_props.border.right),
1631 (box_props.padding.left, box_props.padding.right),
1632 )
1633 })
1634 })
1635 }
1636 LayoutWidth::MinContent => intrinsic.min_content_width,
1639 LayoutWidth::MaxContent => intrinsic.max_content_width,
1640 LayoutWidth::FitContent(px) => {
1644 let em = get_element_font_size(styled_dom, id, node_state);
1645 let rem = super::getters::get_root_font_size(styled_dom, node_state);
1646 let arg = super::calc::resolve_pixel_value_with_viewport(
1647 &px, containing_block_size.width, em, rem,
1648 viewport_size.width, viewport_size.height,
1649 );
1650 intrinsic.max_content_width.min(intrinsic.min_content_width.max(arg))
1651 }
1652 LayoutWidth::Calc(items) => {
1653 use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1654 let em = get_element_font_size(styled_dom, id, node_state);
1655 let calc_ctx = super::calc::CalcResolveContext {
1656 items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1657 };
1658 super::calc::evaluate_calc(&calc_ctx, containing_block_size.width)
1659 }
1660 };
1661 let resolved_width = resolved_width.max(0.0);
1663
1664 let resolved_height = match css_height.unwrap_or_default() {
1677 LayoutHeight::Auto => {
1678 let abs_stretch_fit = if matches!(
1703 position,
1704 LayoutPosition::Absolute | LayoutPosition::Fixed
1705 ) && !is_replaced
1706 {
1707 let off = crate::solver3::positioning::resolve_position_offsets(
1708 styled_dom, dom_id, *containing_block_size, *viewport_size,
1709 );
1710 match (off.top, off.bottom) {
1711 (Some(t), Some(b)) => Some(
1712 (containing_block_size.height
1713 - t
1714 - b
1715 - box_props.margin.top
1716 - box_props.margin.bottom)
1717 .max(0.0),
1718 ),
1719 _ => None,
1720 }
1721 } else {
1722 None
1723 };
1724 match abs_stretch_fit {
1725 Some(h) => h,
1726 None if is_replaced => intrinsic.max_content_height,
1731 None => match display.unwrap_or_default() {
1732 LayoutDisplay::Block
1733 | LayoutDisplay::FlowRoot
1734 | LayoutDisplay::ListItem
1735 | LayoutDisplay::Flex
1736 | LayoutDisplay::Grid => 0.0,
1737 LayoutDisplay::Inline => 0.0,
1740 _ => intrinsic.max_content_height,
1742 },
1743 }
1744 }
1745 LayoutHeight::Px(px) => {
1746 let em = get_element_font_size(styled_dom, id, node_state);
1747 let rem = super::getters::get_root_font_size(styled_dom, node_state);
1748 let pixels_opt = super::calc::resolve_pixel_value_no_percent_with_viewport(
1749 &px, em, rem,
1750 viewport_size.width, viewport_size.height,
1751 );
1752
1753 pixels_opt.unwrap_or_else(|| {
1755 px.to_percent().map_or(intrinsic.max_content_height, |p| {
1756 resolve_percentage_with_box_model(
1757 containing_block_size.height,
1758 p.get(),
1759 (box_props.margin.top, box_props.margin.bottom),
1760 (box_props.border.top, box_props.border.bottom),
1761 (box_props.padding.top, box_props.padding.bottom),
1762 )
1763 })
1764 })
1765 }
1766 LayoutHeight::MinContent => intrinsic.max_content_height,
1768 LayoutHeight::MaxContent => intrinsic.max_content_height,
1770 LayoutHeight::FitContent(px) => {
1773 let em = get_element_font_size(styled_dom, id, node_state);
1774 let rem = super::getters::get_root_font_size(styled_dom, node_state);
1775 let arg = super::calc::resolve_pixel_value_with_viewport(
1776 &px, containing_block_size.height, em, rem,
1777 viewport_size.width, viewport_size.height,
1778 );
1779 let auto_height = intrinsic.max_content_height;
1780 auto_height.min(auto_height.max(arg))
1781 }
1782 LayoutHeight::Calc(items) => {
1783 use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1784 let em = get_element_font_size(styled_dom, id, node_state);
1785 let calc_ctx = super::calc::CalcResolveContext {
1786 items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1787 };
1788 super::calc::evaluate_calc(&calc_ctx, containing_block_size.height)
1789 }
1790 };
1791 let resolved_height = resolved_height.max(0.0);
1793
1794 let (resolved_width, resolved_height) = if is_replaced
1799 && width_is_auto
1800 && matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed)
1801 {
1802 let has_intrinsic_width = intrinsic.preferred_width.is_some_and(|w| w > 0.0);
1803 let has_intrinsic_height = intrinsic.preferred_height.is_some_and(|h| h > 0.0);
1804 let intrinsic_ratio = match (intrinsic.preferred_width, intrinsic.preferred_height) {
1805 (Some(iw), Some(ih)) if ih > 0.0 => Some(iw / ih),
1806 _ => None,
1807 };
1808
1809 intrinsic_ratio.map_or((resolved_width, resolved_height), |ratio| if height_is_auto && !has_intrinsic_width && has_intrinsic_height {
1810 (resolved_height * ratio, resolved_height)
1813 } else if !height_is_auto {
1814 (resolved_height * ratio, resolved_height)
1817 } else if height_is_auto && !has_intrinsic_width && !has_intrinsic_height {
1818 let block_width = (containing_block_size.width
1821 - box_props.margin.left
1822 - box_props.margin.right
1823 - box_props.border.left
1824 - box_props.border.right
1825 - box_props.padding.left
1826 - box_props.padding.right)
1827 .max(0.0);
1828 (block_width, block_width / ratio)
1829 } else {
1830 (resolved_width, resolved_height)
1831 })
1832 } else {
1833 (resolved_width, resolved_height)
1834 };
1835
1836 #[allow(clippy::cast_precision_loss)] let (resolved_width, resolved_height) = if is_replaced {
1843 (resolved_width, resolved_height)
1844 } else if let MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(ar)) =
1845 crate::solver3::getters::get_aspect_ratio_property(styled_dom, id, node_state)
1846 {
1847 let ratio = if ar.height == 0 { 0.0 } else { ar.width as f32 / ar.height as f32 };
1848 if ratio > 0.0 && height_is_auto && !width_is_auto {
1849 (resolved_width, resolved_width / ratio)
1850 } else if ratio > 0.0 && width_is_auto && !height_is_auto {
1851 (resolved_height * ratio, resolved_height)
1852 } else {
1853 (resolved_width, resolved_height)
1854 }
1855 } else {
1856 (resolved_width, resolved_height)
1857 };
1858
1859 let has_intrinsic_ratio = intrinsic.preferred_width.is_some()
1873 && intrinsic.preferred_height.is_some()
1874 && intrinsic.preferred_width.unwrap_or(0.0) > 0.0
1875 && intrinsic.preferred_height.unwrap_or(0.0) > 0.0;
1876
1877 let (constrained_width, constrained_height) = if has_intrinsic_ratio {
1879 apply_constraint_violation_table(
1882 styled_dom,
1883 id,
1884 node_state,
1885 resolved_width,
1886 resolved_height,
1887 containing_block_size.width,
1888 containing_block_size.height,
1889 box_props,
1890 )
1891 } else {
1892 let cw = apply_width_constraints(
1894 styled_dom,
1895 id,
1896 node_state,
1897 resolved_width,
1898 containing_block_size.width,
1899 box_props,
1900 );
1901
1902 let ch = apply_height_constraints(
1903 styled_dom,
1904 id,
1905 node_state,
1906 resolved_height,
1907 containing_block_size.height,
1908 box_props,
1909 );
1910 (cw, ch)
1911 };
1912
1913 let box_sizing = match get_css_box_sizing(styled_dom, id, node_state) {
1925 MultiValue::Exact(bs) => bs,
1926 MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
1927 azul_css::props::layout::LayoutBoxSizing::ContentBox
1928 }
1929 };
1930
1931 let (border_box_width, border_box_height) = match box_sizing {
1932 azul_css::props::layout::LayoutBoxSizing::BorderBox => {
1933 let min_border_box_w = box_props.padding.left
1936 + box_props.padding.right
1937 + box_props.border.left
1938 + box_props.border.right;
1939 let min_border_box_h = box_props.padding.top
1940 + box_props.padding.bottom
1941 + box_props.border.top
1942 + box_props.border.bottom;
1943 let bw = if width_is_quantitative {
1951 constrained_width.max(min_border_box_w)
1952 } else {
1953 constrained_width
1954 + box_props.padding.left
1955 + box_props.padding.right
1956 + box_props.border.left
1957 + box_props.border.right
1958 };
1959 let bh = if height_is_quantitative {
1960 constrained_height.max(min_border_box_h)
1961 } else {
1962 constrained_height
1963 + box_props.padding.top
1964 + box_props.padding.bottom
1965 + box_props.border.top
1966 + box_props.border.bottom
1967 };
1968 (bw, bh)
1969 }
1970 azul_css::props::layout::LayoutBoxSizing::ContentBox => {
1971 let border_box_width = constrained_width
1973 + box_props.padding.left
1974 + box_props.padding.right
1975 + box_props.border.left
1976 + box_props.border.right;
1977 let border_box_height = constrained_height
1978 + box_props.padding.top
1979 + box_props.padding.bottom
1980 + box_props.border.top
1981 + box_props.border.bottom;
1982 (border_box_width, border_box_height)
1983 }
1984 };
1985
1986 let (main_size, cross_size) = if is_vertical {
1997 (border_box_width, border_box_height)
2000 } else {
2001 (border_box_height, border_box_width)
2003 };
2004
2005 let result =
2008 LogicalSize::from_main_cross(main_size, cross_size, writing_mode.unwrap_or_default());
2009
2010 Ok(result)
2011}
2012
2013fn apply_constraint_violation_table(
2021 styled_dom: &StyledDom,
2022 id: NodeId,
2023 node_state: &StyledNodeState,
2024 w: f32, h: f32, containing_block_width: f32,
2027 containing_block_height: f32,
2028 box_props: &BoxProps,
2029) -> (f32, f32) {
2030 use crate::solver3::getters::{
2031 get_css_min_width, get_css_max_width, get_css_min_height, get_css_max_height, MultiValue,
2032 };
2033
2034 let em = get_element_font_size(styled_dom, id, node_state);
2037 let rem = super::getters::get_root_font_size(styled_dom, node_state);
2038
2039 let min_w = match get_css_min_width(styled_dom, id, node_state) {
2045 MultiValue::Exact(mw) => resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem).unwrap_or(0.0),
2046 _ => 0.0,
2047 };
2048
2049 let max_w = match get_css_max_width(styled_dom, id, node_state) {
2051 MultiValue::Exact(mw) => {
2052 if mw.inner.number.get() >= core::f32::MAX - 1.0 {
2053 f32::MAX
2054 } else {
2055 resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem).unwrap_or(f32::MAX)
2056 }
2057 }
2058 _ => f32::MAX,
2059 };
2060
2061 let min_h = match get_css_min_height(styled_dom, id, node_state) {
2063 MultiValue::Exact(mh) => resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem).unwrap_or(0.0),
2064 _ => 0.0,
2065 };
2066
2067 let max_h = match get_css_max_height(styled_dom, id, node_state) {
2069 MultiValue::Exact(mh) => {
2070 if mh.inner.number.get() >= core::f32::MAX - 1.0 {
2071 f32::MAX
2072 } else {
2073 resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem).unwrap_or(f32::MAX)
2074 }
2075 }
2076 _ => f32::MAX,
2077 };
2078
2079 let max_w = max_w.max(min_w);
2081 let max_h = max_h.max(min_h);
2082
2083 if w <= 0.0 || h <= 0.0 {
2085 return (w.max(min_w).min(max_w), h.max(min_h).min(max_h));
2086 }
2087
2088 let w_over = w > max_w;
2089 let w_under = w < min_w;
2090 let h_over = h > max_h;
2091 let h_under = h < min_h;
2092
2093 match (w_over, w_under, h_over, h_under) {
2095 (false, false, false, false) => (w, h),
2097
2098 (true, false, false, false) => {
2100 (max_w, (max_w * h / w).max(min_h))
2101 }
2102
2103 (false, true, false, false) => {
2105 (min_w, (min_w * h / w).min(max_h))
2106 }
2107
2108 (false, false, true, false) => {
2110 ((max_h * w / h).max(min_w), max_h)
2111 }
2112
2113 (false, false, false, true) => {
2115 ((min_h * w / h).min(max_w), min_h)
2116 }
2117
2118 (true, false, true, false) => {
2120 if max_w / w <= max_h / h {
2121 (max_w, (max_w * h / w).max(min_h))
2122 } else {
2123 ((max_h * w / h).max(min_w), max_h)
2124 }
2125 }
2126
2127 (false, true, false, true) => {
2129 if min_w / w <= min_h / h {
2130 ((min_h * w / h).min(max_w), min_h)
2131 } else {
2132 (min_w, (min_w * h / w).min(max_h))
2133 }
2134 }
2135
2136 (false, true, true, false) => (min_w, max_h),
2138
2139 (true, false, false, true) => (max_w, min_h),
2141
2142 _ => (w.max(min_w).min(max_w), h.max(min_h).min(max_h)),
2144 }
2145}
2146
2147fn apply_width_constraints(
2156 styled_dom: &StyledDom,
2157 id: NodeId,
2158 node_state: &StyledNodeState,
2159 tentative_width: f32,
2160 containing_block_width: f32,
2161 box_props: &BoxProps,
2162) -> f32 {
2163 use crate::solver3::getters::{get_css_max_width, get_css_min_width, MultiValue};
2164
2165 let em = get_element_font_size(styled_dom, id, node_state);
2167 let rem = super::getters::get_root_font_size(styled_dom, node_state);
2168
2169 let min_width = match get_css_min_width(styled_dom, id, node_state) {
2172 MultiValue::Exact(mw) => resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem).unwrap_or(0.0),
2173 _ => 0.0,
2174 };
2175
2176 let max_width = match get_css_max_width(styled_dom, id, node_state) {
2178 MultiValue::Exact(mw) => {
2179 if mw.inner.number.get() >= core::f32::MAX - 1.0 {
2180 None
2181 } else {
2182 resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem)
2183 }
2184 }
2185 _ => None,
2186 };
2187
2188 let mut result = tentative_width;
2191 if let Some(max) = max_width {
2192 result = result.min(max);
2193 }
2194 result.max(min_width)
2195}
2196
2197fn apply_height_constraints(
2203 styled_dom: &StyledDom,
2204 id: NodeId,
2205 node_state: &StyledNodeState,
2206 tentative_height: f32,
2207 containing_block_height: f32,
2208 box_props: &BoxProps,
2209) -> f32 {
2210 use crate::solver3::getters::{get_css_max_height, get_css_min_height, MultiValue};
2211
2212 let em = get_element_font_size(styled_dom, id, node_state);
2214 let rem = super::getters::get_root_font_size(styled_dom, node_state);
2215
2216 let min_height = match get_css_min_height(styled_dom, id, node_state) {
2219 MultiValue::Exact(mh) => resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem).unwrap_or(0.0),
2220 _ => 0.0,
2221 };
2222
2223 let max_height = match get_css_max_height(styled_dom, id, node_state) {
2225 MultiValue::Exact(mh) => {
2226 if mh.inner.number.get() >= core::f32::MAX - 1.0 {
2227 None
2228 } else {
2229 resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem)
2230 }
2231 }
2232 _ => None,
2233 };
2234
2235 let mut result = tentative_height;
2239 if let Some(max) = max_height {
2240 result = result.min(max);
2241 }
2242 result.max(min_height)
2243}
2244
2245#[must_use] pub fn extract_text_from_node(styled_dom: &StyledDom, node_id: NodeId) -> Option<String> {
2246 match &styled_dom.node_data.as_container()[node_id].get_node_type() {
2247 NodeType::Text(text_data) => {
2248 Some(text_data.as_str().to_string())
2249 }
2250 _ => None,
2251 }
2252}
2253
2254#[cfg(test)]
2255#[allow(clippy::float_cmp, clippy::too_many_lines)]
2256mod autotest_generated {
2257 use std::collections::{BTreeMap, HashMap, HashSet};
2258
2259 use azul_core::{
2260 dom::{Dom, DomId, IdOrClass},
2261 selection::TextSelection,
2262 };
2263 use azul_css::props::basic::{FontRef, SizeMetric};
2264
2265 use super::*;
2266 use crate::solver3::{
2267 geometry::{EdgeSizes, MarginAuto, PackedBoxProps},
2268 layout_tree::{generate_layout_tree, LayoutNodeCold, LayoutNodeWarm},
2269 };
2270
2271 const VIEWPORT: LogicalSize = LogicalSize {
2276 width: 800.0,
2277 height: 600.0,
2278 };
2279
2280 const BLOCK: FormattingContext = FormattingContext::Block {
2281 establishes_new_context: false,
2282 };
2283
2284 fn size(w: f32, h: f32) -> LogicalSize {
2285 LogicalSize::new(w, h)
2286 }
2287
2288 fn all_edges(v: f32) -> EdgeSizes {
2289 EdgeSizes {
2290 top: v,
2291 right: v,
2292 bottom: v,
2293 left: v,
2294 }
2295 }
2296
2297 fn props(margin: f32, border: f32, padding: f32) -> BoxProps {
2299 BoxProps {
2300 margin: all_edges(margin),
2301 border: all_edges(border),
2302 padding: all_edges(padding),
2303 margin_auto: MarginAuto::default(),
2304 }
2305 }
2306
2307 fn zero_props() -> BoxProps {
2308 props(0.0, 0.0, 0.0)
2309 }
2310
2311 fn isz(min_w: f32, max_w: f32, min_h: f32, max_h: f32) -> IntrinsicSizes {
2312 IntrinsicSizes {
2313 min_content_width: min_w,
2314 max_content_width: max_w,
2315 preferred_width: None,
2316 min_content_height: min_h,
2317 max_content_height: max_h,
2318 preferred_height: None,
2319 preferred_aspect_ratio: None,
2320 }
2321 }
2322
2323 fn styled(dom: Dom, css_str: &str) -> StyledDom {
2324 let mut dom = dom;
2325 let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
2326 StyledDom::create(&mut dom, css)
2327 }
2328
2329 fn div_class(class: &str) -> Dom {
2330 Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
2331 }
2332
2333 struct Env {
2339 styled_dom: StyledDom,
2340 font_manager: FontManager<FontRef>,
2341 text_selections: BTreeMap<DomId, TextSelection>,
2342 counters: HashMap<(usize, String), i32>,
2343 image_cache: azul_core::resources::ImageCache,
2344 debug_messages: Option<Vec<LayoutDebugMessage>>,
2345 }
2346
2347 impl Env {
2348 fn new(styled_dom: StyledDom) -> Self {
2349 Self {
2350 styled_dom,
2351 font_manager: FontManager::new(FcFontCache::default())
2352 .expect("FontManager over an empty font cache"),
2353 text_selections: BTreeMap::new(),
2354 counters: HashMap::new(),
2355 image_cache: azul_core::resources::ImageCache::default(),
2356 debug_messages: None,
2357 }
2358 }
2359
2360 fn ctx(&mut self) -> LayoutContext<'_, FontRef> {
2361 LayoutContext {
2362 scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
2363 styled_dom: &self.styled_dom,
2364 font_manager: &self.font_manager,
2365 text_selections: &self.text_selections,
2366 debug_messages: &mut self.debug_messages,
2367 counters: &mut self.counters,
2368 viewport_size: VIEWPORT,
2369 fragmentation_context: None,
2370 cursor_is_visible: true,
2371 cursor_locations: Vec::new(),
2372 preedit_text: None,
2373 dirty_text_overrides: BTreeMap::new(),
2374 cache_map: crate::solver3::cache::LayoutCacheMap::default(),
2375 image_cache: &self.image_cache,
2376 system_style: None,
2377 get_system_time_fn: azul_core::task::GetSystemTimeCallback {
2378 cb: azul_core::task::get_system_time_libstd,
2379 },
2380 }
2381 }
2382 }
2383
2384 fn hot(parent: Option<usize>, fc: FormattingContext, bp: &BoxProps) -> LayoutNodeHot {
2385 LayoutNodeHot {
2386 box_props: PackedBoxProps::pack(bp),
2387 dom_node_id: None,
2388 used_size: None,
2389 formatting_context: fc,
2390 parent,
2391 }
2392 }
2393
2394 fn tree_of(nodes: Vec<LayoutNodeHot>, child_lists: &[Vec<usize>]) -> LayoutTree {
2398 let n = nodes.len();
2399 let mut children_arena: Vec<usize> = Vec::new();
2400 let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
2401 for cl in child_lists {
2402 let start = u32::try_from(children_arena.len()).expect("arena fits in u32");
2403 children_arena.extend_from_slice(cl);
2404 children_offsets.push((start, u32::try_from(cl.len()).expect("len fits in u32")));
2405 }
2406 while children_offsets.len() < n {
2407 children_offsets.push((0, 0));
2408 }
2409 LayoutTree {
2410 nodes,
2411 warm: vec![LayoutNodeWarm::default(); n],
2412 cold: vec![LayoutNodeCold::default(); n],
2413 root: 0,
2414 dom_to_layout: BTreeMap::new(),
2415 children_arena,
2416 children_offsets,
2417 subtree_needs_intrinsic: Vec::new(),
2418 }
2419 }
2420
2421 fn layout_index(tree: &LayoutTree, dom_id: NodeId) -> usize {
2423 *tree
2424 .dom_to_layout
2425 .get(&dom_id)
2426 .and_then(|v| v.first())
2427 .expect("DOM node has a layout node")
2428 }
2429
2430 #[test]
2435 fn resolve_percentage_at_zero_is_zero_on_both_operands() {
2436 assert_eq!(
2437 resolve_percentage_with_box_model(0.0, 0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2438 0.0
2439 );
2440 assert_eq!(
2441 resolve_percentage_with_box_model(800.0, 0.0, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2442 0.0
2443 );
2444 }
2445
2446 #[test]
2447 fn resolve_percentage_ignores_the_box_model_arguments_entirely() {
2448 let plain =
2452 resolve_percentage_with_box_model(800.0, 0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0));
2453 let poisoned = resolve_percentage_with_box_model(
2454 800.0,
2455 0.5,
2456 (f32::NAN, f32::INFINITY),
2457 (f32::MAX, f32::MIN),
2458 (-1e30, 1e30),
2459 );
2460 assert_eq!(plain, 400.0);
2461 assert_eq!(poisoned, 400.0, "box-model args must not leak into the result");
2462 }
2463
2464 #[test]
2465 fn resolve_percentage_floors_negative_products_at_zero() {
2466 assert_eq!(
2468 resolve_percentage_with_box_model(-800.0, 0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2469 0.0
2470 );
2471 assert_eq!(
2472 resolve_percentage_with_box_model(800.0, -0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2473 0.0
2474 );
2475 assert_eq!(
2477 resolve_percentage_with_box_model(-800.0, -0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2478 400.0
2479 );
2480 }
2481
2482 #[test]
2483 fn resolve_percentage_never_returns_nan() {
2484 for (cb, pct) in [
2487 (f32::NAN, 0.5),
2488 (800.0, f32::NAN),
2489 (f32::NAN, f32::NAN),
2490 (f32::INFINITY, 0.0),
2491 (f32::NEG_INFINITY, 0.0),
2492 (f32::NEG_INFINITY, 0.5),
2493 ] {
2494 let r = resolve_percentage_with_box_model(cb, pct, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0));
2495 assert!(!r.is_nan(), "NaN escaped for cb={cb}, pct={pct}");
2496 assert_eq!(r, 0.0, "cb={cb}, pct={pct}");
2497 }
2498 }
2499
2500 #[test]
2501 fn resolve_percentage_saturates_to_infinity_on_overflow() {
2502 let r =
2504 resolve_percentage_with_box_model(f32::MAX, 100.0, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0));
2505 assert!(r.is_infinite() && r.is_sign_positive());
2506 let r = resolve_percentage_with_box_model(
2508 f32::INFINITY,
2509 0.5,
2510 (0.0, 0.0),
2511 (0.0, 0.0),
2512 (0.0, 0.0),
2513 );
2514 assert!(r.is_infinite() && r.is_sign_positive());
2515 }
2516
2517 #[test]
2518 fn resolve_percentage_is_monotone_in_the_percentage() {
2519 let at = |p: f32| {
2520 resolve_percentage_with_box_model(800.0, p, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0))
2521 };
2522 assert!(at(0.0) <= at(0.25) && at(0.25) <= at(0.5) && at(0.5) <= at(1.0));
2523 assert_eq!(at(1.0), 800.0);
2524 }
2525
2526 #[test]
2531 fn resolve_px_absolute_length_ignores_the_containing_block() {
2532 let bp = props(7.0, 3.0, 11.0);
2533 let px = PixelValue::const_px(50);
2534 assert_eq!(
2535 resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0),
2536 Some(50.0)
2537 );
2538 assert_eq!(
2540 resolve_px_with_box_model(&px, -1.0e30, &bp, false, 16.0, 16.0),
2541 Some(50.0)
2542 );
2543 }
2544
2545 #[test]
2546 fn resolve_px_percentage_resolves_against_the_containing_block_on_either_axis() {
2547 let bp = props(10.0, 2.0, 5.0);
2548 let px = PixelValue::const_percent(50);
2549 let horizontal = resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0);
2550 let vertical = resolve_px_with_box_model(&px, 800.0, &bp, false, 16.0, 16.0);
2551 assert_eq!(horizontal, Some(400.0));
2552 assert_eq!(vertical, horizontal);
2555 }
2556
2557 #[test]
2558 fn resolve_px_percentage_against_a_degenerate_containing_block_is_zero() {
2559 let bp = zero_props();
2560 let px = PixelValue::const_percent(50);
2561 for cb in [-800.0, f32::NAN, f32::NEG_INFINITY] {
2562 let r = resolve_px_with_box_model(&px, cb, &bp, true, 16.0, 16.0)
2563 .expect("a percentage always resolves to Some");
2564 assert!(!r.is_nan(), "NaN escaped for cb={cb}");
2565 assert_eq!(r, 0.0, "cb={cb}");
2566 }
2567 }
2568
2569 #[test]
2570 fn resolve_px_em_and_rem_resolve_against_the_supplied_font_sizes() {
2571 let bp = zero_props();
2572 assert_eq!(
2573 resolve_px_with_box_model(&PixelValue::const_em(3), 800.0, &bp, true, 20.0, 16.0),
2574 Some(60.0)
2575 );
2576 assert_eq!(
2577 resolve_px_with_box_model(
2578 &PixelValue::from_metric(SizeMetric::Rem, 2.0),
2579 800.0,
2580 &bp,
2581 true,
2582 20.0,
2583 16.0
2584 ),
2585 Some(32.0)
2586 );
2587 assert_eq!(
2589 resolve_px_with_box_model(&PixelValue::const_em(3), 800.0, &bp, true, 0.0, 0.0),
2590 Some(0.0)
2591 );
2592 }
2593
2594 #[test]
2595 fn resolve_px_returns_none_for_viewport_units() {
2596 let bp = zero_props();
2602 for metric in [
2603 SizeMetric::Vw,
2604 SizeMetric::Vh,
2605 SizeMetric::Vmin,
2606 SizeMetric::Vmax,
2607 ] {
2608 let px = PixelValue::from_metric(metric, 10.0);
2609 assert_eq!(
2610 resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0),
2611 None,
2612 "{metric:?} unexpectedly resolved"
2613 );
2614 }
2615 }
2616
2617 #[test]
2618 fn resolve_px_extreme_lengths_stay_finite() {
2619 let bp = zero_props();
2623 for raw in [f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
2624 let px = PixelValue::px(raw);
2625 let r = resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0)
2626 .expect("px metric always resolves to Some");
2627 assert!(r.is_finite(), "non-finite length from PixelValue::px({raw})");
2628 }
2629 assert_eq!(
2630 resolve_px_with_box_model(&PixelValue::px(f32::NAN), 800.0, &bp, true, 16.0, 16.0),
2631 Some(0.0),
2632 "NaN saturates to 0 in the fixed-point encoding"
2633 );
2634 }
2635
2636 #[test]
2641 fn auto_block_inline_size_subtracts_the_full_horizontal_box_model() {
2642 let cb = size(800.0, 600.0);
2643 assert_eq!(auto_block_inline_size(&cb, &props(10.0, 2.0, 5.0)), 766.0);
2645 assert_eq!(auto_block_inline_size(&cb, &zero_props()), 800.0);
2646 }
2647
2648 #[test]
2649 fn auto_block_inline_size_floors_at_zero_when_the_box_model_exceeds_the_cb() {
2650 let cb = size(10.0, 600.0);
2651 assert_eq!(auto_block_inline_size(&cb, &props(100.0, 50.0, 25.0)), 0.0);
2652 assert_eq!(auto_block_inline_size(&size(0.0, 0.0), &zero_props()), 0.0);
2654 assert_eq!(auto_block_inline_size(&size(-800.0, 0.0), &zero_props()), 0.0);
2656 }
2657
2658 #[test]
2659 fn auto_block_inline_size_never_returns_nan() {
2660 let cases = [
2661 (size(f32::NAN, 0.0), zero_props()),
2662 (size(f32::INFINITY, 0.0), props(f32::INFINITY, 0.0, 0.0)),
2663 (size(f32::NEG_INFINITY, 0.0), zero_props()),
2664 (size(0.0, 0.0), props(f32::NAN, 0.0, 0.0)),
2665 ];
2666 for (cb, bp) in cases {
2667 let r = auto_block_inline_size(&cb, &bp);
2668 assert!(!r.is_nan(), "NaN escaped for cb.width={}", cb.width);
2669 assert_eq!(r, 0.0);
2670 }
2671 }
2672
2673 #[test]
2674 fn auto_block_inline_size_saturates_rather_than_overflowing() {
2675 let r = auto_block_inline_size(&size(f32::MAX, 0.0), &props(1.0, 1.0, 1.0));
2677 assert!(r.is_finite() && r > 0.0);
2678 let r = auto_block_inline_size(&size(f32::INFINITY, 0.0), &props(1.0, 1.0, 1.0));
2679 assert!(r.is_infinite() && r.is_sign_positive());
2680 }
2681
2682 fn chain_tree() -> LayoutTree {
2688 let bp = zero_props();
2689 tree_of(
2690 vec![
2691 hot(None, BLOCK, &bp),
2692 hot(Some(0), BLOCK, &bp),
2693 hot(Some(1), BLOCK, &bp),
2694 ],
2695 &[vec![1], vec![2], vec![]],
2696 )
2697 }
2698
2699 #[test]
2700 fn dirty_closure_of_an_empty_set_is_empty() {
2701 let tree = chain_tree();
2702 let closure = compute_dirty_ancestor_closure(&tree, &BTreeSet::new());
2703 assert!(closure.is_empty());
2704 }
2705
2706 #[test]
2707 fn dirty_closure_of_a_leaf_contains_every_ancestor_up_to_the_root() {
2708 let tree = chain_tree();
2709 let dirty: BTreeSet<usize> = [2].into_iter().collect();
2710 let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2711 assert_eq!(closure, [0, 1, 2].into_iter().collect::<HashSet<usize>>());
2712 }
2713
2714 #[test]
2715 fn dirty_closure_tolerates_out_of_range_dirty_indices() {
2716 let tree = chain_tree();
2717 let dirty: BTreeSet<usize> = [usize::MAX, 999, 2].into_iter().collect();
2718 let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2719 assert!(closure.contains(&usize::MAX) && closure.contains(&999));
2722 assert!(closure.contains(&0) && closure.contains(&1) && closure.contains(&2));
2723 }
2724
2725 #[test]
2726 fn dirty_closure_terminates_on_a_cyclic_parent_chain() {
2727 let bp = zero_props();
2730 let tree = tree_of(
2731 vec![hot(Some(1), BLOCK, &bp), hot(Some(0), BLOCK, &bp)],
2732 &[vec![], vec![]],
2733 );
2734 let dirty: BTreeSet<usize> = [0].into_iter().collect();
2735 let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2736 assert_eq!(closure, [0, 1].into_iter().collect::<HashSet<usize>>());
2737 }
2738
2739 #[test]
2740 fn dirty_closure_terminates_on_a_self_parenting_node() {
2741 let bp = zero_props();
2742 let tree = tree_of(vec![hot(Some(0), BLOCK, &bp)], &[vec![]]);
2743 let dirty: BTreeSet<usize> = [0].into_iter().collect();
2744 let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2745 assert_eq!(closure, [0].into_iter().collect::<HashSet<usize>>());
2746 }
2747
2748 #[test]
2753 fn intrinsic_size_calculator_new_starts_without_a_dirty_closure() {
2754 let mut env = Env::new(styled(Dom::create_body(), ""));
2755 let mut ctx = env.ctx();
2756 let mut text_cache = LayoutCache::new();
2757 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2758 assert!(
2759 calc.dirty_closure.is_none(),
2760 "a fresh calculator must not skip any node"
2761 );
2762 assert_eq!(calc.ctx.viewport_size, VIEWPORT, "ctx is threaded through");
2763 }
2764
2765 #[test]
2770 fn calculate_intrinsic_recursive_rejects_an_out_of_range_node_index() {
2771 let mut env = Env::new(styled(Dom::create_body(), ""));
2772 let mut ctx = env.ctx();
2773 let mut text_cache = LayoutCache::new();
2774 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2775 let mut tree = chain_tree();
2776
2777 for bogus in [3, 999, usize::MAX] {
2778 let r = calc.calculate_intrinsic_recursive(&mut tree, bogus, false);
2779 assert!(
2780 matches!(r, Err(LayoutError::InvalidTree)),
2781 "index {bogus} must be rejected, not panic"
2782 );
2783 }
2784 }
2785
2786 #[test]
2787 fn calculate_node_intrinsic_sizes_rejects_an_out_of_range_node_index() {
2788 let mut env = Env::new(styled(Dom::create_body(), ""));
2789 let mut ctx = env.ctx();
2790 let mut text_cache = LayoutCache::new();
2791 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2792 let tree = chain_tree();
2793 let r = calc.calculate_node_intrinsic_sizes(&tree, usize::MAX, &[]);
2794 assert!(matches!(r, Err(LayoutError::InvalidTree)));
2795 }
2796
2797 #[test]
2798 fn calculate_intrinsic_recursive_skips_stray_child_indices_instead_of_aborting() {
2799 let bp = zero_props();
2802 let mut tree = tree_of(
2803 vec![hot(None, BLOCK, &bp), hot(Some(0), BLOCK, &bp)],
2804 &[vec![1, 4242], vec![]],
2805 );
2806 let mut env = Env::new(styled(Dom::create_body(), ""));
2807 let mut ctx = env.ctx();
2808 let mut text_cache = LayoutCache::new();
2809 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2810
2811 let r = calc.calculate_intrinsic_recursive(&mut tree, 0, false);
2812 let sizes = r.expect("a stray child index must be skipped, not fatal");
2813 assert!(sizes.min_content_width.is_finite());
2814 assert!(tree.warm(0).and_then(|w| w.intrinsic_sizes).is_some());
2815 }
2816
2817 #[test]
2818 fn calculate_intrinsic_recursive_reuses_the_cache_for_nodes_outside_the_dirty_closure() {
2819 let mut tree = chain_tree();
2820 let cached = isz(11.0, 22.0, 33.0, 44.0);
2821 tree.warm_mut(0)
2822 .expect("root warm slot")
2823 .intrinsic_sizes = Some(cached);
2824
2825 let mut env = Env::new(styled(Dom::create_body(), ""));
2826 let mut ctx = env.ctx();
2827 let mut text_cache = LayoutCache::new();
2828 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2829 calc.dirty_closure = Some(HashSet::new());
2832
2833 let sizes = calc
2834 .calculate_intrinsic_recursive(&mut tree, 0, false)
2835 .expect("cached path");
2836 assert_eq!(sizes.min_content_width, 11.0);
2837 assert_eq!(sizes.max_content_width, 22.0);
2838 assert_eq!(sizes.min_content_height, 33.0);
2839 assert_eq!(sizes.max_content_height, 44.0);
2840 assert!(tree.warm(1).and_then(|w| w.intrinsic_sizes).is_none());
2842 }
2843
2844 fn block_parent_with_children(n: usize) -> LayoutTree {
2850 parent_with_children(BLOCK, n)
2851 }
2852
2853 fn parent_with_children(fc: FormattingContext, n: usize) -> LayoutTree {
2855 let bp = zero_props();
2856 let mut nodes = vec![hot(None, fc, &bp)];
2857 let mut kids = Vec::new();
2858 for i in 0..n {
2859 nodes.push(hot(Some(0), BLOCK, &bp));
2860 kids.push(i + 1);
2861 }
2862 let mut child_lists = vec![kids];
2863 child_lists.resize(n + 1, Vec::new());
2864 tree_of(nodes, &child_lists)
2865 }
2866
2867 #[test]
2868 fn block_intrinsic_sizes_take_the_max_width_and_the_sum_of_heights() {
2869 let tree = block_parent_with_children(2);
2870 let mut env = Env::new(styled(Dom::create_body(), ""));
2871 let mut ctx = env.ctx();
2872 let mut text_cache = LayoutCache::new();
2873 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2874
2875 let children = [(1usize, isz(10.0, 20.0, 5.0, 6.0)), (2usize, isz(30.0, 40.0, 7.0, 8.0))];
2876 let r = calc
2877 .calculate_block_intrinsic_sizes(&tree, 0, &children)
2878 .expect("valid tree");
2879 assert_eq!(r.min_content_width, 30.0, "cross axis = widest child");
2880 assert_eq!(r.max_content_width, 40.0);
2881 assert_eq!(r.min_content_height, 14.0, "main axis = stacked heights");
2882 assert_eq!(r.max_content_height, 14.0);
2883 }
2884
2885 #[test]
2886 fn block_intrinsic_sizes_ignore_children_missing_from_the_intrinsics_slice() {
2887 let tree = block_parent_with_children(2);
2888 let mut env = Env::new(styled(Dom::create_body(), ""));
2889 let mut ctx = env.ctx();
2890 let mut text_cache = LayoutCache::new();
2891 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2892
2893 let r = calc
2894 .calculate_block_intrinsic_sizes(&tree, 0, &[])
2895 .expect("valid tree");
2896 assert_eq!(r.min_content_width, 0.0);
2897 assert_eq!(r.max_content_width, 0.0);
2898 assert_eq!(r.min_content_height, 0.0);
2899 assert_eq!(r.max_content_height, 0.0);
2900 }
2901
2902 #[test]
2903 fn block_intrinsic_sizes_saturate_to_infinity_instead_of_overflowing() {
2904 let tree = block_parent_with_children(2);
2905 let mut env = Env::new(styled(Dom::create_body(), ""));
2906 let mut ctx = env.ctx();
2907 let mut text_cache = LayoutCache::new();
2908 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2909
2910 let huge = isz(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
2911 let children = [(1usize, huge), (2usize, huge)];
2912 let r = calc
2913 .calculate_block_intrinsic_sizes(&tree, 0, &children)
2914 .expect("valid tree");
2915 assert!(r.min_content_height.is_infinite() && r.min_content_height.is_sign_positive());
2917 assert_eq!(r.max_content_width, f32::MAX, "cross axis only takes a max");
2918 }
2919
2920 #[test]
2921 fn block_intrinsic_sizes_sanitize_nan_on_the_cross_axis() {
2922 let tree = block_parent_with_children(1);
2923 let mut env = Env::new(styled(Dom::create_body(), ""));
2924 let mut ctx = env.ctx();
2925 let mut text_cache = LayoutCache::new();
2926 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2927
2928 let nan = isz(f32::NAN, f32::NAN, f32::NAN, f32::NAN);
2929 let r = calc
2930 .calculate_block_intrinsic_sizes(&tree, 0, &[(1usize, nan)])
2931 .expect("valid tree");
2932 assert!(!r.min_content_width.is_nan() && r.min_content_width == 0.0);
2937 assert!(!r.max_content_width.is_nan() && r.max_content_width == 0.0);
2938 assert!(r.min_content_height.is_nan());
2939 }
2940
2941 #[test]
2942 fn block_intrinsic_sizes_reject_an_out_of_range_node_index() {
2943 let tree = block_parent_with_children(1);
2944 let mut env = Env::new(styled(Dom::create_body(), ""));
2945 let mut ctx = env.ctx();
2946 let mut text_cache = LayoutCache::new();
2947 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2948 let r = calc.calculate_block_intrinsic_sizes(&tree, usize::MAX, &[]);
2949 assert!(matches!(r, Err(LayoutError::InvalidTree)));
2950 }
2951
2952 fn flex_parent_with_children(n: usize) -> LayoutTree {
2957 parent_with_children(FormattingContext::Flex, n)
2958 }
2959
2960 #[test]
2961 fn flex_row_intrinsic_sizes_sum_the_main_axis_and_max_the_cross_axis() {
2962 let tree = flex_parent_with_children(2);
2963 let mut env = Env::new(styled(Dom::create_body(), ""));
2964 let mut ctx = env.ctx();
2965 let mut text_cache = LayoutCache::new();
2966 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2967
2968 let children = [(1usize, isz(10.0, 20.0, 5.0, 6.0)), (2usize, isz(30.0, 40.0, 7.0, 8.0))];
2969 let r = calc
2970 .calculate_flex_intrinsic_sizes(&tree, 0, &children)
2971 .expect("valid tree");
2972 assert_eq!(r.min_content_width, 40.0);
2975 assert_eq!(r.max_content_width, 60.0);
2976 assert_eq!(r.min_content_height, 7.0);
2977 assert_eq!(r.max_content_height, 8.0);
2978 }
2979
2980 #[test]
2981 fn flex_intrinsic_sizes_are_zero_when_no_child_intrinsics_are_supplied() {
2982 let tree = flex_parent_with_children(3);
2983 let mut env = Env::new(styled(Dom::create_body(), ""));
2984 let mut ctx = env.ctx();
2985 let mut text_cache = LayoutCache::new();
2986 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2987
2988 let r = calc
2989 .calculate_flex_intrinsic_sizes(&tree, 0, &[])
2990 .expect("valid tree");
2991 assert_eq!(r.min_content_width, 0.0);
2992 assert_eq!(r.max_content_width, 0.0);
2993 assert_eq!(r.min_content_height, 0.0);
2994 assert_eq!(r.max_content_height, 0.0);
2995 }
2996
2997 #[test]
2998 fn flex_intrinsic_sizes_saturate_on_a_summing_overflow() {
2999 let tree = flex_parent_with_children(2);
3000 let mut env = Env::new(styled(Dom::create_body(), ""));
3001 let mut ctx = env.ctx();
3002 let mut text_cache = LayoutCache::new();
3003 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3004
3005 let huge = isz(f32::MAX, f32::MAX, 1.0, 2.0);
3006 let children = [(1usize, huge), (2usize, huge)];
3007 let r = calc
3008 .calculate_flex_intrinsic_sizes(&tree, 0, &children)
3009 .expect("valid tree");
3010 assert!(r.min_content_width.is_infinite() && r.min_content_width.is_sign_positive());
3011 assert!(r.max_content_width.is_infinite() && r.max_content_width.is_sign_positive());
3012 assert_eq!(r.max_content_height, 2.0);
3014 }
3015
3016 #[test]
3017 fn flex_intrinsic_sizes_reject_an_out_of_range_node_index() {
3018 let tree = flex_parent_with_children(1);
3019 let mut env = Env::new(styled(Dom::create_body(), ""));
3020 let mut ctx = env.ctx();
3021 let mut text_cache = LayoutCache::new();
3022 let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3023 let r = calc.calculate_flex_intrinsic_sizes(&tree, usize::MAX, &[]);
3024 assert!(matches!(r, Err(LayoutError::InvalidTree)));
3025 }
3026
3027 fn table_tree() -> LayoutTree {
3033 let bp = zero_props();
3034 tree_of(
3035 vec![
3036 hot(None, FormattingContext::Table, &bp),
3037 hot(Some(0), FormattingContext::TableRow, &bp),
3038 hot(Some(1), FormattingContext::TableCell, &bp),
3039 hot(Some(1), FormattingContext::TableCell, &bp),
3040 ],
3041 &[vec![1], vec![2, 3], vec![], vec![]],
3042 )
3043 }
3044
3045 #[test]
3046 fn table_intrinsic_sizes_sum_columns_and_stack_row_heights() {
3047 let tree = table_tree();
3048 let mut env = Env::new(styled(Dom::create_body(), ""));
3049 let mut ctx = env.ctx();
3050 let mut text_cache = LayoutCache::new();
3051 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3052
3053 let cells = [(2usize, isz(30.0, 50.0, 10.0, 20.0)), (3usize, isz(40.0, 60.0, 10.0, 15.0))];
3055 let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &cells);
3056 assert_eq!(r.min_content_width, 70.0, "sum of per-column minima");
3057 assert_eq!(r.max_content_width, 110.0, "sum of per-column maxima");
3058 assert_eq!(r.min_content_height, 20.0, "row height = tallest cell");
3059 assert_eq!(r.max_content_height, 20.0);
3060 }
3061
3062 #[test]
3063 fn table_intrinsic_sizes_are_zero_when_cells_carry_no_measurable_content() {
3064 let tree = table_tree();
3068 let mut env = Env::new(styled(Dom::create_body(), ""));
3069 let mut ctx = env.ctx();
3070 let mut text_cache = LayoutCache::new();
3071 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3072
3073 let rows = [(1usize, isz(1.0, 2.0, 3.0, 4.0))];
3074 let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &rows);
3075 assert_eq!(r.min_content_width, 0.0);
3076 assert_eq!(r.max_content_width, 0.0);
3077 assert_eq!(r.max_content_height, 0.0);
3078 }
3079
3080 #[test]
3081 fn table_intrinsic_sizes_of_a_table_without_rows_are_zero() {
3082 let bp = zero_props();
3085 let tree = tree_of(
3086 vec![
3087 hot(None, FormattingContext::Table, &bp),
3088 hot(Some(0), BLOCK, &bp),
3089 ],
3090 &[vec![1], vec![]],
3091 );
3092 let mut env = Env::new(styled(Dom::create_body(), ""));
3093 let mut ctx = env.ctx();
3094 let mut text_cache = LayoutCache::new();
3095 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3096
3097 let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &[(1usize, isz(9.0, 9.0, 9.0, 9.0))]);
3098 assert_eq!(r.min_content_width, 0.0);
3099 assert_eq!(r.max_content_width, 0.0);
3100 assert_eq!(r.min_content_height, 0.0);
3101 }
3102
3103 #[test]
3104 fn table_intrinsic_sizes_saturate_on_extreme_cell_widths() {
3105 let tree = table_tree();
3106 let mut env = Env::new(styled(Dom::create_body(), ""));
3107 let mut ctx = env.ctx();
3108 let mut text_cache = LayoutCache::new();
3109 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3110
3111 let huge = isz(f32::MAX, f32::MAX, 1.0, 1.0);
3112 let cells = [(2usize, huge), (3usize, huge)];
3113 let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &cells);
3114 assert!(r.min_content_width.is_infinite() && r.min_content_width.is_sign_positive());
3115 assert!(r.max_content_width.is_infinite() && r.max_content_width.is_sign_positive());
3116 assert_eq!(r.max_content_height, 1.0);
3117 }
3118
3119 #[test]
3120 fn table_intrinsic_sizes_with_an_out_of_range_index_are_zero() {
3121 let tree = table_tree();
3122 let mut env = Env::new(styled(Dom::create_body(), ""));
3123 let mut ctx = env.ctx();
3124 let mut text_cache = LayoutCache::new();
3125 let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3126
3127 let r = calc.calculate_table_intrinsic_sizes(&tree, usize::MAX, &[]);
3129 assert_eq!(r.min_content_width, 0.0);
3130 assert_eq!(r.max_content_height, 0.0);
3131 }
3132
3133 fn flex_dom() -> StyledDom {
3141 styled(
3142 Dom::create_body().with_child(div_class("flex").with_child(div_class("a"))),
3143 ".flex { display: flex; } .a { display: block; min-width: 120px; min-height: 30px; }",
3144 )
3145 }
3146
3147 #[test]
3148 fn calculate_intrinsic_sizes_is_a_no_op_when_nothing_is_dirty() {
3149 let mut env = Env::new(flex_dom());
3150 let mut ctx = env.ctx();
3151 let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3152 let mut text_cache = LayoutCache::new();
3153
3154 calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &BTreeSet::new())
3155 .expect("empty dirty set returns early");
3156 assert!(
3157 tree.warm.iter().all(|w| w.intrinsic_sizes.is_none()),
3158 "an empty dirty set must not compute anything"
3159 );
3160 }
3161
3162 #[test]
3163 fn calculate_intrinsic_sizes_applies_the_min_width_floor_bottom_up() {
3164 let mut env = Env::new(flex_dom());
3165 let mut ctx = env.ctx();
3166 let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3167 let mut text_cache = LayoutCache::new();
3168 let dirty: BTreeSet<usize> = (0..tree.nodes.len()).collect();
3169
3170 calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty).expect("sizing");
3171
3172 let a = layout_index(&tree, NodeId::new(2));
3176 let a_sizes = tree
3177 .warm(a)
3178 .and_then(|w| w.intrinsic_sizes)
3179 .expect("`.a` was measured");
3180 assert_eq!(a_sizes.min_content_width, 120.0);
3181 assert_eq!(a_sizes.max_content_width, 120.0);
3182 assert_eq!(a_sizes.min_content_height, 30.0);
3183 assert_eq!(a_sizes.max_content_height, 30.0);
3184
3185 let f = layout_index(&tree, NodeId::new(1));
3187 let f_sizes = tree
3188 .warm(f)
3189 .and_then(|w| w.intrinsic_sizes)
3190 .expect("`.flex` was measured");
3191 assert_eq!(f_sizes.min_content_width, 120.0);
3192 assert_eq!(f_sizes.max_content_width, 120.0);
3193 assert_eq!(f_sizes.max_content_height, 30.0);
3194 }
3195
3196 #[test]
3197 fn calculate_intrinsic_sizes_tolerates_bogus_dirty_node_indices() {
3198 let mut env = Env::new(flex_dom());
3199 let mut ctx = env.ctx();
3200 let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3201 let mut text_cache = LayoutCache::new();
3202 let dirty: BTreeSet<usize> = [0, 999, usize::MAX].into_iter().collect();
3205
3206 calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty)
3207 .expect("stale dirty ids must be ignored, not fatal");
3208 let root = tree.warm(tree.root).and_then(|w| w.intrinsic_sizes);
3209 assert!(root.is_some(), "the root is still measured");
3210 }
3211
3212 #[test]
3213 fn calculate_intrinsic_sizes_is_idempotent_across_repeated_passes() {
3214 let mut env = Env::new(flex_dom());
3215 let mut ctx = env.ctx();
3216 let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3217 let mut text_cache = LayoutCache::new();
3218 let dirty: BTreeSet<usize> = (0..tree.nodes.len()).collect();
3219
3220 calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty).expect("pass 1");
3221 let a = layout_index(&tree, NodeId::new(2));
3222 let first = tree.warm(a).and_then(|w| w.intrinsic_sizes).expect("measured");
3223
3224 calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty).expect("pass 2");
3225 let second = tree.warm(a).and_then(|w| w.intrinsic_sizes).expect("measured");
3226
3227 assert_eq!(first.min_content_width, second.min_content_width);
3228 assert_eq!(first.max_content_width, second.max_content_width);
3229 assert_eq!(first.min_content_height, second.min_content_height);
3230 assert_eq!(first.max_content_height, second.max_content_height);
3231 }
3232
3233 fn text_dom(text: &str) -> StyledDom {
3238 styled(
3239 Dom::create_body().with_child(div_class("p").with_child(Dom::create_text(text))),
3240 ".p { display: block; }",
3241 )
3242 }
3243
3244 fn collected_text(items: &[InlineContent]) -> String {
3245 items
3246 .iter()
3247 .filter_map(|item| match item {
3248 InlineContent::Text(run) => Some(run.text.as_str().to_string()),
3249 _ => None,
3250 })
3251 .collect()
3252 }
3253
3254 fn text_ifc_index(tree: &LayoutTree, text_dom: NodeId, block_dom: NodeId) -> usize {
3259 tree.dom_to_layout
3260 .get(&text_dom)
3261 .and_then(|v| v.first())
3262 .copied()
3263 .unwrap_or_else(|| layout_index(tree, block_dom))
3264 }
3265
3266 #[test]
3267 fn collect_inline_content_gathers_the_text_of_an_ifc_root() {
3268 let mut env = Env::new(text_dom("hello world"));
3269 let mut ctx = env.ctx();
3270 let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3271 let idx = text_ifc_index(&tree, NodeId::new(2), NodeId::new(1));
3272
3273 let items = collect_inline_content(&mut ctx, &tree, idx).expect("collect");
3274 assert!(!items.is_empty(), "the IFC root must see its text");
3275 assert!(collected_text(&items).contains("hello"));
3276 }
3277
3278 #[test]
3279 fn collect_inline_content_preserves_unicode_verbatim() {
3280 let needle = "e\u{301}llo مرحبا 👨\u{200d}👩\u{200d}👧";
3283 let mut env = Env::new(text_dom(needle));
3284 let mut ctx = env.ctx();
3285 let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3286 let idx = text_ifc_index(&tree, NodeId::new(2), NodeId::new(1));
3287
3288 let items = collect_inline_content(&mut ctx, &tree, idx).expect("collect");
3289 let text = collected_text(&items);
3290 assert!(text.contains('\u{301}'), "combining acute survived");
3291 assert!(text.contains("مرحبا"), "RTL run survived");
3292 assert!(text.contains("👨\u{200d}👩\u{200d}👧"), "ZWJ sequence survived");
3293 }
3294
3295 #[test]
3296 fn collect_inline_content_handles_whitespace_only_and_very_long_text() {
3297 for text in [" \n\t".to_string(), "x".repeat(20_000)] {
3298 let mut env = Env::new(text_dom(&text));
3299 let mut ctx = env.ctx();
3300 let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3301 let idx = text_ifc_index(&tree, NodeId::new(2), NodeId::new(1));
3302 let items = collect_inline_content(&mut ctx, &tree, idx)
3303 .expect("degenerate text must still collect");
3304 assert!(
3307 collected_text(&items).len() <= text.len(),
3308 "the same text run was collected more than once"
3309 );
3310 }
3311 }
3312
3313 #[test]
3314 fn collect_inline_content_rejects_an_out_of_range_root_index() {
3315 let mut env = Env::new(text_dom("hello"));
3316 let mut ctx = env.ctx();
3317 let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3318
3319 for bogus in [tree.nodes.len(), 999, usize::MAX] {
3320 let r = collect_inline_content(&mut ctx, &tree, bogus);
3321 assert!(
3322 matches!(r, Err(LayoutError::InvalidTree)),
3323 "index {bogus} must be rejected, not panic"
3324 );
3325 }
3326 }
3327
3328 #[test]
3329 fn collect_inline_content_of_a_text_free_subtree_is_empty() {
3330 let mut env = Env::new(flex_dom());
3331 let mut ctx = env.ctx();
3332 let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3333 let a = layout_index(&tree, NodeId::new(2));
3334
3335 let items = collect_inline_content(&mut ctx, &tree, a).expect("collect");
3336 assert!(
3337 collected_text(&items).is_empty(),
3338 "a childless block has no inline text"
3339 );
3340 }
3341
3342 #[test]
3347 fn subtree_contains_text_sees_the_node_itself_and_its_descendants() {
3348 let dom = text_dom("hi");
3349 assert!(subtree_contains_text(&dom, NodeId::new(2)), "the text node itself");
3351 assert!(subtree_contains_text(&dom, NodeId::new(1)), "its parent");
3352 assert!(subtree_contains_text(&dom, NodeId::new(0)), "the root");
3353 }
3354
3355 #[test]
3356 fn subtree_contains_text_is_false_for_a_text_free_subtree() {
3357 let dom = styled(
3358 Dom::create_body().with_child(div_class("a").with_child(div_class("b"))),
3359 "",
3360 );
3361 assert!(!subtree_contains_text(&dom, NodeId::new(0)));
3362 assert!(!subtree_contains_text(&dom, NodeId::new(1)));
3363 assert!(!subtree_contains_text(&dom, NodeId::new(2)));
3364 }
3365
3366 #[test]
3367 fn subtree_contains_text_walks_a_deeply_nested_subtree() {
3368 const DEPTH: usize = 200;
3370 let mut inner = Dom::create_div().with_child(Dom::create_text("deep"));
3371 for _ in 0..DEPTH {
3372 inner = Dom::create_div().with_child(inner);
3373 }
3374 let dom = styled(Dom::create_body().with_child(inner), "");
3375 assert!(subtree_contains_text(&dom, NodeId::new(0)));
3376
3377 let mut empty = Dom::create_div();
3378 for _ in 0..DEPTH {
3379 empty = Dom::create_div().with_child(empty);
3380 }
3381 let dom = styled(Dom::create_body().with_child(empty), "");
3382 assert!(!subtree_contains_text(&dom, NodeId::new(0)));
3383 }
3384
3385 #[test]
3390 fn extract_text_from_node_round_trips_the_exact_string() {
3391 for needle in [
3392 "hello world",
3393 " \n\t",
3394 "e\u{301}llo مرحبا 👨\u{200d}👩\u{200d}👧",
3395 "line1\nline2\r\n\u{0}nul",
3396 ] {
3397 let dom = text_dom(needle);
3398 assert_eq!(
3399 extract_text_from_node(&dom, NodeId::new(2)).as_deref(),
3400 Some(needle),
3401 "text must survive the DOM round-trip byte for byte"
3402 );
3403 }
3404 }
3405
3406 #[test]
3407 fn extract_text_from_node_is_none_for_non_text_nodes() {
3408 let dom = text_dom("hello");
3409 assert_eq!(extract_text_from_node(&dom, NodeId::new(0)), None, "body");
3410 assert_eq!(extract_text_from_node(&dom, NodeId::new(1)), None, "div");
3411 }
3412
3413 #[test]
3414 fn extract_text_from_node_handles_a_very_long_string() {
3415 let long = "ü".repeat(50_000);
3416 let dom = text_dom(&long);
3417 let got = extract_text_from_node(&dom, NodeId::new(2)).expect("text node");
3418 assert_eq!(got.chars().count(), 50_000);
3419 assert_eq!(got, long);
3420 }
3421
3422 fn constraints_dom() -> StyledDom {
3430 styled(
3431 Dom::create_body()
3432 .with_child(div_class("plain"))
3433 .with_child(div_class("pct"))
3434 .with_child(div_class("clamped"))
3435 .with_child(div_class("maxed"))
3436 .with_child(div_class("pctmin"))
3437 .with_child(div_class("bbox"))
3438 .with_child(div_class("autoblock"))
3439 .with_child(div_class("vwmin"))
3440 .with_child(div_class("em"))
3441 .with_child(div_class("hclamped"))
3442 .with_child(div_class("row10")),
3443 "
3444 .plain { display: block; width: 50px; height: 20px; }
3445 .pct { display: block; width: 50%; height: 25%; }
3446 .clamped { display: block; width: 300px; min-width: 200px; max-width: 100px; }
3447 .maxed { display: block; width: 300px; max-width: 100px; }
3448 .pctmin { display: block; min-width: 50%; }
3449 .bbox { display: block; width: 5px; height: 5px; box-sizing: border-box; }
3450 .autoblock { display: block; }
3451 .vwmin { display: block; width: 300px; min-width: 10vw; }
3452 .em { display: block; font-size: 20px; min-width: 3em; }
3453 .hclamped { display: block; height: 300px; min-height: 200px; max-height: 100px; }
3454 .row10 { display: block; min-width: 200px; max-height: 50px; }
3455 ",
3456 )
3457 }
3458
3459 const PLAIN: NodeId = NodeId::new(1);
3460 const PCT: NodeId = NodeId::new(2);
3461 const CLAMPED: NodeId = NodeId::new(3);
3462 const MAXED: NodeId = NodeId::new(4);
3463 const PCTMIN: NodeId = NodeId::new(5);
3464 const BBOX: NodeId = NodeId::new(6);
3465 const AUTOBLOCK: NodeId = NodeId::new(7);
3466 const VWMIN: NodeId = NodeId::new(8);
3467 const EM: NodeId = NodeId::new(9);
3468 const HCLAMPED: NodeId = NodeId::new(10);
3469 const ROW10: NodeId = NodeId::new(11);
3470
3471 fn node_state(dom: &StyledDom, id: NodeId) -> StyledNodeState {
3472 dom.styled_nodes.as_container()[id]
3473 .styled_node_state
3474 }
3475
3476 fn used_size(
3477 dom: &StyledDom,
3478 id: NodeId,
3479 cb: LogicalSize,
3480 bp: &BoxProps,
3481 ) -> LogicalSize {
3482 calculate_used_size_for_node(
3483 dom,
3484 Some(id),
3485 &cb,
3486 IntrinsicSizes::default(),
3487 bp,
3488 &VIEWPORT,
3489 )
3490 .expect("used size")
3491 }
3492
3493 #[test]
3494 fn used_size_of_an_anonymous_box_fills_the_cb_inline_and_uses_content_height() {
3495 let dom = constraints_dom();
3496 let cb = size(800.0, 600.0);
3497 let bp = zero_props();
3498
3499 let r = calculate_used_size_for_node(&dom, None, &cb, isz(0.0, 0.0, 0.0, 42.0), &bp, &VIEWPORT)
3500 .expect("anonymous box");
3501 assert_eq!(r.width, 800.0);
3502 assert_eq!(r.height, 42.0);
3503
3504 let r = calculate_used_size_for_node(&dom, None, &cb, isz(0.0, 0.0, 0.0, -5.0), &bp, &VIEWPORT)
3507 .expect("anonymous box");
3508 assert_eq!(r.height, 0.0);
3509 }
3510
3511 #[test]
3512 fn used_size_resolves_absolute_lengths_and_adds_the_content_box_extras() {
3513 let dom = constraints_dom();
3514 let cb = size(800.0, 600.0);
3515
3516 let r = used_size(&dom, PLAIN, cb, &zero_props());
3517 assert_eq!(r.width, 50.0);
3518 assert_eq!(r.height, 20.0);
3519
3520 let r = used_size(&dom, PLAIN, cb, &props(0.0, 2.0, 10.0));
3522 assert_eq!(r.width, 50.0 + 2.0 * (2.0 + 10.0));
3523 assert_eq!(r.height, 20.0 + 2.0 * (2.0 + 10.0));
3524 }
3525
3526 #[test]
3527 fn used_size_resolves_percentages_against_the_physical_containing_block() {
3528 let dom = constraints_dom();
3529 let r = used_size(&dom, PCT, size(800.0, 600.0), &zero_props());
3530 assert_eq!(r.width, 400.0, "50% of the CB width");
3531 assert_eq!(r.height, 150.0, "25% of the CB height");
3532 }
3533
3534 #[test]
3535 fn used_size_percentages_against_degenerate_containing_blocks_never_produce_nan() {
3536 let dom = constraints_dom();
3537 let bp = zero_props();
3538 for cb in [
3539 size(f32::NAN, f32::NAN),
3540 size(-800.0, -600.0),
3541 size(0.0, 0.0),
3542 size(f32::NEG_INFINITY, f32::NEG_INFINITY),
3543 ] {
3544 let r = used_size(&dom, PCT, cb, &bp);
3545 assert!(!r.width.is_nan() && !r.height.is_nan(), "NaN for cb={cb:?}");
3546 assert!(r.width >= 0.0 && r.height >= 0.0, "negative size for cb={cb:?}");
3547 }
3548 let r = used_size(&dom, PCT, size(f32::INFINITY, f32::INFINITY), &bp);
3550 assert!(r.width.is_infinite() && r.width.is_sign_positive());
3551 }
3552
3553 #[test]
3554 fn used_size_min_width_overrides_max_width_when_they_conflict() {
3555 let dom = constraints_dom();
3557 let cb = size(800.0, 600.0);
3558 assert_eq!(used_size(&dom, CLAMPED, cb, &zero_props()).width, 200.0);
3559 assert_eq!(used_size(&dom, MAXED, cb, &zero_props()).width, 100.0);
3561 }
3562
3563 #[test]
3564 fn used_size_min_height_overrides_max_height_when_they_conflict() {
3565 let dom = constraints_dom();
3566 let cb = size(800.0, 600.0);
3567 assert_eq!(used_size(&dom, HCLAMPED, cb, &zero_props()).height, 200.0);
3568 }
3569
3570 #[test]
3571 fn used_size_border_box_floors_at_the_padding_plus_border_sum() {
3572 let dom = constraints_dom();
3575 let r = used_size(&dom, BBOX, size(800.0, 600.0), &props(0.0, 0.0, 10.0));
3576 assert_eq!(r.width, 20.0);
3577 assert_eq!(r.height, 20.0);
3578
3579 let r = used_size(&dom, BBOX, size(800.0, 600.0), &zero_props());
3581 assert_eq!(r.width, 5.0);
3582 assert_eq!(r.height, 5.0);
3583 }
3584
3585 #[test]
3586 fn used_size_auto_width_block_fills_the_cb_minus_its_box_model() {
3587 let dom = constraints_dom();
3588 let cb = size(800.0, 600.0);
3589
3590 let r = used_size(&dom, AUTOBLOCK, cb, &props(100.0, 0.0, 0.0));
3591 assert_eq!(r.width, 600.0, "800 - 2*100 margin");
3592 assert_eq!(r.height, 0.0, "auto block height is filled in after layout");
3593
3594 let r = used_size(&dom, AUTOBLOCK, size(10.0, 600.0), &props(100.0, 0.0, 0.0));
3596 assert_eq!(r.width, 0.0);
3597 }
3598
3599 fn width_constrained(dom: &StyledDom, id: NodeId, tentative: f32, cb_width: f32) -> f32 {
3604 let state = node_state(dom, id);
3605 apply_width_constraints(dom, id, &state, tentative, cb_width, &zero_props())
3606 }
3607
3608 fn height_constrained(dom: &StyledDom, id: NodeId, tentative: f32, cb_height: f32) -> f32 {
3609 let state = node_state(dom, id);
3610 apply_height_constraints(dom, id, &state, tentative, cb_height, &zero_props())
3611 }
3612
3613 #[test]
3614 fn width_constraints_are_the_identity_without_min_or_max() {
3615 let dom = constraints_dom();
3616 for tentative in [0.0, 42.0, f32::MAX] {
3617 assert_eq!(width_constrained(&dom, PLAIN, tentative, 800.0), tentative);
3618 }
3619 }
3620
3621 #[test]
3622 fn width_constraints_clamp_then_let_min_win_over_max() {
3623 let dom = constraints_dom();
3624 assert_eq!(width_constrained(&dom, MAXED, 300.0, 800.0), 100.0, "max clamps");
3625 assert_eq!(width_constrained(&dom, MAXED, 50.0, 800.0), 50.0, "below max: untouched");
3626 assert_eq!(
3627 width_constrained(&dom, CLAMPED, 300.0, 800.0),
3628 200.0,
3629 "min-width overrides max-width per §10.4"
3630 );
3631 }
3632
3633 #[test]
3634 fn width_constraints_resolve_percentage_minimums_against_the_containing_block() {
3635 let dom = constraints_dom();
3636 assert_eq!(width_constrained(&dom, PCTMIN, 10.0, 800.0), 400.0);
3637 assert_eq!(width_constrained(&dom, PCTMIN, 10.0, -800.0), 10.0);
3639 let r = width_constrained(&dom, PCTMIN, 10.0, f32::NAN);
3641 assert!(!r.is_nan() && r == 10.0);
3642 }
3643
3644 #[test]
3645 fn width_constraints_resolve_em_minimums_against_the_elements_own_font_size() {
3646 let dom = constraints_dom();
3648 assert_eq!(width_constrained(&dom, EM, 10.0, 800.0), 60.0);
3649 }
3650
3651 #[test]
3652 fn width_constraints_never_return_nan() {
3653 let dom = constraints_dom();
3656 assert_eq!(width_constrained(&dom, PLAIN, f32::NAN, 800.0), 0.0);
3657 assert_eq!(width_constrained(&dom, MAXED, f32::NAN, 800.0), 100.0);
3662 assert_eq!(width_constrained(&dom, CLAMPED, f32::NAN, 800.0), 200.0);
3663 }
3664
3665 #[test]
3666 fn width_constraints_handle_infinite_tentative_widths() {
3667 let dom = constraints_dom();
3668 assert_eq!(width_constrained(&dom, MAXED, f32::INFINITY, 800.0), 100.0);
3669 assert!(width_constrained(&dom, PLAIN, f32::INFINITY, 800.0).is_infinite());
3672 assert_eq!(width_constrained(&dom, CLAMPED, f32::NEG_INFINITY, 800.0), 200.0);
3673 }
3674
3675 #[test]
3676 fn width_constraints_ignore_viewport_unit_minimums() {
3677 let dom = constraints_dom();
3683 assert_eq!(width_constrained(&dom, VWMIN, 300.0, 800.0), 300.0);
3684 assert_eq!(
3685 width_constrained(&dom, VWMIN, 10.0, 800.0),
3686 10.0,
3687 "10vw (= 80px on an 800px viewport) does not floor the width"
3688 );
3689 }
3690
3691 #[test]
3692 fn height_constraints_clamp_then_let_min_win_over_max() {
3693 let dom = constraints_dom();
3694 assert_eq!(height_constrained(&dom, HCLAMPED, 300.0, 600.0), 200.0);
3695 assert_eq!(height_constrained(&dom, PLAIN, 42.0, 600.0), 42.0);
3696 }
3697
3698 #[test]
3699 fn height_constraints_never_return_nan() {
3700 let dom = constraints_dom();
3701 assert_eq!(height_constrained(&dom, PLAIN, f32::NAN, 600.0), 0.0);
3702 assert_eq!(height_constrained(&dom, HCLAMPED, f32::NAN, 600.0), 200.0);
3703 }
3704
3705 #[test]
3706 fn height_constraints_are_stable_at_the_f32_boundaries() {
3707 let dom = constraints_dom();
3708 assert_eq!(height_constrained(&dom, HCLAMPED, f32::MAX, 600.0), 200.0);
3709 assert_eq!(height_constrained(&dom, HCLAMPED, f32::MIN, 600.0), 200.0);
3710 assert_eq!(height_constrained(&dom, PLAIN, f32::MIN, 600.0), 0.0);
3713 assert_eq!(height_constrained(&dom, PLAIN, -1.0, 600.0), 0.0);
3714 assert!(height_constrained(&dom, PLAIN, f32::MAX, 600.0).is_finite());
3715 }
3716
3717 fn cvt(dom: &StyledDom, id: NodeId, w: f32, h: f32) -> (f32, f32) {
3722 let state = node_state(dom, id);
3723 apply_constraint_violation_table(dom, id, &state, w, h, 800.0, 600.0, &zero_props())
3724 }
3725
3726 #[test]
3727 fn constraint_violation_table_row1_leaves_an_unviolated_box_alone() {
3728 let dom = constraints_dom();
3729 assert_eq!(cvt(&dom, PLAIN, 200.0, 100.0), (200.0, 100.0));
3730 }
3731
3732 #[test]
3733 fn constraint_violation_table_row2_preserves_the_aspect_ratio_under_max_width() {
3734 let dom = constraints_dom();
3736 assert_eq!(cvt(&dom, MAXED, 200.0, 100.0), (100.0, 50.0));
3737 }
3738
3739 #[test]
3740 fn constraint_violation_table_row10_pins_min_width_and_max_height_together() {
3741 let dom = constraints_dom();
3744 assert_eq!(cvt(&dom, ROW10, 100.0, 100.0), (200.0, 50.0));
3745 }
3746
3747 #[test]
3748 fn constraint_violation_table_guards_against_division_by_zero() {
3749 let dom = constraints_dom();
3751 assert_eq!(cvt(&dom, MAXED, 0.0, 100.0), (0.0, 100.0));
3752 assert_eq!(cvt(&dom, MAXED, 200.0, 0.0), (100.0, 0.0));
3753 assert_eq!(cvt(&dom, MAXED, 0.0, 0.0), (0.0, 0.0));
3754 assert_eq!(cvt(&dom, MAXED, -50.0, -50.0), (0.0, 0.0), "negatives clamp up to 0");
3755 }
3756
3757 #[test]
3758 fn constraint_violation_table_survives_extreme_ratios() {
3759 let dom = constraints_dom();
3762 let (w, h) = cvt(&dom, MAXED, f32::MAX, f32::MIN_POSITIVE);
3763 assert_eq!(w, 100.0, "max-width still clamps");
3764 assert!(h.is_finite() && h >= 0.0, "scaled height stays finite: {h}");
3765
3766 let (w, h) = cvt(&dom, MAXED, f32::MIN_POSITIVE, f32::MAX);
3767 assert!(w.is_finite() && w >= 0.0, "w={w}");
3768 assert!(h.is_finite() && h >= 0.0, "h={h}");
3769 }
3770
3771 #[test]
3772 fn constraint_violation_table_is_idempotent() {
3773 let dom = constraints_dom();
3776 for (id, w, h) in [
3777 (MAXED, 200.0_f32, 100.0_f32),
3778 (ROW10, 100.0, 100.0),
3779 (PLAIN, 200.0, 100.0),
3780 ] {
3781 let first = cvt(&dom, id, w, h);
3782 let second = cvt(&dom, id, first.0, first.1);
3783 assert_eq!(first, second, "not a fixed point for {id:?}");
3784 }
3785 }
3786}