1use std::{
3 cell::Cell,
4 collections::BTreeMap,
5 hash::{Hash, Hasher},
6 sync::Arc,
7};
8
9use azul_core::diff::NodeDataFingerprint;
10
11use crate::text3::cache::UnifiedConstraints;
12
13thread_local! {
14 static IFC_ID_COUNTER: Cell<u32> = const { Cell::new(0) };
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct IfcId(pub u32);
40
41impl IfcId {
42 #[must_use] pub fn unique() -> Self {
44 IFC_ID_COUNTER.with(|c| {
45 let v = c.get();
46 c.set(v.wrapping_add(1));
47 Self(v)
48 })
49 }
50
51 pub fn reset_counter() {
53 IFC_ID_COUNTER.with(|c| c.set(0));
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct IfcMembership {
86 pub ifc_id: IfcId,
88 pub ifc_root_layout_index: usize,
91 pub run_index: u32,
94}
95
96use azul_core::{
97 dom::{FormattingContext, NodeData, NodeId, NodeType},
98 geom::{LogicalPosition, LogicalRect, LogicalSize},
99 styled_dom::StyledDom,
100};
101use azul_css::{
102 corety::LayoutDebugMessage,
103 css::CssPropertyValue,
104 codegen::format::GetHash,
105 props::{
106 basic::{
107 pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
108 },
109 layout::{
110 LayoutDisplay, LayoutFloat, LayoutHeight, LayoutMaxHeight, LayoutMaxWidth,
111 LayoutMinHeight, LayoutMinWidth, LayoutOverflow, LayoutPosition, LayoutWidth,
112 LayoutWritingMode,
113 },
114 property::{CssProperty, CssPropertyType},
115 style::{StyleTextAlign, StyleWhiteSpace},
116 },
117};
118use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
119
120#[cfg(feature = "text_layout")]
121use crate::text3;
122use crate::{
123 debug_log,
124 font::parsed::ParsedFont,
125 font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
126 solver3::{
127 geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
128 getters::{
129 get_css_height, get_css_max_height, get_css_max_width, get_css_min_height,
130 get_css_min_width, get_css_width, get_direction_property as get_direction,
131 get_display_property, get_float, get_overflow_x,
132 get_overflow_y, get_position, get_text_align,
133 get_text_orientation_property as get_text_orientation,
134 get_white_space_property, get_writing_mode, MultiValue,
135 },
136 scrollbar::ScrollbarRequirements,
137 LayoutContext, Result,
138 },
139 text3::cache::AvailableSpace,
140};
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
153pub enum DirtyFlag {
154 #[default]
156 None,
157 Paint,
160 Layout,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
169pub struct SubtreeHash(pub u64);
170
171#[derive(Copy, Debug, Clone)]
180pub struct InlineItemMetrics {
181 pub source_node_id: Option<NodeId>,
184 pub advance_width: f32,
186 pub line_height_contribution: f32,
188 pub can_break: bool,
191 pub line_index: u32,
193 pub x_offset: f32,
195}
196
197#[derive(Debug, Clone)]
216pub struct CachedInlineLayout {
217 pub layout: Arc<UnifiedLayout>,
219 pub available_width: AvailableSpace,
223 pub has_floats: bool,
226 pub constraints: Option<UnifiedConstraints>,
229 pub item_metrics: Vec<InlineItemMetrics>,
237 pub line_breaks: Option<crate::text3::cache::CachedLineBreaks>,
241 pub inline_content_hash: u64,
246}
247
248impl CachedInlineLayout {
249 #[must_use] pub fn new(
251 layout: Arc<UnifiedLayout>,
252 available_width: AvailableSpace,
253 has_floats: bool,
254 ) -> Self {
255 let item_metrics = Self::extract_item_metrics(&layout);
256 Self {
257 layout,
258 available_width,
259 has_floats,
260 constraints: None,
261 item_metrics,
262 line_breaks: None,
263 inline_content_hash: 0,
264 }
265 }
266
267 #[must_use] pub fn new_with_constraints(
269 layout: Arc<UnifiedLayout>,
270 available_width: AvailableSpace,
271 has_floats: bool,
272 constraints: UnifiedConstraints,
273 ) -> Self {
274 let item_metrics = Self::extract_item_metrics(&layout);
275 let available_width_px = match available_width {
276 AvailableSpace::Definite(w) => w,
277 _ => f32::MAX,
278 };
279 let line_breaks = Some(crate::text3::cache::extract_line_breaks(
280 &layout.items, available_width_px,
281 ));
282 Self {
283 layout,
284 available_width,
285 has_floats,
286 constraints: Some(constraints),
287 item_metrics,
288 line_breaks,
289 inline_content_hash: 0,
290 }
291 }
292
293 #[allow(clippy::cast_possible_truncation)] fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
301 use crate::text3::cache::{ShapedItem, get_item_vertical_metrics_approx};
302
303 layout.items.iter().map(|positioned_item| {
304 let bounds = positioned_item.item.bounds();
305 let (ascent, descent) = get_item_vertical_metrics_approx(&positioned_item.item);
306
307 let source_node_id = match &positioned_item.item {
308 ShapedItem::Cluster(c) => c.source_node_id,
309 ShapedItem::Object { .. }
313 | ShapedItem::CombinedBlock { .. }
314 | ShapedItem::Tab { .. }
315 | ShapedItem::Break { .. } => None,
316 };
317
318 let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
324
325 InlineItemMetrics {
326 source_node_id,
327 advance_width: bounds.width,
328 line_height_contribution: ascent + descent,
329 can_break,
330 line_index: positioned_item.line_index as u32,
331 x_offset: positioned_item.position.x,
332 }
333 }).collect()
334 }
335
336 #[must_use] pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
346 if new_has_floats && !self.has_floats {
353 return false;
354 }
355
356 if self.has_floats && !new_has_floats {
359 return self.width_constraint_matches(new_width);
361 }
362
363 self.width_constraint_matches(new_width)
365 }
366
367 const LAYOUT_WIDTH_EPSILON: f32 = 0.1;
371
372 #[allow(clippy::match_same_arms)] fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
375 match (self.available_width, new_width) {
376 (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
378 (old - new).abs() < Self::LAYOUT_WIDTH_EPSILON
379 }
380 (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
382 (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
384 _ => false,
386 }
387 }
388
389 #[must_use] pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
393 if new_has_floats && !self.has_floats {
395 return true;
396 }
397
398 !self.width_constraint_matches(new_width)
400 }
401
402 #[inline]
407 #[must_use] pub const fn get_layout(&self) -> &Arc<UnifiedLayout> {
408 &self.layout
409 }
410
411 #[inline]
416 #[must_use] pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
417 self.layout.clone()
418 }
419}
420
421#[derive(Debug, Clone)]
443#[repr(C)]
444pub struct LayoutNode {
445 pub box_props: BoxProps,
452 pub dom_node_id: Option<NodeId>,
455 pub children: Vec<usize>,
458 pub used_size: Option<LogicalSize>,
461 pub formatting_context: FormattingContext,
464 pub parent: Option<usize>,
467
468 pub intrinsic_sizes: Option<IntrinsicSizes>,
473 pub baseline: Option<f32>,
477 pub inline_layout_result: Option<CachedInlineLayout>,
490 pub scrollbar_info: Option<ScrollbarRequirements>,
494 pub relative_position: Option<LogicalPosition>,
497 pub overflow_content_size: Option<LogicalSize>,
502 pub taffy_cache: TaffyCache,
505 pub computed_style: ComputedLayoutStyle,
509 pub pseudo_element: Option<PseudoElement>,
512 pub escaped_top_margin: Option<f32>,
517 pub escaped_bottom_margin: Option<f32>,
522 pub parent_formatting_context: Option<FormattingContext>,
525 pub ifc_membership: Option<IfcMembership>,
530 pub containing_block_index: Option<usize>,
537
538 pub anonymous_type: Option<AnonymousBoxType>,
543 pub node_data_fingerprint: NodeDataFingerprint,
547 pub subtree_hash: SubtreeHash,
551 pub dirty_flag: DirtyFlag,
554 pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
558 pub ifc_id: Option<IfcId>,
562}
563
564#[derive(Debug, Clone, Default)]
572pub struct ComputedLayoutStyle {
573 pub display: LayoutDisplay,
575 pub position: LayoutPosition,
577 pub float: LayoutFloat,
579 pub overflow_x: LayoutOverflow,
581 pub overflow_y: LayoutOverflow,
583 pub writing_mode: azul_css::props::layout::LayoutWritingMode,
585 pub direction: azul_css::props::style::StyleDirection,
587 pub text_orientation: azul_css::props::style::effects::StyleTextOrientation,
589 pub width: Option<azul_css::props::layout::LayoutWidth>,
591 pub height: Option<azul_css::props::layout::LayoutHeight>,
593 pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
595 pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
597 pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
599 pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
601 pub text_align: azul_css::props::style::StyleTextAlign,
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub enum PseudoElement {
611 Marker,
613 Before,
615 After,
617}
618
619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623pub enum AnonymousBoxType {
624 InlineWrapper,
626 ListItemMarker,
629 TableWrapper,
631 TableRowGroup,
633 TableRow,
635 TableCell,
637}
638
639#[allow(missing_copy_implementations)]
651#[derive(Debug, Clone)]
652pub struct LayoutNodeHot {
653 pub box_props: crate::solver3::geometry::PackedBoxProps,
657 pub dom_node_id: Option<NodeId>,
659 pub used_size: Option<LogicalSize>,
661 pub formatting_context: FormattingContext,
663 pub parent: Option<usize>,
665}
666
667#[derive(Debug, Clone, Default)]
673pub struct LayoutNodeWarm {
674 pub intrinsic_sizes: Option<IntrinsicSizes>,
676 pub baseline: Option<f32>,
678 pub inline_layout_result: Option<CachedInlineLayout>,
680 pub scrollbar_info: Option<ScrollbarRequirements>,
682 pub relative_position: Option<LogicalPosition>,
684 pub overflow_content_size: Option<LogicalSize>,
686 pub taffy_cache: TaffyCache,
688 pub computed_style: ComputedLayoutStyle,
690 pub pseudo_element: Option<PseudoElement>,
692 pub escaped_top_margin: Option<f32>,
694 pub escaped_bottom_margin: Option<f32>,
696 pub parent_formatting_context: Option<FormattingContext>,
698 pub ifc_membership: Option<IfcMembership>,
700 pub containing_block_index: Option<usize>,
702}
703
704#[derive(Debug, Clone)]
709#[derive(Default)]
710pub struct LayoutNodeCold {
711 pub anonymous_type: Option<AnonymousBoxType>,
713 pub node_data_fingerprint: NodeDataFingerprint,
715 pub subtree_hash: SubtreeHash,
717 pub dirty_flag: DirtyFlag,
719 pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
721 pub ifc_id: Option<IfcId>,
723}
724
725
726impl LayoutNode {
727 #[must_use] pub fn split(self) -> (LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold) {
730 (
731 LayoutNodeHot {
732 box_props: crate::solver3::geometry::PackedBoxProps::pack(&self.box_props),
733 dom_node_id: self.dom_node_id,
734 used_size: self.used_size,
735 formatting_context: self.formatting_context,
736 parent: self.parent,
737 },
738 LayoutNodeWarm {
739 intrinsic_sizes: self.intrinsic_sizes,
740 baseline: self.baseline,
741 inline_layout_result: self.inline_layout_result,
742 scrollbar_info: self.scrollbar_info,
743 relative_position: self.relative_position,
744 overflow_content_size: self.overflow_content_size,
745 taffy_cache: self.taffy_cache,
746 computed_style: self.computed_style,
747 pseudo_element: self.pseudo_element,
748 escaped_top_margin: self.escaped_top_margin,
749 escaped_bottom_margin: self.escaped_bottom_margin,
750 parent_formatting_context: self.parent_formatting_context,
751 ifc_membership: self.ifc_membership,
752 containing_block_index: self.containing_block_index,
753 },
754 LayoutNodeCold {
755 anonymous_type: self.anonymous_type,
756 node_data_fingerprint: self.node_data_fingerprint,
757 subtree_hash: self.subtree_hash,
758 dirty_flag: self.dirty_flag,
759 unresolved_box_props: self.unresolved_box_props,
760 ifc_id: self.ifc_id,
761 },
762 )
763 }
764}
765
766#[derive(Debug, Clone)]
773pub struct LayoutTree {
774 pub nodes: Vec<LayoutNodeHot>,
776 pub warm: Vec<LayoutNodeWarm>,
778 pub cold: Vec<LayoutNodeCold>,
780 pub root: usize,
782 pub dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
789 pub children_arena: Vec<usize>,
791 pub children_offsets: Vec<(u32, u32)>,
793 pub subtree_needs_intrinsic: Vec<bool>,
807}
808
809#[derive(Copy, Debug, Clone, Default)]
811pub struct LayoutTreeMemoryReport {
812 pub node_count: usize,
813 pub hot_bytes: usize,
814 pub warm_bytes: usize,
815 pub warm_inline_layout_bytes: usize,
816 pub warm_taffy_cache_bytes: usize,
817 pub cold_bytes: usize,
818 pub dom_to_layout_bytes: usize,
819 pub children_arena_bytes: usize,
820 pub children_offsets_bytes: usize,
821}
822
823impl LayoutTreeMemoryReport {
824 #[must_use] pub const fn total_bytes(&self) -> usize {
825 self.hot_bytes
826 + self.warm_bytes
827 + self.warm_inline_layout_bytes
828 + self.warm_taffy_cache_bytes
829 + self.cold_bytes
830 + self.dom_to_layout_bytes
831 + self.children_arena_bytes
832 + self.children_offsets_bytes
833 }
834}
835
836impl LayoutTree {
837 #[must_use] pub fn memory_report(&self) -> LayoutTreeMemoryReport {
839 let mut report = LayoutTreeMemoryReport {
840 node_count: self.nodes.len(),
841 hot_bytes: self.nodes.capacity() * size_of::<LayoutNodeHot>(),
842 warm_bytes: self.warm.capacity() * size_of::<LayoutNodeWarm>(),
843 cold_bytes: self.cold.capacity() * size_of::<LayoutNodeCold>(),
844 children_arena_bytes: self.children_arena.capacity() * size_of::<usize>(),
845 children_offsets_bytes: self.children_offsets.capacity() * size_of::<(u32, u32)>(),
846 dom_to_layout_bytes: 0,
847 warm_inline_layout_bytes: 0,
848 warm_taffy_cache_bytes: 0,
849 };
850 let entries = self.dom_to_layout.len();
853 report.dom_to_layout_bytes = entries * (size_of::<NodeId>() + size_of::<Vec<usize>>());
854 for v in self.dom_to_layout.values() {
855 report.dom_to_layout_bytes += v.capacity() * size_of::<usize>();
856 }
857 for w in &self.warm {
860 if let Some(cached) = &w.inline_layout_result {
861 report.warm_inline_layout_bytes += size_of::<UnifiedLayout>();
863 report.warm_inline_layout_bytes += cached.layout.items.capacity()
864 * size_of::<crate::text3::cache::PositionedItem>();
865 report.warm_inline_layout_bytes += cached.item_metrics.capacity()
866 * size_of::<InlineItemMetrics>();
867 for item in &cached.layout.items {
870 if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
871 report.warm_inline_layout_bytes += c.glyphs.capacity()
872 * size_of::<crate::text3::cache::ShapedGlyph>();
873 report.warm_inline_layout_bytes += c.text.capacity();
874 }
875 }
876 }
877 report.warm_taffy_cache_bytes += size_of::<TaffyCache>();
879 }
880 report
881 }
882
883 #[inline]
885 #[must_use] pub fn children(&self, index: usize) -> &[usize] {
886 if let Some(&(start, len)) = self.children_offsets.get(index) {
887 &self.children_arena[(start as usize)..((start as usize) + (len as usize))]
888 } else {
889 &[]
890 }
891 }
892
893 #[inline]
895 #[must_use] pub fn get(&self, index: usize) -> Option<&LayoutNodeHot> {
896 self.nodes.get(index)
897 }
898
899 #[inline]
901 pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNodeHot> {
902 self.nodes.get_mut(index)
903 }
904
905 #[inline]
907 #[must_use] pub fn warm(&self, index: usize) -> Option<&LayoutNodeWarm> {
908 self.warm.get(index)
909 }
910
911 #[inline]
913 pub fn warm_mut(&mut self, index: usize) -> Option<&mut LayoutNodeWarm> {
914 self.warm.get_mut(index)
915 }
916
917 #[inline]
919 #[must_use] pub fn cold(&self, index: usize) -> Option<&LayoutNodeCold> {
920 self.cold.get(index)
921 }
922
923 #[inline]
925 pub fn cold_mut(&mut self, index: usize) -> Option<&mut LayoutNodeCold> {
926 self.cold.get_mut(index)
927 }
928
929 fn root_node(&self) -> &LayoutNodeHot {
930 &self.nodes[self.root]
931 }
932
933 #[must_use] pub fn get_full_node(&self, index: usize) -> Option<LayoutNode> {
937 let hot = self.nodes.get(index)?;
938 let warm = self.warm.get(index).cloned().unwrap_or_default();
939 let cold = self.cold.get(index).cloned().unwrap_or_default();
940 let children = self.children(index).to_vec();
941 Some(LayoutNode {
942 box_props: hot.box_props.unpack(),
943 dom_node_id: hot.dom_node_id,
944 children,
945 used_size: hot.used_size,
946 formatting_context: hot.formatting_context,
947 parent: hot.parent,
948 intrinsic_sizes: warm.intrinsic_sizes,
949 baseline: warm.baseline,
950 inline_layout_result: warm.inline_layout_result,
951 scrollbar_info: warm.scrollbar_info,
952 relative_position: warm.relative_position,
953 overflow_content_size: warm.overflow_content_size,
954 taffy_cache: warm.taffy_cache,
955 computed_style: warm.computed_style,
956 pseudo_element: warm.pseudo_element,
957 escaped_top_margin: warm.escaped_top_margin,
958 escaped_bottom_margin: warm.escaped_bottom_margin,
959 parent_formatting_context: warm.parent_formatting_context,
960 ifc_membership: warm.ifc_membership,
961 containing_block_index: warm.containing_block_index,
962 anonymous_type: cold.anonymous_type,
963 node_data_fingerprint: cold.node_data_fingerprint,
964 subtree_hash: cold.subtree_hash,
965 dirty_flag: cold.dirty_flag,
966 unresolved_box_props: cold.unresolved_box_props,
967 ifc_id: cold.ifc_id,
968 })
969 }
970
971 fn resolve_box_props(
973 &mut self,
974 node_index: usize,
975 containing_block: LogicalSize,
976 viewport_size: LogicalSize,
977 element_font_size: f32,
978 root_font_size: f32,
979 ) {
980 let params = crate::solver3::geometry::ResolutionParams {
981 containing_block,
982 viewport_size,
983 element_font_size,
984 root_font_size,
985 };
986 if let (Some(hot), Some(cold)) = (self.nodes.get_mut(node_index), self.cold.get(node_index)) {
987 hot.box_props = crate::solver3::geometry::PackedBoxProps::pack(&cold.unresolved_box_props.resolve(¶ms));
988 }
989 }
990
991 pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
993 if flag == DirtyFlag::None {
994 return;
995 }
996
997 let mut current_index = Some(start_index);
998 while let Some(index) = current_index {
999 let Some(cold) = self.cold.get_mut(index) else {
1000 break;
1001 };
1002 if cold.dirty_flag >= flag {
1003 break;
1004 }
1005 cold.dirty_flag = flag;
1006 current_index = self.nodes.get(index).and_then(|n| n.parent);
1007 }
1008 }
1009
1010 fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
1012 if flag == DirtyFlag::None {
1013 return;
1014 }
1015
1016 let mut stack = vec![start_index];
1017 while let Some(index) = stack.pop() {
1018 let children = self.children(index).to_vec();
1019 if let Some(cold) = self.cold.get_mut(index) {
1020 if cold.dirty_flag < flag {
1021 cold.dirty_flag = flag;
1022 }
1023 stack.extend_from_slice(&children);
1024 }
1025 }
1026 }
1027
1028 fn clear_all_dirty_flags(&mut self) {
1030 for cold in &mut self.cold {
1031 cold.dirty_flag = DirtyFlag::None;
1032 }
1033 }
1034
1035 #[must_use] pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&Arc<UnifiedLayout>> {
1037 let warm = self.warm.get(layout_index)?;
1038
1039 if let Some(cached) = &warm.inline_layout_result {
1041 return Some(cached.get_layout());
1042 }
1043
1044 if let Some(ifc_membership) = &warm.ifc_membership {
1046 let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
1047 if let Some(cached) = &ifc_root_warm.inline_layout_result {
1048 return Some(cached.get_layout());
1049 }
1050 }
1051
1052 None
1053 }
1054
1055 #[must_use] pub fn get_ifc_root_layout_index(&self, layout_index: usize) -> usize {
1062 if let Some(warm) = self.warm.get(layout_index) {
1063 if warm.inline_layout_result.is_none() {
1064 if let Some(ifc_membership) = &warm.ifc_membership {
1065 return ifc_membership.ifc_root_layout_index;
1066 }
1067 }
1068 }
1069 layout_index
1070 }
1071
1072 #[must_use] pub fn get_content_size(&self, index: usize) -> LogicalSize {
1074 let Some(warm) = self.warm.get(index) else {
1075 return LogicalSize::default();
1076 };
1077
1078 if let Some(content_size) = warm.overflow_content_size {
1079 return content_size;
1080 }
1081
1082 let Some(hot) = self.nodes.get(index) else {
1083 return LogicalSize::default();
1084 };
1085
1086 let mut content_size = hot.used_size.unwrap_or_default();
1087
1088 if let Some(ref cached_layout) = warm.inline_layout_result {
1089 let text_layout = &cached_layout.layout;
1090 let mut max_x: f32 = 0.0;
1091 let mut max_y: f32 = 0.0;
1092 for positioned_item in &text_layout.items {
1093 let item_bounds = positioned_item.item.bounds();
1094 max_x = max_x.max(positioned_item.position.x + item_bounds.width);
1095 max_y = max_y.max(positioned_item.position.y + item_bounds.height);
1096 }
1097 content_size.width = content_size.width.max(max_x);
1098 content_size.height = content_size.height.max(max_y);
1099 }
1100
1101 content_size
1102 }
1103}
1104
1105pub fn generate_layout_tree<T: ParsedFontTrait>(
1110 ctx: &mut LayoutContext<'_, T>,
1111) -> Result<LayoutTree> {
1112 let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
1113 let root_id = ctx
1114 .styled_dom
1115 .root
1116 .into_crate_internal()
1117 .unwrap_or(NodeId::ZERO);
1118 let root_index =
1119 builder.process_node(ctx.styled_dom, root_id, None, ctx.debug_messages)?;
1120 let mut layout_tree = builder.build(root_index);
1121
1122 layout_tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(ctx.styled_dom, &layout_tree);
1129
1130 debug_log!(
1131 ctx,
1132 "Generated layout tree with {} nodes (incl. anonymous)",
1133 layout_tree.nodes.len()
1134 );
1135
1136 Ok(layout_tree)
1137}
1138
1139pub(crate) fn is_shrink_to_fit_context(
1151 styled_dom: &StyledDom,
1152 dom_node_id: Option<NodeId>,
1153 fc: FormattingContext,
1154) -> bool {
1155 use crate::solver3::getters::{get_float, MultiValue};
1156 use crate::solver3::positioning::get_position_type;
1157 use azul_css::props::layout::{LayoutFloat, LayoutPosition};
1158
1159 match fc {
1160 FormattingContext::Flex
1161 | FormattingContext::Grid
1162 | FormattingContext::Table
1163 | FormattingContext::InlineBlock => return true,
1164 _ => {}
1165 }
1166 let Some(dom_id) = dom_node_id else { return false; };
1167 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1168 let float_val = match get_float(styled_dom, dom_id, node_state) {
1169 MultiValue::Exact(v) => v,
1170 _ => LayoutFloat::None,
1171 };
1172 if float_val != LayoutFloat::None {
1173 return true;
1174 }
1175 let pos = get_position_type(styled_dom, Some(dom_id));
1176 if pos == LayoutPosition::Absolute || pos == LayoutPosition::Fixed {
1177 return true;
1181 }
1182 false
1183}
1184
1185fn compute_subtree_needs_intrinsic(
1190 styled_dom: &StyledDom,
1191 tree: &LayoutTree,
1192) -> Vec<bool> {
1193 let n = tree.nodes.len();
1194 let mut out = vec![false; n];
1195 for idx in (0..n).rev() {
1196 let hot = &tree.nodes[idx];
1197 let self_stf = is_shrink_to_fit_context(styled_dom, hot.dom_node_id, hot.formatting_context);
1198 let mut any = self_stf;
1199 if !any {
1200 for &child in tree.children(idx) {
1201 if out.get(child).copied().unwrap_or(false) {
1202 any = true;
1203 break;
1204 }
1205 }
1206 }
1207 out[idx] = any;
1208 }
1209 out
1210}
1211
1212#[derive(Debug)]
1219pub struct LayoutTreeBuilder {
1220 nodes: Vec<LayoutNode>,
1221 dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
1222 viewport_size: LogicalSize,
1223}
1224
1225impl LayoutTreeBuilder {
1226 #[must_use] pub const fn new(viewport_size: LogicalSize) -> Self {
1227 Self {
1228 nodes: Vec::new(),
1229 dom_to_layout: BTreeMap::new(),
1230 viewport_size,
1231 }
1232 }
1233
1234 #[must_use] pub fn get(&self, index: usize) -> Option<&LayoutNode> {
1235 self.nodes.get(index)
1236 }
1237
1238 pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
1239 self.nodes.get_mut(index)
1240 }
1241
1242 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn process_node(
1248 &mut self,
1249 styled_dom: &StyledDom,
1250 dom_id: NodeId,
1251 parent_idx: Option<usize>,
1252 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1253 ) -> Result<usize> {
1254 let node_data = &styled_dom.node_data.as_container()[dom_id];
1255 let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages);
1256 let raw_display = get_display_type(styled_dom, dom_id);
1257
1258 let raw_display = if raw_display.is_layout_internal() && is_replaced_element(node_data) {
1262 LayoutDisplay::Inline
1263 } else {
1264 raw_display
1265 };
1266
1267 let node_position = self.nodes.get(node_idx).map(|n| n.computed_style.position).unwrap_or_default();
1277 let node_float = self.nodes.get(node_idx).map(|n| n.computed_style.float).unwrap_or_default();
1278 let is_absolute_or_fixed = matches!(node_position, LayoutPosition::Absolute | LayoutPosition::Fixed);
1279 let is_floated = node_float != LayoutFloat::None;
1280 let is_root = parent_idx.is_none();
1281
1282 if is_absolute_or_fixed && is_floated {
1284 if let Some(node) = self.nodes.get_mut(node_idx) {
1285 node.computed_style.float = LayoutFloat::None;
1286 }
1287 }
1288
1289 let is_flex_grid_child = parent_idx
1290 .and_then(|p| self.nodes.get(p).map(|n| matches!(n.formatting_context, FormattingContext::Flex | FormattingContext::Grid)))
1291 .unwrap_or(false);
1292
1293 let display_type = crate::solver3::getters::get_computed_display(
1294 raw_display, is_absolute_or_fixed, is_floated, is_root, is_flex_grid_child,
1295 );
1296
1297 if display_type != raw_display {
1299 if let Some(node) = self.nodes.get_mut(node_idx) {
1300 node.computed_style.display = display_type;
1301 node.formatting_context = determine_formatting_context_for_display(
1302 styled_dom, dom_id, display_type,
1303 );
1304 }
1305 }
1306
1307 if is_absolute_or_fixed {
1309 let cb_index = if matches!(node_position, LayoutPosition::Fixed) {
1310 None
1312 } else {
1313 let mut ancestor = parent_idx;
1315 loop {
1316 match ancestor {
1317 Some(idx) => {
1318 let pos = self.nodes.get(idx)
1319 .map(|n| n.computed_style.position)
1320 .unwrap_or_default();
1321 if pos.is_positioned() {
1322 break Some(idx);
1323 }
1324 ancestor = self.nodes.get(idx).and_then(|n| n.parent);
1325 }
1326 None => break None, }
1328 }
1329 };
1330 if let Some(node) = self.nodes.get_mut(node_idx) {
1331 node.containing_block_index = cb_index;
1332 }
1333 }
1334
1335 if parent_idx.is_none() {
1336 if let Some(node) = self.nodes.get_mut(node_idx) {
1337 if let FormattingContext::Block { ref mut establishes_new_context } = node.formatting_context {
1338 *establishes_new_context = true;
1339 }
1340 }
1341 }
1342
1343 if display_type == LayoutDisplay::ListItem {
1351 self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
1352 }
1353
1354 if display_type == LayoutDisplay::Contents && is_replaced_element(node_data) {
1378 if let Some(parent) = parent_idx {
1380 if let Some(p) = self.nodes.get_mut(parent) {
1381 p.children.retain(|&c| c != node_idx);
1382 }
1383 }
1384 if let Some(node) = self.nodes.get_mut(node_idx) {
1385 node.computed_style.display = LayoutDisplay::None;
1386 node.formatting_context = FormattingContext::None;
1387 }
1388 return Ok(node_idx);
1389 }
1390
1391 if display_type == LayoutDisplay::Contents {
1392 if let Some(parent) = parent_idx {
1394 if let Some(p) = self.nodes.get_mut(parent) {
1395 p.children.retain(|&c| c != node_idx);
1396 }
1397 }
1398 let effective_parent = parent_idx.unwrap_or(node_idx);
1400 for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1401 self.process_node(styled_dom, child_dom_id, Some(effective_parent), debug_messages)?;
1402 }
1403 return Ok(node_idx);
1404 }
1405
1406 match display_type {
1407 LayoutDisplay::Block
1408 | LayoutDisplay::InlineBlock
1409 | LayoutDisplay::FlowRoot
1410 | LayoutDisplay::ListItem => {
1411 self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?;
1412 }
1413 LayoutDisplay::Table | LayoutDisplay::InlineTable => {
1416 self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?;
1417 }
1418 LayoutDisplay::TableRowGroup
1419 | LayoutDisplay::TableHeaderGroup
1420 | LayoutDisplay::TableFooterGroup => {
1421 self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?;
1422 }
1423 LayoutDisplay::TableRow => {
1424 self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?;
1425 }
1426 LayoutDisplay::TableColumn => {
1427 }
1432 LayoutDisplay::TableColumnGroup => {
1433 for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1436 let child_display = get_display_type(styled_dom, child_dom_id);
1437 if child_display == LayoutDisplay::TableColumn {
1438 self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1439 }
1440 }
1442 }
1443 _ => {
1447 let children: Vec<NodeId> = dom_id
1455 .az_children(&styled_dom.node_hierarchy.as_container())
1456 .filter(|&child_id| {
1458 if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
1460 return false;
1461 }
1462 let node_data = &styled_dom.node_data.as_container()[child_id];
1464 if let NodeType::Text(text) = node_data.get_node_type() {
1465 return !text.as_str().trim().is_empty();
1467 }
1468 true
1469 })
1470 .collect();
1471
1472 let is_flex_or_grid = matches!(
1473 display_type,
1474 LayoutDisplay::Flex | LayoutDisplay::InlineFlex
1475 | LayoutDisplay::Grid | LayoutDisplay::InlineGrid
1476 );
1477
1478 for child_dom_id in children {
1479 let child_display = get_display_type(styled_dom, child_dom_id);
1485 if is_flex_or_grid && child_display.creates_table_context() {
1486 let wrapper_idx = self.create_anonymous_node(
1487 node_idx,
1488 AnonymousBoxType::TableWrapper,
1489 FormattingContext::Block { establishes_new_context: true },
1490 );
1491 self.process_node(styled_dom, child_dom_id, Some(wrapper_idx), debug_messages)?;
1492 } else {
1493 let child_idx = self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1494 if is_flex_or_grid {
1498 blockify_flex_item_if_table_internal(&mut self.nodes, child_idx);
1499 }
1500 }
1501 }
1502 }
1503 }
1504 Ok(node_idx)
1505 }
1506
1507 fn process_block_children(
1518 &mut self,
1519 styled_dom: &StyledDom,
1520 parent_dom_id: NodeId,
1521 parent_idx: usize,
1522 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1523 ) -> Result<()> {
1524 let children: Vec<NodeId> = parent_dom_id
1526 .az_children(&styled_dom.node_hierarchy.as_container())
1527 .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
1528 .collect();
1529
1530 if let Some(msgs) = debug_messages.as_mut() {
1532 msgs.push(LayoutDebugMessage::info(format!(
1533 "[process_block_children] DOM node {} has {} children: {:?}",
1534 parent_dom_id.index(),
1535 children.len(),
1536 children.iter().map(NodeId::index).collect::<Vec<_>>()
1537 )));
1538 }
1539
1540 let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
1541
1542 if let Some(msgs) = debug_messages.as_mut() {
1543 msgs.push(LayoutDebugMessage::info(format!(
1544 "[process_block_children] has_block_child={}, children display types: {:?}",
1545 has_block_child,
1546 children
1547 .iter()
1548 .map(|c| {
1549 let dt = get_display_type(styled_dom, *c);
1550 let is_block = is_block_level(styled_dom, *c);
1551 format!("{}:{:?}(block={})", c.index(), dt, is_block)
1552 })
1553 .collect::<Vec<_>>()
1554 )));
1555 }
1556
1557 if !has_block_child {
1558 if let Some(msgs) = debug_messages.as_mut() {
1560 msgs.push(LayoutDebugMessage::info(format!(
1561 "[process_block_children] All inline, processing {} children directly",
1562 children.len()
1563 )));
1564 }
1565 for child_id in children {
1566 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1567 }
1568 return Ok(());
1569 }
1570
1571 let mut inline_run = Vec::new();
1573
1574 for child_id in children {
1575 if is_block_level(styled_dom, child_id) {
1576 if !inline_run.is_empty() {
1584 self.flush_inline_run(styled_dom, parent_idx, &mut inline_run, debug_messages)?;
1585 }
1586 if let Some(msgs) = debug_messages.as_mut() {
1588 msgs.push(LayoutDebugMessage::info(format!(
1589 "[process_block_children] Processing block child DOM {}",
1590 child_id.index()
1591 )));
1592 }
1593 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1594 } else {
1595 inline_run.push(child_id);
1596 }
1597 }
1598 if !inline_run.is_empty() {
1600 self.flush_inline_run(styled_dom, parent_idx, &mut inline_run, debug_messages)?;
1601 }
1602
1603 Ok(())
1604 }
1605
1606 fn process_table_level_children(
1611 &mut self,
1612 styled_dom: &StyledDom,
1613 parent_dom_id: NodeId,
1614 parent_idx: usize,
1615 is_expected_child: fn(LayoutDisplay) -> bool,
1616 anon_type: AnonymousBoxType,
1617 anon_fc: FormattingContext,
1618 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1619 ) -> Result<()> {
1620 let parent_display = get_display_type(styled_dom, parent_dom_id);
1621 let mut non_matching_children = Vec::new();
1622
1623 for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1624 if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1625 continue;
1626 }
1627
1628 let child_display = get_display_type(styled_dom, child_id);
1629
1630 if is_expected_child(child_display) {
1631 if !non_matching_children.is_empty() {
1632 let anon_idx = self.create_anonymous_node(
1633 parent_idx,
1634 anon_type,
1635 anon_fc,
1636 );
1637 #[allow(clippy::iter_with_drain)] for np_id in non_matching_children.drain(..) {
1639 self.process_node(styled_dom, np_id, Some(anon_idx), debug_messages)?;
1640 }
1641 }
1642 self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1643 } else {
1644 non_matching_children.push(child_id);
1645 }
1646 }
1647
1648 if !non_matching_children.is_empty() {
1649 let anon_idx = self.create_anonymous_node(
1650 parent_idx,
1651 anon_type,
1652 anon_fc,
1653 );
1654 for np_id in non_matching_children {
1655 self.process_node(styled_dom, np_id, Some(anon_idx), debug_messages)?;
1656 }
1657 }
1658
1659 Ok(())
1660 }
1661
1662 fn process_table_children(
1663 &mut self,
1664 styled_dom: &StyledDom,
1665 parent_dom_id: NodeId,
1666 parent_idx: usize,
1667 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1668 ) -> Result<()> {
1669 self.process_table_level_children(
1670 styled_dom, parent_dom_id, parent_idx,
1671 is_proper_table_child,
1672 AnonymousBoxType::TableRow,
1673 FormattingContext::TableRow,
1674 debug_messages,
1675 )
1676 }
1677
1678 fn process_table_row_group_children(
1679 &mut self,
1680 styled_dom: &StyledDom,
1681 parent_dom_id: NodeId,
1682 parent_idx: usize,
1683 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1684 ) -> Result<()> {
1685 self.process_table_level_children(
1686 styled_dom, parent_dom_id, parent_idx,
1687 |d| d == LayoutDisplay::TableRow,
1688 AnonymousBoxType::TableRow,
1689 FormattingContext::TableRow,
1690 debug_messages,
1691 )
1692 }
1693
1694 fn process_table_row_children(
1695 &mut self,
1696 styled_dom: &StyledDom,
1697 parent_dom_id: NodeId,
1698 parent_idx: usize,
1699 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1700 ) -> Result<()> {
1701 self.process_table_level_children(
1702 styled_dom, parent_dom_id, parent_idx,
1703 |d| d == LayoutDisplay::TableCell,
1704 AnonymousBoxType::TableCell,
1705 FormattingContext::Block { establishes_new_context: true },
1706 debug_messages,
1707 )
1708 }
1709 fn flush_inline_run(
1712 &mut self,
1713 styled_dom: &StyledDom,
1714 parent_idx: usize,
1715 inline_run: &mut Vec<NodeId>,
1716 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1717 ) -> Result<()> {
1718 let all_whitespace = inline_run
1719 .iter()
1720 .all(|id| is_whitespace_only_text(styled_dom, *id));
1721 if all_whitespace {
1722 if let Some(msgs) = debug_messages.as_mut() {
1723 msgs.push(LayoutDebugMessage::info(format!(
1724 "[process_block_children] Skipping whitespace-only inline run: {:?}",
1725 inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
1726 )));
1727 }
1728 inline_run.clear();
1729 } else {
1730 if let Some(msgs) = debug_messages.as_mut() {
1731 msgs.push(LayoutDebugMessage::info(format!(
1732 "[process_block_children] Creating anon wrapper for inline run: {:?}",
1733 inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
1734 )));
1735 }
1736 let anon_idx = self.create_anonymous_node(
1737 parent_idx,
1738 AnonymousBoxType::InlineWrapper,
1739 FormattingContext::Block {
1740 establishes_new_context: true,
1741 },
1742 );
1743 for inline_child_id in inline_run.drain(..) {
1744 self.process_node(styled_dom, inline_child_id, Some(anon_idx), debug_messages)?;
1745 }
1746 }
1747 Ok(())
1748 }
1749
1750 pub fn create_anonymous_node(
1762 &mut self,
1763 parent: usize,
1764 anon_type: AnonymousBoxType,
1765 fc: FormattingContext,
1766 ) -> usize {
1767 let index = self.nodes.len();
1768
1769 let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context);
1771
1772 self.nodes.push(LayoutNode {
1773 box_props: BoxProps::default(),
1775 dom_node_id: None,
1776 children: Vec::new(),
1777 used_size: None,
1778 formatting_context: fc,
1779 parent: Some(parent),
1780 intrinsic_sizes: None,
1782 baseline: None,
1783 inline_layout_result: None,
1784 scrollbar_info: None,
1785 relative_position: None,
1786 overflow_content_size: None,
1787 taffy_cache: TaffyCache::new(),
1788 computed_style: ComputedLayoutStyle::default(),
1789 pseudo_element: None,
1790 escaped_top_margin: None,
1791 escaped_bottom_margin: None,
1792 parent_formatting_context: parent_fc,
1793 ifc_membership: None,
1794 containing_block_index: None,
1795 anonymous_type: Some(anon_type),
1797 node_data_fingerprint: NodeDataFingerprint::default(),
1798 subtree_hash: SubtreeHash(0),
1799 dirty_flag: DirtyFlag::Layout,
1800 unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1801 ifc_id: None,
1802 });
1803
1804 self.nodes[parent].children.push(index);
1805 index
1806 }
1807
1808 pub fn create_marker_pseudo_element(
1817 &mut self,
1818 styled_dom: &StyledDom,
1819 list_item_dom_id: NodeId,
1820 list_item_idx: usize,
1821 ) -> usize {
1822 let index = self.nodes.len();
1823
1824 let parent_fc = self
1827 .nodes
1828 .get(list_item_idx)
1829 .map(|n| n.formatting_context);
1830 self.nodes.push(LayoutNode {
1831 box_props: BoxProps::default(),
1833 dom_node_id: Some(list_item_dom_id),
1834 children: Vec::new(),
1835 used_size: None,
1836 formatting_context: FormattingContext::Inline,
1837 parent: Some(list_item_idx),
1838 intrinsic_sizes: None,
1840 baseline: None,
1841 inline_layout_result: None,
1842 scrollbar_info: None,
1843 relative_position: None,
1844 overflow_content_size: None,
1845 taffy_cache: TaffyCache::new(),
1846 computed_style: ComputedLayoutStyle::default(),
1847 pseudo_element: Some(PseudoElement::Marker),
1848 escaped_top_margin: None,
1849 escaped_bottom_margin: None,
1850 parent_formatting_context: parent_fc,
1851 ifc_membership: None,
1852 containing_block_index: None,
1853 anonymous_type: None,
1855 node_data_fingerprint: NodeDataFingerprint::default(),
1856 subtree_hash: SubtreeHash(0),
1857 dirty_flag: DirtyFlag::Layout,
1858 unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1859 ifc_id: None,
1860 });
1861
1862 self.nodes[list_item_idx].children.insert(0, index);
1864
1865 self.dom_to_layout
1867 .entry(list_item_dom_id)
1868 .or_default()
1869 .push(index);
1870
1871 index
1872 }
1873
1874 pub fn blockify_node_display(
1891 &mut self,
1892 styled_dom: &StyledDom,
1893 dom_id: NodeId,
1894 node_idx: usize,
1895 parent_idx: Option<usize>,
1896 ) {
1897 let node_data = &styled_dom.node_data.as_container()[dom_id];
1898 let raw_display = {
1901 let d = get_display_type(styled_dom, dom_id);
1902 if d.is_layout_internal() && is_replaced_element(node_data) {
1903 LayoutDisplay::Inline
1904 } else {
1905 d
1906 }
1907 };
1908 let (position, float) = self
1909 .nodes
1910 .get(node_idx)
1911 .map(|n| (n.computed_style.position, n.computed_style.float))
1912 .unwrap_or_default();
1913 let is_absolute_or_fixed =
1914 matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed);
1915 let is_floated = float != LayoutFloat::None;
1916 let is_root = parent_idx.is_none();
1917 let is_flex_grid_child = parent_idx
1918 .and_then(|p| self.nodes.get(p))
1919 .is_some_and(|n| {
1920 matches!(
1921 n.formatting_context,
1922 FormattingContext::Flex | FormattingContext::Grid
1923 )
1924 });
1925 let display_type = crate::solver3::getters::get_computed_display(
1926 raw_display,
1927 is_absolute_or_fixed,
1928 is_floated,
1929 is_root,
1930 is_flex_grid_child,
1931 );
1932 if display_type != raw_display {
1933 if let Some(node) = self.nodes.get_mut(node_idx) {
1934 node.computed_style.display = display_type;
1935 node.formatting_context =
1936 determine_formatting_context_for_display(styled_dom, dom_id, display_type);
1937 }
1938 }
1939 }
1940
1941 #[allow(clippy::cast_possible_truncation)] pub fn create_node_from_dom(
1943 &mut self,
1944 styled_dom: &StyledDom,
1945 dom_id: NodeId,
1946 parent: Option<usize>,
1947 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1948 ) -> usize {
1949 let index = self.nodes.len();
1950 { let _ = (0xCE00_0000u32 | (index as u32 & 0xffff)); }
1953 let parent_fc =
1954 parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context));
1955 { let _ = (0xCD00_0001u32 | (u32::from(parent_fc.is_some()) << 8)); }
1958 let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
1959 { let _ = (0xCA00_0001u32); }
1960 self.nodes.push(LayoutNode {
1961 box_props: collected.resolved,
1963 dom_node_id: Some(dom_id),
1964 children: Vec::new(),
1965 used_size: None,
1966 formatting_context: determine_formatting_context(styled_dom, dom_id),
1967 parent,
1968 intrinsic_sizes: None,
1970 baseline: None,
1971 inline_layout_result: None,
1972 scrollbar_info: None,
1973 relative_position: None,
1974 overflow_content_size: None,
1975 taffy_cache: TaffyCache::new(),
1976 computed_style: {
1978 let mut style = compute_layout_style(styled_dom, dom_id);
1979 if parent.is_none() {
1980 use azul_css::props::layout::LayoutOverflow;
1984 if style.overflow_x == LayoutOverflow::Visible {
1985 style.overflow_x = LayoutOverflow::Auto;
1986 } else if style.overflow_x == LayoutOverflow::Clip {
1987 style.overflow_x = LayoutOverflow::Hidden;
1988 }
1989 if style.overflow_y == LayoutOverflow::Visible {
1990 style.overflow_y = LayoutOverflow::Auto;
1991 } else if style.overflow_y == LayoutOverflow::Clip {
1992 style.overflow_y = LayoutOverflow::Hidden;
1993 }
1994 }
1995 style
1996 },
1997 pseudo_element: None,
1998 escaped_top_margin: None,
1999 escaped_bottom_margin: None,
2000 parent_formatting_context: parent_fc,
2001 ifc_membership: None,
2002 containing_block_index: None,
2003 anonymous_type: None,
2005 node_data_fingerprint: NodeDataFingerprint::compute(
2006 &styled_dom.node_data.as_container()[dom_id],
2007 styled_dom.styled_nodes.as_container().get(dom_id).map(|n| &n.styled_node_state),
2008 ),
2009 subtree_hash: SubtreeHash(0),
2010 dirty_flag: DirtyFlag::Layout,
2011 unresolved_box_props: collected.unresolved,
2012 ifc_id: None,
2013 });
2014 { let _ = (0xCB00_0001u32 | ((self.nodes.len() as u32 & 0xff) << 8)); }
2015 if let Some(p) = parent {
2016 self.nodes[p].children.push(index);
2017 }
2018 self.dom_to_layout.entry(dom_id).or_default().push(index);
2019 unsafe {
2023 let c = crate::az_mark_read(0x40500);
2024 crate::az_mark(0x60500_u32, (c.wrapping_add(1)));
2025 if (c as usize) < 14 {
2026 crate::az_mark((0x40504 + (c as usize) * 4) as u32, (0xDD00_0000 | (dom_id.index() as u32 & 0xffff)));
2027 }
2028 }
2029 index
2030 }
2031
2032 pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
2033 let index = self.nodes.len();
2034 let mut new_node = old_node.clone();
2035 new_node.parent = parent;
2036 new_node.parent_formatting_context =
2037 parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context));
2038 new_node.children = Vec::new();
2039 new_node.dirty_flag = DirtyFlag::None;
2040 self.nodes.push(new_node);
2041 if let Some(p) = parent {
2042 self.nodes[p].children.push(index);
2043 }
2044 if let Some(dom_id) = old_node.dom_node_id {
2045 self.dom_to_layout.entry(dom_id).or_default().push(index);
2046 }
2047 index
2048 }
2049
2050 #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn build(self, root_idx: usize) -> LayoutTree {
2052 let nodes = self.nodes;
2053 let node_count = nodes.len();
2054
2055 let total_children: usize = nodes.iter().map(|n| n.children.len()).sum();
2057 let mut arena = Vec::with_capacity(total_children);
2058 let mut offsets = Vec::with_capacity(node_count);
2059
2060 let mut hot_nodes = Vec::with_capacity(node_count);
2062 let mut warm_nodes = Vec::with_capacity(node_count);
2063 let mut cold_nodes = Vec::with_capacity(node_count);
2064
2065 for node in nodes {
2066 let start = arena.len() as u32;
2068 let len = node.children.len() as u32;
2069 arena.extend_from_slice(&node.children);
2070 offsets.push((start, len));
2071
2072 let (hot, warm, cold) = node.split();
2074 hot_nodes.push(hot);
2075 warm_nodes.push(warm);
2076 cold_nodes.push(cold);
2077 }
2078
2079 LayoutTree {
2085 nodes: hot_nodes,
2086 warm: warm_nodes,
2087 cold: cold_nodes,
2088 root: root_idx,
2089 dom_to_layout: self.dom_to_layout,
2090 children_arena: arena,
2091 children_offsets: offsets,
2092 subtree_needs_intrinsic: Vec::new(),
2095 }
2096 }
2097}
2098
2099#[must_use] pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2103 matches!(
2104 get_display_type(styled_dom, node_id),
2105 LayoutDisplay::Block
2106 | LayoutDisplay::FlowRoot
2107 | LayoutDisplay::Flex
2108 | LayoutDisplay::Grid
2109 | LayoutDisplay::Table
2110 | LayoutDisplay::TableCaption
2111 | LayoutDisplay::TableRow
2112 | LayoutDisplay::TableRowGroup
2113 | LayoutDisplay::TableHeaderGroup
2114 | LayoutDisplay::TableFooterGroup
2115 | LayoutDisplay::TableCell
2116 | LayoutDisplay::ListItem
2117 )
2118}
2119
2120fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2128 let node_data = &styled_dom.node_data.as_container()[node_id];
2130 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2131 return true;
2132 }
2133
2134 matches!(
2136 get_display_type(styled_dom, node_id),
2137 LayoutDisplay::Inline
2138 | LayoutDisplay::InlineBlock
2139 | LayoutDisplay::InlineTable
2140 | LayoutDisplay::InlineFlex
2141 | LayoutDisplay::InlineGrid
2142 )
2143}
2144
2145pub(crate) fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2152 let hierarchy = styled_dom.node_hierarchy.as_container();
2153 let Some(node_hier) = hierarchy.get(node_id) else {
2154 return false;
2155 };
2156
2157 let mut current_child = node_hier.first_child_id(node_id);
2159
2160 if current_child.is_none() {
2162 return false;
2163 }
2164
2165 while let Some(child_id) = current_child {
2167 let is_inline = is_inline_level(styled_dom, child_id);
2168
2169 if !is_inline {
2170 return false;
2172 }
2173
2174 if let Some(child_hier) = hierarchy.get(child_id) {
2176 current_child = child_hier.next_sibling_id();
2177 } else {
2178 break;
2179 }
2180 }
2181
2182 true
2184}
2185
2186fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
2191 let styled_node_state = styled_dom
2192 .styled_nodes
2193 .as_container()
2194 .get(dom_id)
2195 .map(|n| n.styled_node_state)
2196 .unwrap_or_default();
2197
2198 let display = match get_display_property(styled_dom, Some(dom_id)) {
2200 MultiValue::Exact(d) => d,
2201 MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
2202 };
2203
2204 let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2206
2207 let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2209
2210 let is_replaced = matches!(
2213 styled_dom.node_data.as_container()[dom_id].get_node_type(),
2214 NodeType::Image(_) | NodeType::VirtualView
2215 );
2216 let overflow_x = {
2217 let v = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2218 if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2219 };
2220 let overflow_y = {
2221 let v = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2222 if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2223 };
2224
2225 let writing_mode = {
2228 let own_wm = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2229 let nd = &styled_dom.node_data.as_container()[dom_id];
2230 if matches!(nd.node_type, NodeType::Html) {
2231 styled_dom
2233 .node_hierarchy
2234 .as_container()
2235 .get(dom_id)
2236 .and_then(|node| node.first_child_id(dom_id))
2237 .and_then(|child_id| {
2238 let child_data = &styled_dom.node_data.as_container()[child_id];
2239 if matches!(child_data.node_type, NodeType::Body) {
2240 let child_state = &styled_dom
2241 .styled_nodes
2242 .as_container()[child_id]
2243 .styled_node_state;
2244 Some(get_writing_mode(styled_dom, child_id, child_state)
2245 .unwrap_or_default())
2246 } else {
2247 None
2248 }
2249 })
2250 .unwrap_or(own_wm)
2251 } else {
2252 own_wm
2253 }
2254 };
2255 let direction = get_direction(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2256 let text_orientation = get_text_orientation(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2257
2258 let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2260
2261 let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
2263 MultiValue::Exact(w) => Some(w),
2264 _ => None,
2265 };
2266 let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
2267 MultiValue::Exact(h) => Some(h),
2268 _ => None,
2269 };
2270
2271 let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
2273 MultiValue::Exact(v) => Some(v),
2274 _ => None,
2275 };
2276 let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
2277 MultiValue::Exact(v) => Some(v),
2278 _ => None,
2279 };
2280 let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
2281 MultiValue::Exact(v) => Some(v),
2282 _ => None,
2283 };
2284 let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
2285 MultiValue::Exact(v) => Some(v),
2286 _ => None,
2287 };
2288
2289 ComputedLayoutStyle {
2290 display,
2291 position,
2292 float,
2293 overflow_x,
2294 overflow_y,
2295 writing_mode,
2296 direction,
2297 text_orientation,
2298 width,
2299 height,
2300 min_width,
2301 min_height,
2302 max_width,
2303 max_height,
2304 text_align,
2305 }
2306}
2307
2308fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2312 { let _ = (0xC3_000001u32); } let node_state = styled_dom
2314 .styled_nodes
2315 .as_container()
2316 .get(dom_id)
2317 .map(|n| &n.styled_node_state)
2318 .copied()
2319 .unwrap_or_default();
2320 { let _ = (0xC3_000002u32); } crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
2323}
2324
2325fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2327 styled_dom
2328 .node_hierarchy
2329 .as_container()
2330 .get(dom_id)
2331 .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
2332 .map_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE, |parent_id| get_element_font_size(styled_dom, parent_id))
2333}
2334
2335fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
2337 get_element_font_size(styled_dom, NodeId::new(0))
2339}
2340
2341fn create_resolution_context(
2343 styled_dom: &StyledDom,
2344 dom_id: NodeId,
2345 containing_block_size: Option<PhysicalSize>,
2346 viewport_size: LogicalSize,
2347) -> ResolutionContext {
2348 { let _ = (0xC1_000001u32); } let element_font_size = get_element_font_size(styled_dom, dom_id);
2350 { let _ = (0xC1_000002u32); } let parent_font_size = get_parent_font_size(styled_dom, dom_id);
2352 { let _ = (0xC1_000003u32); } let root_font_size = get_root_font_size(styled_dom);
2354 { let _ = (0xC1_000004u32); } ResolutionContext {
2357 element_font_size,
2358 parent_font_size,
2359 root_font_size,
2360 containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
2362 element_size: None, viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
2364 }
2365}
2366
2367struct CollectedBoxProps {
2369 unresolved: crate::solver3::geometry::UnresolvedBoxProps,
2370 resolved: BoxProps,
2371}
2372
2373#[allow(clippy::too_many_lines)] fn collect_box_props(
2380 styled_dom: &StyledDom,
2381 dom_id: NodeId,
2382 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2383 viewport_size: LogicalSize,
2384) -> CollectedBoxProps {
2385 use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
2386 #[allow(clippy::wildcard_imports)] use crate::solver3::getters::*;
2388 use azul_css::props::style::border::BorderStyle;
2389 { let _ = (0xC0_000001u32); } let node_data = &styled_dom.node_data.as_container()[dom_id];
2393
2394 let node_state = styled_dom
2396 .styled_nodes
2397 .as_container()
2398 .get(dom_id)
2399 .map(|n| &n.styled_node_state)
2400 .copied()
2401 .unwrap_or_default();
2402 { let _ = (0xC0_000002u32); } let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
2408 { let _ = (0xC0_000003u32); } let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
2412 { let _ = (0xC0_000004u32); } let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
2414 let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
2415 let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
2416
2417 let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
2419 match mv {
2420 MultiValue::Auto => UnresolvedMargin::Auto,
2421 MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
2422 _ => UnresolvedMargin::Zero,
2423 }
2424 };
2425
2426 let unresolved_margin = UnresolvedEdge {
2428 top: to_unresolved_margin(&margin_top_mv),
2429 right: to_unresolved_margin(&margin_right_mv),
2430 bottom: to_unresolved_margin(&margin_bottom_mv),
2431 left: to_unresolved_margin(&margin_left_mv),
2432 };
2433 { let _ = (0xC0_000005u32); } let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
2437 let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
2438 let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
2439 let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
2440
2441 let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
2443 match mv {
2444 MultiValue::Exact(pv) => pv,
2445 _ => PixelValue::const_px(0),
2446 }
2447 };
2448
2449 let unresolved_padding = UnresolvedEdge {
2451 top: to_pixel_value(padding_top_mv),
2452 right: to_pixel_value(padding_right_mv),
2453 bottom: to_pixel_value(padding_bottom_mv),
2454 left: to_pixel_value(padding_left_mv),
2455 };
2456 { let _ = (0xC0_000056u32); } let unresolved_padding = match get_display_type(styled_dom, dom_id) {
2469 LayoutDisplay::TableRow
2470 | LayoutDisplay::TableRowGroup
2471 | LayoutDisplay::TableHeaderGroup
2472 | LayoutDisplay::TableFooterGroup
2473 | LayoutDisplay::TableColumn
2474 | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2475 top: PixelValue::const_px(0),
2476 right: PixelValue::const_px(0),
2477 bottom: PixelValue::const_px(0),
2478 left: PixelValue::const_px(0),
2479 },
2480 _ => unresolved_padding,
2481 };
2482 { let _ = (0xC0_000006u32); } let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
2486 let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
2487 let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
2488 let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
2489
2490 let style_zeroes_width = |s: BorderStyle| matches!(s, BorderStyle::None | BorderStyle::Hidden);
2494
2495 let (bs_top, bs_right, bs_bottom, bs_left) = {
2499 let cache_ptr = &styled_dom.css_property_cache.ptr;
2500 if node_state.is_normal() {
2501 cache_ptr.compact_cache.as_ref().map_or_else(|| (
2502 cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2503 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2504 cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2505 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2506 cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2507 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2508 cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2509 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2510 ), |cc| {
2511 let idx = dom_id.index();
2512 (cc.get_border_top_style(idx), cc.get_border_right_style(idx),
2513 cc.get_border_bottom_style(idx), cc.get_border_left_style(idx))
2514 })
2515 } else {
2516 (
2517 cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2518 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2519 cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2520 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2521 cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2522 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2523 cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2524 .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2525 )
2526 }
2527 };
2528
2529 let unresolved_border = UnresolvedEdge {
2531 top: if style_zeroes_width(bs_top) { PixelValue::const_px(0) } else { to_pixel_value(border_top_mv) },
2532 right: if style_zeroes_width(bs_right) { PixelValue::const_px(0) } else { to_pixel_value(border_right_mv) },
2533 bottom: if style_zeroes_width(bs_bottom) { PixelValue::const_px(0) } else { to_pixel_value(border_bottom_mv) },
2534 left: if style_zeroes_width(bs_left) { PixelValue::const_px(0) } else { to_pixel_value(border_left_mv) },
2535 };
2536 { let _ = (0xC0_000007u32); } let display_type = get_display_type(styled_dom, dom_id);
2544 let unresolved_margin = match display_type {
2545 LayoutDisplay::TableRow
2546 | LayoutDisplay::TableRowGroup
2547 | LayoutDisplay::TableHeaderGroup
2548 | LayoutDisplay::TableFooterGroup
2549 | LayoutDisplay::TableCell
2550 | LayoutDisplay::TableColumn
2551 | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2552 top: UnresolvedMargin::Zero,
2553 right: UnresolvedMargin::Zero,
2554 bottom: UnresolvedMargin::Zero,
2555 left: UnresolvedMargin::Zero,
2556 },
2557 LayoutDisplay::Inline => {
2562 let is_replaced = matches!(
2563 node_data.get_node_type(),
2564 NodeType::Image(_) | NodeType::VirtualView
2565 );
2566 if is_replaced {
2567 unresolved_margin
2568 } else {
2569 UnresolvedEdge {
2570 top: UnresolvedMargin::Zero,
2571 bottom: UnresolvedMargin::Zero,
2572 ..unresolved_margin
2573 }
2574 }
2575 },
2576 _ => unresolved_margin,
2577 };
2578
2579 let unresolved = UnresolvedBoxProps {
2581 margin: unresolved_margin,
2582 padding: unresolved_padding,
2583 border: unresolved_border,
2584 };
2585
2586 let params = crate::solver3::geometry::ResolutionParams {
2588 containing_block: viewport_size,
2589 viewport_size,
2590 element_font_size: context.parent_font_size,
2591 root_font_size: context.root_font_size,
2592 };
2593
2594 let resolved = unresolved.resolve(¶ms);
2596
2597 if let Some(msgs) = debug_messages.as_mut() {
2598 msgs.push(LayoutDebugMessage::box_props(format!(
2599 "[BOX] node[{}] {:?} pad=[{:.1} {:.1} {:.1} {:.1}] mar=[{:.1} {:.1} {:.1} {:.1}] bor=[{:.1} {:.1} {:.1} {:.1}]",
2600 dom_id.index(), node_data.node_type,
2601 resolved.padding.top, resolved.padding.right, resolved.padding.bottom, resolved.padding.left,
2602 resolved.margin.top, resolved.margin.right, resolved.margin.bottom, resolved.margin.left,
2603 resolved.border.top, resolved.border.right, resolved.border.bottom, resolved.border.left,
2604 )));
2605
2606 let has_vh = match &unresolved_margin.top {
2607 UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
2608 _ => false,
2609 };
2610 if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
2611 msgs.push(LayoutDebugMessage::box_props(format!(
2612 "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
2613 dom_id, node_data.node_type,
2614 unresolved_margin.top,
2615 resolved.margin.top,
2616 viewport_size
2617 )));
2618 }
2619
2620 msgs.push(LayoutDebugMessage::box_props(format!(
2621 "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
2622 dom_id, node_data.node_type,
2623 resolved.margin_auto.left, resolved.margin_auto.right,
2624 resolved.margin_auto.top, resolved.margin_auto.bottom,
2625 unresolved_margin.left
2626 )));
2627
2628 if matches!(node_data.node_type, NodeType::Body) {
2629 msgs.push(LayoutDebugMessage::box_props(format!(
2630 "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
2631 resolved.margin.top, resolved.margin.right,
2632 resolved.margin.bottom, resolved.margin.left
2633 )));
2634 }
2635 }
2636
2637 CollectedBoxProps { unresolved, resolved }
2638}
2639
2640#[must_use] pub fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2651 let binding = styled_dom.node_data.as_container();
2652 let node_data = binding.get(node_id);
2653 if let Some(data) = node_data {
2654 if let NodeType::Text(text) = data.get_node_type() {
2655 if !text.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
2658 return false;
2659 }
2660 let white_space = styled_dom
2666 .styled_nodes
2667 .as_container()
2668 .get(node_id)
2669 .map_or(StyleWhiteSpace::Normal, |n| {
2670 match get_white_space_property(styled_dom, node_id, &n.styled_node_state) {
2671 MultiValue::Exact(ws) => ws,
2672 _ => StyleWhiteSpace::Normal,
2673 }
2674 });
2675 return match white_space {
2676 StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap | StyleWhiteSpace::PreLine => true,
2678 StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => false,
2680 };
2681 }
2682 }
2683
2684 false
2685}
2686
2687fn should_skip_for_table_structure(
2695 styled_dom: &StyledDom,
2696 node_id: NodeId,
2697 parent_display: LayoutDisplay,
2698) -> bool {
2699 matches!(
2702 parent_display,
2703 LayoutDisplay::Table
2704 | LayoutDisplay::InlineTable
2705 | LayoutDisplay::TableRowGroup
2706 | LayoutDisplay::TableHeaderGroup
2707 | LayoutDisplay::TableFooterGroup
2708 | LayoutDisplay::TableRow
2709 ) && is_whitespace_only_text(styled_dom, node_id)
2710}
2711
2712const fn is_proper_table_child(display: LayoutDisplay) -> bool {
2716 matches!(
2717 display,
2718 LayoutDisplay::TableRowGroup
2719 | LayoutDisplay::TableHeaderGroup
2720 | LayoutDisplay::TableFooterGroup
2721 | LayoutDisplay::TableRow
2722 | LayoutDisplay::TableColumnGroup
2723 | LayoutDisplay::TableColumn
2724 | LayoutDisplay::TableCaption
2725 )
2726}
2727
2728#[must_use] pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
2742 use crate::solver3::getters::get_display_property;
2743 get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
2744}
2745
2746fn blockify_flex_item_if_table_internal(nodes: &mut [LayoutNode], node_idx: usize) {
2761 if let Some(node) = nodes.get_mut(node_idx) {
2762 let is_table_internal = matches!(
2763 node.formatting_context,
2764 FormattingContext::TableCell
2765 | FormattingContext::TableRow
2766 | FormattingContext::TableRowGroup
2767 | FormattingContext::TableColumnGroup
2768 | FormattingContext::TableCaption
2769 | FormattingContext::Table
2770 );
2771 if is_table_internal {
2772 node.formatting_context = FormattingContext::Block {
2773 establishes_new_context: true,
2774 };
2775 }
2776 }
2777}
2778
2779const fn is_replaced_element(node_data: &NodeData) -> bool {
2784 matches!(
2785 node_data.get_node_type(),
2786 NodeType::Image(_)
2787 | NodeType::VirtualView
2788 | NodeType::Br
2789 | NodeType::Wbr
2790 | NodeType::Meter
2791 | NodeType::Progress
2792 | NodeType::Canvas
2793 | NodeType::Embed
2794 | NodeType::Object
2795 | NodeType::Audio
2796 | NodeType::Video
2797 | NodeType::Input
2798 | NodeType::TextArea
2799 | NodeType::Select
2800 )
2801}
2802
2803fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2807 let display = get_display_type(styled_dom, node_id);
2808 if matches!(
2809 display,
2810 LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption | LayoutDisplay::FlowRoot
2811 ) {
2812 return true;
2813 }
2814
2815 if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2816 let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
2822 let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
2823 if overflow_x.establishes_bfc() || overflow_y.establishes_bfc() {
2824 return true;
2825 }
2826
2827 let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
2828 if position.is_absolute_or_fixed() {
2829 return true;
2830 }
2831
2832 let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
2833 if !float.is_none() {
2834 return true;
2835 }
2836 }
2837
2838 if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2840 let hierarchy = styled_dom.node_hierarchy.as_container();
2841 if let Some(parent_dom_id) = hierarchy[node_id].parent_id() {
2842 let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
2843 let child_wm = get_writing_mode(styled_dom, node_id, &styled_node.styled_node_state).unwrap_or_default();
2844 let parent_wm = get_writing_mode(styled_dom, parent_dom_id, parent_state).unwrap_or_default();
2845 if child_wm != parent_wm {
2846 return true;
2847 }
2848 }
2849 }
2850
2851 let node_data = &styled_dom.node_data.as_container()[node_id];
2853 if is_replaced_element(node_data) {
2854 return true;
2855 }
2856
2857 if styled_dom.root.into_crate_internal() == Some(node_id) {
2859 return true;
2860 }
2861
2862 false
2863}
2864
2865#[allow(clippy::match_same_arms)] fn determine_formatting_context_for_display(
2873 styled_dom: &StyledDom,
2874 node_id: NodeId,
2875 display_type: LayoutDisplay,
2876) -> FormattingContext {
2877 let node_data = &styled_dom.node_data.as_container()[node_id];
2878 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2879 #[cfg(feature = "web_lift")]
2884 unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0001) as u32); }
2885 return FormattingContext::Inline;
2886 }
2887 match display_type {
2889 LayoutDisplay::Inline => FormattingContext::Inline,
2892 LayoutDisplay::FlowRoot => FormattingContext::Block {
2898 establishes_new_context: true,
2899 },
2900 LayoutDisplay::Block | LayoutDisplay::ListItem => {
2901 if has_only_inline_children(styled_dom, node_id) {
2902 #[cfg(feature = "web_lift")]
2903 unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0002) as u32); }
2904 FormattingContext::Inline
2905 } else {
2906 #[cfg(feature = "web_lift")]
2907 unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0004) as u32); }
2908 FormattingContext::Block {
2909 establishes_new_context: establishes_new_block_formatting_context(
2910 styled_dom, node_id,
2911 ),
2912 }
2913 }
2914 }
2915 LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
2916 LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
2924 LayoutDisplay::TableRowGroup
2925 | LayoutDisplay::TableHeaderGroup
2926 | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
2927 LayoutDisplay::TableRow => FormattingContext::TableRow,
2928 LayoutDisplay::TableCell => FormattingContext::TableCell,
2929 LayoutDisplay::None => FormattingContext::None,
2932 LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
2933 LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
2934 LayoutDisplay::TableCaption => FormattingContext::TableCaption,
2935 LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
2936 LayoutDisplay::TableColumn => FormattingContext::None,
2938 LayoutDisplay::Contents => FormattingContext::Contents,
2941 LayoutDisplay::RunIn | LayoutDisplay::Marker => {
2948 FormattingContext::Block {
2949 establishes_new_context: true,
2950 }
2951 }
2952 }
2953}
2954
2955fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
2957 let node_data = &styled_dom.node_data.as_container()[node_id];
2958 if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2964 #[cfg(feature = "web_lift")]
2965 unsafe { crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0001); }
2966 return FormattingContext::Inline;
2967 }
2968 let display_type = get_display_type(styled_dom, node_id);
2969 let fc = determine_formatting_context_for_display(styled_dom, node_id, display_type);
2970 #[cfg(feature = "web_lift")]
2971 unsafe {
2972 let disc: u8 = core::ptr::read_volatile((&fc) as *const FormattingContext as *const u8);
2973 crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0010 | disc as u32);
2974 }
2975 fc
2976}
2977
2978#[cfg(test)]
2979#[allow(clippy::float_cmp, clippy::too_many_lines)]
2980mod autotest_generated {
2981 use azul_core::{
2982 dom::{Dom, IdOrClass},
2983 resources::{ImageRef, RawImageFormat},
2984 selection::ContentIndex,
2985 };
2986
2987 use super::*;
2988 use crate::{
2989 solver3::geometry::{EdgeSizes, PackedBoxProps},
2990 text3::cache::{
2991 BreakType, ClearType, InlineBreak, OverflowInfo, Point, PositionedItem, Rect,
2992 ShapedItem,
2993 },
2994 };
2995
2996 const VIEWPORT: LogicalSize = LogicalSize {
3001 width: 800.0,
3002 height: 600.0,
3003 };
3004
3005 fn styled(dom: Dom, css_str: &str) -> StyledDom {
3006 let mut dom = dom;
3007 let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
3008 StyledDom::create(&mut dom, css)
3009 }
3010
3011 fn div_class(class: &str) -> Dom {
3012 Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
3013 }
3014
3015 fn build_tree(styled_dom: &StyledDom) -> LayoutTree {
3019 let mut builder = LayoutTreeBuilder::new(VIEWPORT);
3020 let mut msgs: Option<Vec<LayoutDebugMessage>> = None;
3021 let root_id = styled_dom
3022 .root
3023 .into_crate_internal()
3024 .unwrap_or(NodeId::ZERO);
3025 let root_index = builder
3026 .process_node(styled_dom, root_id, None, &mut msgs)
3027 .expect("process_node on a well-formed DOM");
3028 let mut tree = builder.build(root_index);
3029 tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(styled_dom, &tree);
3030 tree
3031 }
3032
3033 fn mixed_dom() -> StyledDom {
3040 styled(
3041 Dom::create_body()
3042 .with_child(
3043 div_class("block")
3044 .with_child(Dom::create_text("hello"))
3045 .with_child(div_class("inline").with_child(Dom::create_text("world"))),
3046 )
3047 .with_child(
3048 div_class("mixed")
3049 .with_child(Dom::create_text(" \n\t"))
3050 .with_child(div_class("block2"))
3051 .with_child(Dom::create_text("tail")),
3052 ),
3053 ".block { display: block; } .inline { display: inline; } .mixed { display: block; } \
3054 .block2 { display: block; }",
3055 )
3056 }
3057
3058 fn text_node(styled_dom: &StyledDom, needle: &str) -> NodeId {
3061 let container = styled_dom.node_data.as_container();
3062 for i in 0..styled_dom.node_data.len() {
3063 let id = NodeId::new(i);
3064 if let NodeType::Text(text) = container[id].get_node_type() {
3065 if text.as_str() == needle {
3066 return id;
3067 }
3068 }
3069 }
3070 panic!("no text node with content {needle:?}");
3071 }
3072
3073 fn empty_layout() -> Arc<UnifiedLayout> {
3074 Arc::new(UnifiedLayout {
3075 items: Vec::new(),
3076 overflow: OverflowInfo::default(),
3077 })
3078 }
3079
3080 fn layout_of(items: Vec<PositionedItem>) -> Arc<UnifiedLayout> {
3081 Arc::new(UnifiedLayout {
3082 items,
3083 overflow: OverflowInfo::default(),
3084 })
3085 }
3086
3087 fn tab_item(width: f32, height: f32, x: f32, line_index: usize) -> PositionedItem {
3088 PositionedItem {
3089 item: ShapedItem::Tab {
3090 source: ContentIndex {
3091 run_index: 0,
3092 item_index: 0,
3093 },
3094 bounds: Rect {
3095 x: 0.0,
3096 y: 0.0,
3097 width,
3098 height,
3099 },
3100 },
3101 position: Point { x, y: 0.0 },
3102 line_index,
3103 }
3104 }
3105
3106 fn break_item(line_index: usize) -> PositionedItem {
3107 PositionedItem {
3108 item: ShapedItem::Break {
3109 source: ContentIndex {
3110 run_index: 0,
3111 item_index: 0,
3112 },
3113 break_info: InlineBreak {
3114 break_type: BreakType::Hard,
3115 clear: ClearType::None,
3116 content_index: 0,
3117 },
3118 },
3119 position: Point { x: 0.0, y: 0.0 },
3120 line_index,
3121 }
3122 }
3123
3124 fn hot(parent: Option<usize>) -> LayoutNodeHot {
3125 LayoutNodeHot {
3126 box_props: PackedBoxProps::default(),
3127 dom_node_id: None,
3128 used_size: None,
3129 formatting_context: FormattingContext::Block {
3130 establishes_new_context: false,
3131 },
3132 parent,
3133 }
3134 }
3135
3136 fn raw_tree(nodes: Vec<LayoutNodeHot>, child_lists: &[Vec<usize>]) -> LayoutTree {
3139 let n = nodes.len();
3140 let mut children_arena: Vec<usize> = Vec::new();
3141 let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
3142 for cl in child_lists {
3143 let start = u32::try_from(children_arena.len()).unwrap();
3144 children_arena.extend_from_slice(cl);
3145 children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
3146 }
3147 while children_offsets.len() < n {
3148 children_offsets.push((0, 0));
3149 }
3150 LayoutTree {
3151 nodes,
3152 warm: vec![LayoutNodeWarm::default(); n],
3153 cold: vec![LayoutNodeCold::default(); n],
3154 root: 0,
3155 dom_to_layout: BTreeMap::new(),
3156 children_arena,
3157 children_offsets,
3158 subtree_needs_intrinsic: Vec::new(),
3159 }
3160 }
3161
3162 const ALL_DISPLAYS: [LayoutDisplay; 23] = [
3163 LayoutDisplay::None,
3164 LayoutDisplay::Block,
3165 LayoutDisplay::Inline,
3166 LayoutDisplay::InlineBlock,
3167 LayoutDisplay::Flex,
3168 LayoutDisplay::InlineFlex,
3169 LayoutDisplay::Table,
3170 LayoutDisplay::InlineTable,
3171 LayoutDisplay::TableRowGroup,
3172 LayoutDisplay::TableHeaderGroup,
3173 LayoutDisplay::TableFooterGroup,
3174 LayoutDisplay::TableRow,
3175 LayoutDisplay::TableColumnGroup,
3176 LayoutDisplay::TableColumn,
3177 LayoutDisplay::TableCell,
3178 LayoutDisplay::TableCaption,
3179 LayoutDisplay::FlowRoot,
3180 LayoutDisplay::ListItem,
3181 LayoutDisplay::RunIn,
3182 LayoutDisplay::Marker,
3183 LayoutDisplay::Grid,
3184 LayoutDisplay::InlineGrid,
3185 LayoutDisplay::Contents,
3186 ];
3187
3188 #[test]
3193 fn ifcid_unique_hands_out_a_fresh_id_per_call_after_reset() {
3194 IfcId::reset_counter();
3195 assert_eq!(IfcId::unique(), IfcId(0));
3196 assert_eq!(IfcId::unique(), IfcId(1));
3197 assert_eq!(IfcId::unique(), IfcId(2));
3198 IfcId::reset_counter();
3199 assert_eq!(
3200 IfcId::unique(),
3201 IfcId(0),
3202 "reset_counter must restart the sequence, not continue it"
3203 );
3204 IfcId::reset_counter();
3205 }
3206
3207 #[test]
3208 fn ifcid_reset_counter_is_idempotent() {
3209 IfcId::reset_counter();
3210 IfcId::reset_counter();
3211 IfcId::reset_counter();
3212 assert_eq!(IfcId::unique(), IfcId(0));
3213 IfcId::reset_counter();
3214 }
3215
3216 #[test]
3217 fn ifcid_unique_wraps_at_u32_max_instead_of_panicking() {
3218 IFC_ID_COUNTER.with(|c| c.set(u32::MAX));
3221 assert_eq!(IfcId::unique(), IfcId(u32::MAX));
3222 assert_eq!(IfcId::unique(), IfcId(0), "wraps rather than overflow-panics");
3223 assert_eq!(IfcId::unique(), IfcId(1));
3224 IfcId::reset_counter();
3225 }
3226
3227 #[test]
3232 fn cached_inline_layout_new_keeps_the_args_it_was_given() {
3233 let arc = empty_layout();
3234 let c = CachedInlineLayout::new(Arc::clone(&arc), AvailableSpace::Definite(123.5), true);
3235 assert!(Arc::ptr_eq(&c.layout, &arc));
3236 assert_eq!(c.available_width, AvailableSpace::Definite(123.5));
3237 assert!(c.has_floats);
3238 assert!(c.constraints.is_none(), "new() carries no constraints");
3239 assert!(c.line_breaks.is_none(), "new() computes no line breaks");
3240 assert_eq!(c.inline_content_hash, 0, "0 = unknown ⇒ never fast-path-reuse");
3241 assert!(c.item_metrics.is_empty(), "an empty layout has no item metrics");
3242 }
3243
3244 #[test]
3245 fn cached_inline_layout_new_survives_extreme_widths() {
3246 for w in [
3247 AvailableSpace::Definite(0.0),
3248 AvailableSpace::Definite(-1.0),
3249 AvailableSpace::Definite(f32::MAX),
3250 AvailableSpace::Definite(f32::MIN),
3251 AvailableSpace::Definite(f32::INFINITY),
3252 AvailableSpace::Definite(f32::NEG_INFINITY),
3253 AvailableSpace::Definite(f32::NAN),
3254 AvailableSpace::MinContent,
3255 AvailableSpace::MaxContent,
3256 ] {
3257 let c = CachedInlineLayout::new(empty_layout(), w, false);
3258 assert!(c.item_metrics.is_empty());
3259 assert!(c.layout.items.is_empty());
3260 }
3261 }
3262
3263 #[test]
3264 fn extract_item_metrics_mirrors_every_positioned_item() {
3265 let layout = layout_of(vec![tab_item(12.0, 20.0, 5.0, 3), tab_item(0.0, 0.0, 0.0, 0)]);
3266 let m = CachedInlineLayout::extract_item_metrics(&layout);
3267 assert_eq!(m.len(), 2, "one metric entry per PositionedItem, in order");
3268
3269 assert_eq!(m[0].advance_width, 12.0);
3270 assert_eq!(m[0].x_offset, 5.0);
3271 assert_eq!(m[0].line_index, 3);
3272 assert!(m[0].can_break, "a Tab is breakable");
3273 assert!(
3274 m[0].source_node_id.is_none(),
3275 "non-Cluster items expose no source_node_id"
3276 );
3277 assert!(
3279 (m[0].line_height_contribution - 20.0).abs() < 1e-3,
3280 "ascent+descent should reconstruct the height, got {}",
3281 m[0].line_height_contribution
3282 );
3283
3284 assert_eq!(m[1].advance_width, 0.0);
3285 assert_eq!(m[1].line_index, 0);
3286 }
3287
3288 #[test]
3289 fn extract_item_metrics_marks_break_items_as_unbreakable_and_zero_sized() {
3290 let layout = layout_of(vec![break_item(7)]);
3291 let m = CachedInlineLayout::extract_item_metrics(&layout);
3292 assert_eq!(m.len(), 1);
3293 assert!(!m[0].can_break, "ShapedItem::Break is the one non-breakable item");
3294 assert_eq!(m[0].advance_width, 0.0, "a break has no visual geometry");
3295 assert_eq!(m[0].line_height_contribution, 0.0);
3296 assert_eq!(m[0].line_index, 7);
3297 }
3298
3299 #[test]
3300 fn extract_item_metrics_on_an_empty_layout_is_empty_not_a_panic() {
3301 assert!(CachedInlineLayout::extract_item_metrics(&empty_layout()).is_empty());
3302 }
3303
3304 #[test]
3305 fn extract_item_metrics_does_not_choke_on_non_finite_item_bounds() {
3306 let layout = layout_of(vec![
3307 tab_item(f32::INFINITY, f32::NAN, f32::NEG_INFINITY, u32::MAX as usize),
3308 tab_item(f32::MAX, f32::MAX, f32::MIN, 0),
3309 ]);
3310 let m = CachedInlineLayout::extract_item_metrics(&layout);
3311 assert_eq!(m.len(), 2);
3312 assert!(m[0].advance_width.is_infinite());
3313 assert!(m[0].line_height_contribution.is_nan(), "NaN in, NaN out — but no panic");
3314 assert_eq!(m[1].advance_width, f32::MAX);
3315 }
3316
3317 #[test]
3318 fn extract_item_metrics_truncates_a_huge_line_index_into_u32() {
3319 let huge = (u32::MAX as usize) + 5;
3322 let m = CachedInlineLayout::extract_item_metrics(&layout_of(vec![tab_item(
3323 1.0, 1.0, 0.0, huge,
3324 )]));
3325 assert_eq!(m[0].line_index, 4, "wrapping `as u32` truncation, not a panic");
3326 }
3327
3328 #[test]
3329 fn cached_inline_layout_new_with_constraints_records_constraints_and_line_breaks() {
3330 let arc = layout_of(vec![tab_item(10.0, 20.0, 0.0, 0)]);
3331 let c = CachedInlineLayout::new_with_constraints(
3332 Arc::clone(&arc),
3333 AvailableSpace::Definite(200.0),
3334 false,
3335 UnifiedConstraints::default(),
3336 );
3337 assert!(c.constraints.is_some());
3338 let lb = c.line_breaks.expect("new_with_constraints computes line breaks");
3339 assert_eq!(lb.available_width, 200.0);
3340 assert_eq!(c.item_metrics.len(), 1);
3341 }
3342
3343 #[test]
3344 fn new_with_constraints_treats_indefinite_widths_as_f32_max() {
3345 for w in [AvailableSpace::MinContent, AvailableSpace::MaxContent] {
3346 let c = CachedInlineLayout::new_with_constraints(
3347 empty_layout(),
3348 w,
3349 false,
3350 UnifiedConstraints::default(),
3351 );
3352 let lb = c.line_breaks.expect("line breaks");
3353 assert_eq!(
3354 lb.available_width,
3355 f32::MAX,
3356 "indefinite width collapses to f32::MAX for break extraction"
3357 );
3358 assert_eq!(c.available_width, w, "but the cache key keeps the real variant");
3359 }
3360 }
3361
3362 fn cached(width: AvailableSpace, has_floats: bool) -> CachedInlineLayout {
3367 CachedInlineLayout::new(empty_layout(), width, has_floats)
3368 }
3369
3370 #[test]
3371 fn width_constraint_matches_definite_widths_within_the_epsilon() {
3372 let c = cached(AvailableSpace::Definite(100.0), false);
3373 assert!(c.width_constraint_matches(AvailableSpace::Definite(100.0)));
3374 assert!(
3375 c.width_constraint_matches(AvailableSpace::Definite(100.09)),
3376 "sub-0.1px drift must not force a relayout"
3377 );
3378 assert!(c.width_constraint_matches(AvailableSpace::Definite(100.1)));
3383 assert!(
3384 !c.width_constraint_matches(AvailableSpace::Definite(100.2)),
3385 "the epsilon is strict (`< 0.1`), so a 0.2 drift must miss"
3386 );
3387 assert!(!c.width_constraint_matches(AvailableSpace::Definite(0.0)));
3388 }
3389
3390 #[test]
3391 fn width_constraint_matches_only_pairs_like_with_like() {
3392 let min = cached(AvailableSpace::MinContent, false);
3393 let max = cached(AvailableSpace::MaxContent, false);
3394 let def = cached(AvailableSpace::Definite(50.0), false);
3395
3396 assert!(min.width_constraint_matches(AvailableSpace::MinContent));
3397 assert!(max.width_constraint_matches(AvailableSpace::MaxContent));
3398 assert!(!min.width_constraint_matches(AvailableSpace::MaxContent));
3399 assert!(!max.width_constraint_matches(AvailableSpace::MinContent));
3400 assert!(!min.width_constraint_matches(AvailableSpace::Definite(50.0)));
3401 assert!(!def.width_constraint_matches(AvailableSpace::MinContent));
3402 assert!(!def.width_constraint_matches(AvailableSpace::MaxContent));
3403 }
3404
3405 #[test]
3406 fn width_constraint_matches_is_false_for_nan_widths_rather_than_panicking() {
3407 let c = cached(AvailableSpace::Definite(f32::NAN), false);
3410 assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::NAN)));
3411 assert!(!c.width_constraint_matches(AvailableSpace::Definite(0.0)));
3412
3413 let good = cached(AvailableSpace::Definite(10.0), false);
3414 assert!(!good.width_constraint_matches(AvailableSpace::Definite(f32::NAN)));
3415 }
3416
3417 #[test]
3418 fn width_constraint_matches_is_false_for_an_infinite_width_against_itself() {
3419 let c = cached(AvailableSpace::Definite(f32::INFINITY), false);
3422 assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::INFINITY)));
3423 assert!(!c.is_valid_for(AvailableSpace::Definite(f32::INFINITY), false));
3424 assert!(c.should_replace_with(AvailableSpace::Definite(f32::INFINITY), false));
3425 }
3426
3427 #[test]
3428 fn width_constraint_matches_handles_huge_finite_widths() {
3429 let c = cached(AvailableSpace::Definite(f32::MAX), false);
3430 assert!(c.width_constraint_matches(AvailableSpace::Definite(f32::MAX)));
3431 assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::MIN)));
3432 }
3433
3434 #[test]
3435 fn is_valid_for_rejects_a_no_float_cache_when_the_request_gains_floats() {
3436 let widths = [
3441 AvailableSpace::Definite(0.0),
3442 AvailableSpace::Definite(100.0),
3443 AvailableSpace::MinContent,
3444 AvailableSpace::MaxContent,
3445 ];
3446 for cached_floats in [false, true] {
3447 for cached_w in widths {
3448 let c = cached(cached_w, cached_floats);
3449 for new_w in widths {
3450 let width_ok = c.width_constraint_matches(new_w);
3451 assert_eq!(c.is_valid_for(new_w, false), width_ok);
3454 let expected_with_floats = if cached_floats { width_ok } else { false };
3457 assert_eq!(c.is_valid_for(new_w, true), expected_with_floats);
3458 if !cached_floats {
3461 assert!(c.should_replace_with(new_w, true));
3462 }
3463 }
3464 }
3465 }
3466 }
3467
3468 #[test]
3469 fn is_valid_for_returns_the_expected_true_and_false() {
3470 let c = cached(AvailableSpace::Definite(300.0), false);
3471 assert!(c.is_valid_for(AvailableSpace::Definite(300.0), false));
3472 assert!(!c.is_valid_for(AvailableSpace::Definite(299.0), false));
3473 }
3474
3475 #[test]
3476 fn should_replace_with_always_replaces_when_float_info_is_gained() {
3477 let c = cached(AvailableSpace::Definite(300.0), false);
3479 assert!(c.should_replace_with(AvailableSpace::Definite(300.0), true));
3480 assert!(c.should_replace_with(AvailableSpace::MinContent, true));
3481 }
3482
3483 #[test]
3484 fn should_replace_with_keeps_a_float_aware_layout_at_a_matching_width() {
3485 let c = cached(AvailableSpace::Definite(300.0), true);
3486 assert!(
3487 !c.should_replace_with(AvailableSpace::Definite(300.0), false),
3488 "a non-float layout must not overwrite a float-aware one at the same width"
3489 );
3490 assert!(
3491 c.should_replace_with(AvailableSpace::Definite(100.0), false),
3492 "…but a width change still forces a replace"
3493 );
3494 }
3495
3496 #[test]
3497 fn should_replace_with_is_the_negation_of_is_valid_for_when_floats_are_unchanged() {
3498 let widths = [
3499 AvailableSpace::Definite(0.0),
3500 AvailableSpace::Definite(42.0),
3501 AvailableSpace::MinContent,
3502 AvailableSpace::MaxContent,
3503 ];
3504 for floats in [false, true] {
3505 for cached_w in widths {
3506 let c = cached(cached_w, floats);
3507 for new_w in widths {
3508 assert_eq!(
3509 c.should_replace_with(new_w, floats),
3510 !c.is_valid_for(new_w, floats),
3511 "cached={cached_w:?} new={new_w:?} floats={floats}"
3512 );
3513 }
3514 }
3515 }
3516 }
3517
3518 #[test]
3523 fn get_layout_and_clone_layout_hand_back_the_very_same_arc() {
3524 let arc = layout_of(vec![tab_item(1.0, 2.0, 0.0, 0)]);
3525 let c = CachedInlineLayout::new(Arc::clone(&arc), AvailableSpace::MaxContent, false);
3526 assert!(Arc::ptr_eq(c.get_layout(), &arc));
3527
3528 let cloned = c.clone_layout();
3529 assert!(Arc::ptr_eq(&cloned, &arc), "clone_layout must not deep-copy");
3530 assert_eq!(
3531 Arc::strong_count(&arc),
3532 3,
3533 "the original + the cache's + the clone"
3534 );
3535 assert_eq!(c.get_layout().items.len(), 1);
3536 }
3537
3538 #[test]
3539 fn get_layout_works_on_an_empty_extreme_instance() {
3540 let c = cached(AvailableSpace::Definite(f32::NAN), true);
3541 assert!(c.get_layout().items.is_empty());
3542 assert!(c.clone_layout().items.is_empty());
3543 }
3544
3545 #[test]
3550 fn get_full_node_then_split_round_trips_through_the_soa_arrays() {
3551 let sd = mixed_dom();
3552 let tree = build_tree(&sd);
3553 assert!(tree.nodes.len() >= 2);
3554
3555 for i in 0..tree.nodes.len() {
3556 let full = tree.get_full_node(i).expect("in-range node");
3557 let (h, w, c) = full.split();
3558
3559 let hot = tree.get(i).unwrap();
3560 assert_eq!(h.dom_node_id, hot.dom_node_id, "node {i}");
3561 assert_eq!(h.parent, hot.parent, "node {i}");
3562 assert_eq!(h.used_size, hot.used_size, "node {i}");
3563 assert_eq!(h.formatting_context, hot.formatting_context, "node {i}");
3564 assert_eq!(h.box_props.margin, hot.box_props.margin, "node {i}");
3566 assert_eq!(h.box_props.padding, hot.box_props.padding, "node {i}");
3567 assert_eq!(h.box_props.border, hot.box_props.border, "node {i}");
3568
3569 let warm = tree.warm(i).unwrap();
3570 assert_eq!(w.pseudo_element, warm.pseudo_element, "node {i}");
3571 assert_eq!(w.baseline, warm.baseline, "node {i}");
3572 assert_eq!(
3573 w.computed_style.display, warm.computed_style.display,
3574 "node {i}"
3575 );
3576
3577 let cold = tree.cold(i).unwrap();
3578 assert_eq!(c.anonymous_type, cold.anonymous_type, "node {i}");
3579 assert_eq!(c.dirty_flag, cold.dirty_flag, "node {i}");
3580 assert_eq!(c.subtree_hash, cold.subtree_hash, "node {i}");
3581 assert_eq!(c.ifc_id, cold.ifc_id, "node {i}");
3582 }
3583 }
3584
3585 #[test]
3586 fn get_full_node_restores_the_children_from_the_arena() {
3587 let sd = mixed_dom();
3588 let tree = build_tree(&sd);
3589 for i in 0..tree.nodes.len() {
3590 let full = tree.get_full_node(i).unwrap();
3591 assert_eq!(full.children, tree.children(i).to_vec(), "node {i}");
3592 }
3593 }
3594
3595 #[test]
3596 fn get_full_node_is_none_out_of_range() {
3597 let tree = build_tree(&mixed_dom());
3598 assert!(tree.get_full_node(tree.nodes.len()).is_none());
3599 assert!(tree.get_full_node(usize::MAX).is_none());
3600 }
3601
3602 #[test]
3607 fn tree_accessors_return_none_for_every_out_of_range_index() {
3608 let mut tree = build_tree(&mixed_dom());
3609 let n = tree.nodes.len();
3610 for idx in [n, n + 1, usize::MAX, usize::MAX - 1, usize::MAX / 2] {
3611 assert!(tree.get(idx).is_none(), "get({idx})");
3612 assert!(tree.warm(idx).is_none(), "warm({idx})");
3613 assert!(tree.cold(idx).is_none(), "cold({idx})");
3614 assert!(tree.get_mut(idx).is_none(), "get_mut({idx})");
3615 assert!(tree.warm_mut(idx).is_none(), "warm_mut({idx})");
3616 assert!(tree.cold_mut(idx).is_none(), "cold_mut({idx})");
3617 assert!(tree.get_inline_layout_for_node(idx).is_none());
3618 }
3619 }
3620
3621 #[test]
3622 fn tree_accessors_all_resolve_at_index_zero() {
3623 let mut tree = build_tree(&mixed_dom());
3624 assert!(tree.get(0).is_some());
3625 assert!(tree.warm(0).is_some());
3626 assert!(tree.cold(0).is_some());
3627 assert!(tree.get_mut(0).is_some());
3628 assert!(tree.warm_mut(0).is_some());
3629 assert!(tree.cold_mut(0).is_some());
3630 assert_eq!(tree.get(0).unwrap().parent, None, "index 0 is the root");
3631 }
3632
3633 #[test]
3634 fn children_of_an_out_of_range_index_is_an_empty_slice() {
3635 let tree = build_tree(&mixed_dom());
3636 assert!(tree.children(tree.nodes.len()).is_empty());
3637 assert!(tree.children(usize::MAX).is_empty());
3638 assert!(tree.children(usize::MAX - 1).is_empty());
3639 }
3640
3641 #[test]
3642 fn children_arena_slices_agree_with_the_parent_pointers() {
3643 let tree = build_tree(&mixed_dom());
3644 let n = tree.nodes.len();
3645 let mut seen: Vec<usize> = Vec::new();
3646 for i in 0..n {
3647 for &child in tree.children(i) {
3648 assert!(child < n, "child {child} of {i} is out of range");
3649 assert_eq!(
3650 tree.get(child).unwrap().parent,
3651 Some(i),
3652 "child {child} does not point back at parent {i}"
3653 );
3654 seen.push(child);
3655 }
3656 }
3657 seen.sort_unstable();
3658 seen.dedup();
3659 assert_eq!(seen.len(), n - 1, "every node but the root is someone's child");
3660 assert!(!seen.contains(&tree.root), "the root is nobody's child");
3661 }
3662
3663 #[test]
3664 fn children_offsets_stay_inside_the_arena() {
3665 let tree = build_tree(&mixed_dom());
3666 assert_eq!(tree.children_offsets.len(), tree.nodes.len());
3667 let total: usize = tree
3668 .children_offsets
3669 .iter()
3670 .map(|&(_, len)| len as usize)
3671 .sum();
3672 assert_eq!(total, tree.children_arena.len());
3673 for &(start, len) in &tree.children_offsets {
3674 assert!((start as usize) + (len as usize) <= tree.children_arena.len());
3675 }
3676 }
3677
3678 #[test]
3679 fn get_content_size_is_default_for_an_out_of_range_index() {
3680 let tree = build_tree(&mixed_dom());
3681 assert_eq!(tree.get_content_size(usize::MAX), LogicalSize::default());
3682 assert_eq!(tree.get_content_size(tree.nodes.len()), LogicalSize::default());
3683 }
3684
3685 #[test]
3686 fn get_content_size_prefers_the_explicit_overflow_content_size() {
3687 let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3688 tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
3689 tree.warm[0].overflow_content_size = Some(LogicalSize::new(999.0, 888.0));
3690 assert_eq!(tree.get_content_size(0), LogicalSize::new(999.0, 888.0));
3691 }
3692
3693 #[test]
3694 fn get_content_size_grows_the_used_size_to_cover_the_inline_items() {
3695 let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3696 tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
3697 tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3698 layout_of(vec![tab_item(30.0, 40.0, 25.0, 0)]),
3699 AvailableSpace::MaxContent,
3700 false,
3701 ));
3702 let cs = tree.get_content_size(0);
3704 assert_eq!(cs.width, 55.0);
3705 assert_eq!(cs.height, 40.0);
3706 }
3707
3708 #[test]
3709 fn get_content_size_never_shrinks_below_the_used_size() {
3710 let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3711 tree.nodes[0].used_size = Some(LogicalSize::new(500.0, 500.0));
3712 tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3713 layout_of(vec![tab_item(1.0, 1.0, 0.0, 0)]),
3714 AvailableSpace::MaxContent,
3715 false,
3716 ));
3717 assert_eq!(tree.get_content_size(0), LogicalSize::new(500.0, 500.0));
3718 }
3719
3720 #[test]
3721 fn get_content_size_of_a_node_with_no_used_size_is_zero() {
3722 let tree = raw_tree(vec![hot(None)], &[vec![]]);
3723 assert_eq!(tree.get_content_size(0), LogicalSize::default());
3724 }
3725
3726 #[test]
3731 fn get_ifc_root_layout_index_returns_the_input_unchanged_when_out_of_range() {
3732 let tree = build_tree(&mixed_dom());
3733 assert_eq!(tree.get_ifc_root_layout_index(usize::MAX), usize::MAX);
3736 assert_eq!(tree.get_ifc_root_layout_index(0), 0);
3737 }
3738
3739 #[test]
3740 fn get_ifc_root_layout_index_follows_membership_only_for_non_ifc_roots() {
3741 let mut tree = raw_tree(vec![hot(None), hot(Some(0))], &[vec![1], vec![]]);
3742 tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3743 empty_layout(),
3744 AvailableSpace::MaxContent,
3745 false,
3746 ));
3747 tree.warm[1].ifc_membership = Some(IfcMembership {
3748 ifc_id: IfcId(0),
3749 ifc_root_layout_index: 0,
3750 run_index: 0,
3751 });
3752
3753 assert_eq!(tree.get_ifc_root_layout_index(1), 0, "a text node anchors to its IFC root");
3754 assert_eq!(
3755 tree.get_ifc_root_layout_index(0),
3756 0,
3757 "the IFC root itself is its own anchor"
3758 );
3759
3760 tree.warm[0].ifc_membership = Some(IfcMembership {
3763 ifc_id: IfcId(9),
3764 ifc_root_layout_index: 1,
3765 run_index: 0,
3766 });
3767 assert_eq!(tree.get_ifc_root_layout_index(0), 0);
3768 }
3769
3770 #[test]
3771 fn get_inline_layout_for_node_walks_membership_then_gives_up_cleanly() {
3772 let mut tree = raw_tree(vec![hot(None), hot(Some(0)), hot(Some(0))], &[vec![1, 2]]);
3773 let arc = empty_layout();
3774 tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3775 Arc::clone(&arc),
3776 AvailableSpace::MaxContent,
3777 false,
3778 ));
3779 tree.warm[1].ifc_membership = Some(IfcMembership {
3780 ifc_id: IfcId(0),
3781 ifc_root_layout_index: 0,
3782 run_index: 0,
3783 });
3784 assert!(Arc::ptr_eq(tree.get_inline_layout_for_node(0).unwrap(), &arc));
3786 assert!(Arc::ptr_eq(tree.get_inline_layout_for_node(1).unwrap(), &arc));
3787 assert!(tree.get_inline_layout_for_node(2).is_none());
3788 }
3789
3790 #[test]
3791 fn get_inline_layout_for_node_is_none_when_membership_dangles() {
3792 let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3794 tree.warm[0].ifc_membership = Some(IfcMembership {
3795 ifc_id: IfcId(3),
3796 ifc_root_layout_index: usize::MAX,
3797 run_index: 0,
3798 });
3799 assert!(tree.get_inline_layout_for_node(0).is_none());
3800
3801 let mut tree = raw_tree(vec![hot(None), hot(Some(0))], &[vec![1], vec![]]);
3803 tree.warm[1].ifc_membership = Some(IfcMembership {
3804 ifc_id: IfcId(3),
3805 ifc_root_layout_index: 0,
3806 run_index: 0,
3807 });
3808 assert!(tree.get_inline_layout_for_node(1).is_none());
3809 }
3810
3811 fn dirty_tree() -> LayoutTree {
3817 raw_tree(
3818 vec![hot(None), hot(Some(0)), hot(Some(1)), hot(Some(1))],
3819 &[vec![1], vec![2, 3], vec![], vec![]],
3820 )
3821 }
3822
3823 #[test]
3824 fn mark_dirty_walks_up_to_the_root() {
3825 let mut tree = dirty_tree();
3826 tree.mark_dirty(2, DirtyFlag::Layout);
3827 assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3828 assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3829 assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3830 assert_eq!(
3831 tree.cold(3).unwrap().dirty_flag,
3832 DirtyFlag::None,
3833 "the sibling is untouched"
3834 );
3835 }
3836
3837 #[test]
3838 fn mark_dirty_with_flag_none_is_a_no_op() {
3839 let mut tree = dirty_tree();
3840 tree.mark_dirty(2, DirtyFlag::None);
3841 assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3842 }
3843
3844 #[test]
3845 fn mark_dirty_never_downgrades_an_existing_flag() {
3846 let mut tree = dirty_tree();
3847 tree.mark_dirty(2, DirtyFlag::Layout);
3848 tree.mark_dirty(2, DirtyFlag::Paint);
3849 assert_eq!(
3850 tree.cold(2).unwrap().dirty_flag,
3851 DirtyFlag::Layout,
3852 "Layout > Paint — a Paint request must not weaken it"
3853 );
3854 }
3855
3856 #[test]
3857 fn mark_dirty_upgrades_paint_to_layout_and_keeps_propagating() {
3858 let mut tree = dirty_tree();
3859 tree.mark_dirty(2, DirtyFlag::Paint);
3860 assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Paint);
3861 tree.mark_dirty(2, DirtyFlag::Layout);
3862 assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3863 assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3864 }
3865
3866 #[test]
3867 fn mark_dirty_stops_early_when_an_ancestor_is_already_at_least_as_dirty() {
3868 let mut tree = dirty_tree();
3869 tree.mark_dirty(3, DirtyFlag::Layout); tree.mark_dirty(2, DirtyFlag::Layout); assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3872 assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3873 }
3874
3875 #[test]
3876 fn mark_dirty_out_of_range_is_a_silent_no_op() {
3877 let mut tree = dirty_tree();
3878 tree.mark_dirty(usize::MAX, DirtyFlag::Layout);
3879 tree.mark_dirty(tree.nodes.len(), DirtyFlag::Layout);
3880 assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3881 }
3882
3883 #[test]
3884 fn mark_dirty_terminates_on_a_cyclic_parent_chain() {
3885 let mut tree = raw_tree(vec![hot(Some(1)), hot(Some(0))], &[vec![], vec![]]);
3888 tree.mark_dirty(0, DirtyFlag::Layout);
3889 assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3890 assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3891 }
3892
3893 #[test]
3894 fn mark_dirty_terminates_when_a_node_is_its_own_parent() {
3895 let mut tree = raw_tree(vec![hot(Some(0))], &[vec![]]);
3896 tree.mark_dirty(0, DirtyFlag::Layout);
3897 assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3898 }
3899
3900 #[test]
3901 fn mark_subtree_dirty_marks_descendants_but_not_ancestors_or_siblings() {
3902 let mut tree = dirty_tree();
3903 tree.mark_subtree_dirty(1, DirtyFlag::Layout);
3904 assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3905 assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3906 assert_eq!(tree.cold(3).unwrap().dirty_flag, DirtyFlag::Layout);
3907 assert_eq!(
3908 tree.cold(0).unwrap().dirty_flag,
3909 DirtyFlag::None,
3910 "mark_subtree_dirty walks DOWN only"
3911 );
3912 }
3913
3914 #[test]
3915 fn mark_subtree_dirty_with_none_or_a_bad_index_is_a_no_op() {
3916 let mut tree = dirty_tree();
3917 tree.mark_subtree_dirty(0, DirtyFlag::None);
3918 tree.mark_subtree_dirty(usize::MAX, DirtyFlag::Layout);
3919 assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3920 }
3921
3922 #[test]
3923 fn mark_subtree_dirty_does_not_downgrade() {
3924 let mut tree = dirty_tree();
3925 tree.mark_subtree_dirty(0, DirtyFlag::Layout);
3926 tree.mark_subtree_dirty(0, DirtyFlag::Paint);
3927 assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::Layout));
3928 }
3929
3930 #[test]
3931 fn clear_all_dirty_flags_resets_every_node() {
3932 let mut tree = dirty_tree();
3933 tree.mark_subtree_dirty(0, DirtyFlag::Layout);
3934 assert!(tree.cold.iter().any(|c| c.dirty_flag != DirtyFlag::None));
3935 tree.clear_all_dirty_flags();
3936 assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3937 }
3938
3939 #[test]
3940 fn clear_all_dirty_flags_on_an_empty_tree_does_not_panic() {
3941 let mut tree = raw_tree(Vec::new(), &[]);
3942 tree.clear_all_dirty_flags();
3943 assert!(tree.cold.is_empty());
3944 }
3945
3946 #[test]
3951 fn memory_report_total_is_the_sum_of_its_parts() {
3952 let tree = build_tree(&mixed_dom());
3953 let r = tree.memory_report();
3954 assert_eq!(r.node_count, tree.nodes.len());
3955 assert_eq!(
3956 r.total_bytes(),
3957 r.hot_bytes
3958 + r.warm_bytes
3959 + r.warm_inline_layout_bytes
3960 + r.warm_taffy_cache_bytes
3961 + r.cold_bytes
3962 + r.dom_to_layout_bytes
3963 + r.children_arena_bytes
3964 + r.children_offsets_bytes
3965 );
3966 assert!(r.hot_bytes >= r.node_count * size_of::<LayoutNodeHot>());
3967 assert!(r.total_bytes() > 0, "a non-empty tree retains something");
3968 }
3969
3970 #[test]
3971 fn memory_report_of_an_empty_tree_is_all_zero() {
3972 let tree = raw_tree(Vec::new(), &[]);
3973 let r = tree.memory_report();
3974 assert_eq!(r.node_count, 0);
3975 assert_eq!(r.total_bytes(), 0);
3976 }
3977
3978 #[test]
3979 fn memory_report_total_bytes_default_is_zero() {
3980 assert_eq!(LayoutTreeMemoryReport::default().total_bytes(), 0);
3981 }
3982
3983 #[test]
3984 fn memory_report_total_bytes_at_the_usize_boundary_does_not_overflow() {
3985 let eighth = usize::MAX / 8;
3988 let r = LayoutTreeMemoryReport {
3989 node_count: 0,
3990 hot_bytes: eighth,
3991 warm_bytes: eighth,
3992 warm_inline_layout_bytes: eighth,
3993 warm_taffy_cache_bytes: eighth,
3994 cold_bytes: eighth,
3995 dom_to_layout_bytes: eighth,
3996 children_arena_bytes: eighth,
3997 children_offsets_bytes: eighth,
3998 };
3999 assert_eq!(r.total_bytes(), eighth * 8);
4000 assert_eq!(r.total_bytes(), usize::MAX - 7);
4001 }
4002
4003 #[test]
4004 fn memory_report_counts_a_cached_inline_layout() {
4005 let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
4006 let bare = tree.memory_report().warm_inline_layout_bytes;
4007 assert_eq!(bare, 0);
4008
4009 tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
4010 layout_of(vec![tab_item(1.0, 1.0, 0.0, 0)]),
4011 AvailableSpace::MaxContent,
4012 false,
4013 ));
4014 assert!(
4015 tree.memory_report().warm_inline_layout_bytes >= size_of::<UnifiedLayout>(),
4016 "the UnifiedLayout header must at least be counted"
4017 );
4018 }
4019
4020 #[test]
4021 fn root_node_returns_the_hot_node_at_the_root_index() {
4022 let sd = mixed_dom();
4023 let tree = build_tree(&sd);
4024 let root = tree.root_node();
4025 assert_eq!(root.parent, None);
4026 assert_eq!(root.dom_node_id, sd.root.into_crate_internal());
4027 }
4028
4029 #[test]
4034 fn resolve_box_props_out_of_range_is_a_no_op() {
4035 let mut tree = build_tree(&mixed_dom());
4036 tree.resolve_box_props(usize::MAX, VIEWPORT, VIEWPORT, 16.0, 16.0);
4037 tree.resolve_box_props(tree.nodes.len(), VIEWPORT, VIEWPORT, 16.0, 16.0);
4038 }
4039
4040 #[test]
4041 fn resolve_box_props_keeps_the_stored_props_finite_for_nan_and_inf_inputs() {
4042 let sd = styled(
4043 Dom::create_body().with_child(div_class("m")),
4044 ".m { margin: 50%; padding: 10em; border: 1px solid black; }",
4045 );
4046 let mut tree = build_tree(&sd);
4047
4048 for (cb, vp, efs, rfs) in [
4049 (
4050 LogicalSize::new(f32::NAN, f32::NAN),
4051 LogicalSize::new(f32::NAN, f32::NAN),
4052 f32::NAN,
4053 f32::NAN,
4054 ),
4055 (
4056 LogicalSize::new(f32::INFINITY, f32::INFINITY),
4057 LogicalSize::new(f32::INFINITY, f32::INFINITY),
4058 f32::INFINITY,
4059 f32::INFINITY,
4060 ),
4061 (
4062 LogicalSize::new(f32::NEG_INFINITY, 0.0),
4063 LogicalSize::new(0.0, f32::NEG_INFINITY),
4064 f32::NEG_INFINITY,
4065 0.0,
4066 ),
4067 (
4068 LogicalSize::new(f32::MAX, f32::MAX),
4069 LogicalSize::new(f32::MAX, f32::MAX),
4070 f32::MAX,
4071 f32::MAX,
4072 ),
4073 (
4074 LogicalSize::new(0.0, 0.0),
4075 LogicalSize::new(0.0, 0.0),
4076 0.0,
4077 0.0,
4078 ),
4079 ] {
4080 tree.resolve_box_props(1, cb, vp, efs, rfs);
4081 let bp = tree.get(1).unwrap().box_props.unpack();
4082 for v in [
4083 bp.margin.top,
4084 bp.margin.right,
4085 bp.margin.bottom,
4086 bp.margin.left,
4087 bp.padding.top,
4088 bp.padding.left,
4089 bp.border.top,
4090 bp.border.left,
4091 ] {
4092 assert!(
4093 v.is_finite(),
4094 "the i16×10 packing must launder NaN/inf into a finite value, got {v}"
4095 );
4096 assert!(
4097 (-3277.0..=3277.0).contains(&v),
4098 "packed edges are clamped to ±3276.8px, got {v}"
4099 );
4100 }
4101 }
4102 }
4103
4104 #[test]
4105 fn resolve_box_props_resolves_percentages_against_the_containing_block() {
4106 let sd = styled(
4107 Dom::create_body().with_child(div_class("m")),
4108 ".m { margin-left: 50%; }",
4109 );
4110 let mut tree = build_tree(&sd);
4111 tree.resolve_box_props(1, LogicalSize::new(200.0, 100.0), VIEWPORT, 16.0, 16.0);
4112 let bp = tree.get(1).unwrap().box_props.unpack();
4113 assert!(
4114 (bp.margin.left - 100.0).abs() < 0.2,
4115 "50% of a 200px containing block ≈ 100px, got {}",
4116 bp.margin.left
4117 );
4118 }
4119
4120 #[test]
4125 fn a_whitespace_only_inline_run_generates_no_anonymous_box() {
4126 let sd = mixed_dom();
4127 let tree = build_tree(&sd);
4128 let ws = text_node(&sd, " \n\t");
4129 assert!(
4130 !tree.dom_to_layout.contains_key(&ws),
4131 "CSS 2.1 §9.2.2.1: collapsible whitespace generates no box"
4132 );
4133 assert!(
4134 tree.nodes.iter().all(|n| n.dom_node_id != Some(ws)),
4135 "…and no layout node references it"
4136 );
4137 }
4138
4139 #[test]
4140 fn a_real_inline_run_next_to_a_block_sibling_gets_exactly_one_anonymous_wrapper() {
4141 let sd = mixed_dom();
4142 let tree = build_tree(&sd);
4143 let wrappers: Vec<usize> = (0..tree.nodes.len())
4144 .filter(|&i| {
4145 tree.cold(i).unwrap().anonymous_type == Some(AnonymousBoxType::InlineWrapper)
4146 })
4147 .collect();
4148 assert_eq!(
4149 wrappers.len(),
4150 1,
4151 "only the trailing `tail` run needs wrapping"
4152 );
4153
4154 let w = wrappers[0];
4155 assert_eq!(tree.get(w).unwrap().dom_node_id, None, "anon boxes have no DOM node");
4156 assert_eq!(tree.cold(w).unwrap().dirty_flag, DirtyFlag::Layout);
4157 let tail = text_node(&sd, "tail");
4158 let kids = tree.children(w);
4159 assert_eq!(kids.len(), 1);
4160 assert_eq!(tree.get(kids[0]).unwrap().dom_node_id, Some(tail));
4161 }
4162
4163 #[test]
4164 fn an_all_inline_block_container_gets_no_anonymous_wrapper() {
4165 let sd = mixed_dom();
4166 let tree = build_tree(&sd);
4167 let block_idx = (0..tree.nodes.len())
4170 .find(|&i| tree.get(i).unwrap().dom_node_id == Some(NodeId::new(1)))
4171 .expect("the .block layout node");
4172 let kids = tree.children(block_idx);
4173 assert_eq!(kids.len(), 2, "the text run and the inline div, unwrapped");
4174 assert!(
4175 kids.iter()
4176 .all(|&c| tree.cold(c).unwrap().anonymous_type.is_none()),
4177 "an all-inline block container needs no anonymous wrapper"
4178 );
4179 assert_eq!(
4180 tree.get(block_idx).unwrap().formatting_context,
4181 FormattingContext::Inline,
4182 "it establishes an IFC instead"
4183 );
4184 }
4185
4186 #[test]
4187 fn the_marker_pseudo_element_is_inserted_as_the_first_child_of_a_list_item() {
4188 let sd = styled(
4189 Dom::create_body().with_child(div_class("li").with_child(Dom::create_text("item"))),
4190 ".li { display: list-item; }",
4191 );
4192 let tree = build_tree(&sd);
4193
4194 let marker = (0..tree.nodes.len())
4195 .find(|&i| tree.warm(i).unwrap().pseudo_element == Some(PseudoElement::Marker))
4196 .expect("display:list-item must generate a ::marker");
4197 let li = tree.get(marker).unwrap().parent.expect("marker has a parent");
4198 assert_eq!(
4199 tree.children(li)[0],
4200 marker,
4201 "CSS Lists 3 §3.1: ::marker is the FIRST child"
4202 );
4203 assert_eq!(
4204 tree.get(marker).unwrap().dom_node_id,
4205 tree.get(li).unwrap().dom_node_id,
4206 "the marker shares the list-item's DOM node for counter/style resolution"
4207 );
4208 assert_eq!(tree.get(marker).unwrap().formatting_context, FormattingContext::Inline);
4209 assert!(
4210 tree.dom_to_layout[&tree.get(li).unwrap().dom_node_id.unwrap()].contains(&marker),
4211 "the marker is registered in dom_to_layout for counter resolution"
4212 );
4213 }
4214
4215 #[test]
4216 fn display_none_children_never_reach_the_layout_tree() {
4217 let sd = styled(
4218 Dom::create_body()
4219 .with_child(div_class("gone"))
4220 .with_child(div_class("here")),
4221 ".gone { display: none; } .here { display: block; }",
4222 );
4223 let tree = build_tree(&sd);
4224 assert_eq!(
4225 tree.children(tree.root).len(),
4226 1,
4227 "display:none generates no box"
4228 );
4229 }
4230
4231 #[test]
4232 fn display_contents_promotes_its_children_to_the_grandparent() {
4233 let sd = styled(
4234 Dom::create_body().with_child(div_class("c").with_child(div_class("kid"))),
4235 ".c { display: contents; } .kid { display: block; }",
4236 );
4237 let tree = build_tree(&sd);
4238 let root_kids = tree.children(tree.root);
4240 assert!(
4241 root_kids
4242 .iter()
4243 .any(|&i| tree.warm(i).unwrap().computed_style.display == LayoutDisplay::Block),
4244 "the promoted child must be a direct child of the root"
4245 );
4246 }
4247
4248 #[test]
4249 fn a_table_with_a_bare_cell_gets_an_anonymous_row() {
4250 let sd = styled(
4251 Dom::create_body().with_child(div_class("t").with_child(div_class("cell"))),
4252 ".t { display: table; } .cell { display: table-cell; }",
4253 );
4254 let tree = build_tree(&sd);
4255 assert!(
4256 (0..tree.nodes.len()).any(|i| {
4257 tree.cold(i).unwrap().anonymous_type == Some(AnonymousBoxType::TableRow)
4258 }),
4259 "CSS 2.2 §17.2.1 stage 2: a non-proper table child is wrapped in an anonymous row"
4260 );
4261 }
4262
4263 #[test]
4264 fn whitespace_between_table_rows_is_dropped_not_wrapped() {
4265 let sd = styled(
4266 Dom::create_body().with_child(
4267 div_class("t")
4268 .with_child(Dom::create_text(" "))
4269 .with_child(div_class("row")),
4270 ),
4271 ".t { display: table; } .row { display: table-row; }",
4272 );
4273 let tree = build_tree(&sd);
4274 let ws = text_node(&sd, " ");
4275 assert!(
4276 !tree.dom_to_layout.contains_key(&ws),
4277 "stage 1: irrelevant (whitespace) boxes are removed"
4278 );
4279 assert!(
4280 !(0..tree.nodes.len())
4281 .any(|i| tree.cold(i).unwrap().anonymous_type == Some(AnonymousBoxType::TableRow)),
4282 "and no anonymous row is generated for it"
4283 );
4284 }
4285
4286 #[test]
4287 fn table_column_children_are_suppressed_entirely() {
4288 let sd = styled(
4289 Dom::create_body().with_child(div_class("col").with_child(div_class("kid"))),
4290 ".col { display: table-column; } .kid { display: block; }",
4291 );
4292 let tree = build_tree(&sd);
4293 let col = tree.children(tree.root)[0];
4295 assert!(tree.children(col).is_empty());
4296 }
4297
4298 #[test]
4303 fn builder_new_starts_completely_empty() {
4304 let b = LayoutTreeBuilder::new(VIEWPORT);
4305 assert!(b.get(0).is_none());
4306 assert!(b.get(usize::MAX).is_none());
4307 assert!(b.nodes.is_empty());
4308 assert!(b.dom_to_layout.is_empty());
4309 assert_eq!(b.viewport_size, VIEWPORT);
4310 }
4311
4312 #[test]
4313 fn builder_new_accepts_degenerate_viewports() {
4314 for vp in [
4315 LogicalSize::new(0.0, 0.0),
4316 LogicalSize::new(-1.0, -1.0),
4317 LogicalSize::new(f32::MAX, f32::MAX),
4318 LogicalSize::new(f32::NAN, f32::INFINITY),
4319 ] {
4320 let b = LayoutTreeBuilder::new(vp);
4321 assert!(b.nodes.is_empty());
4322 }
4323 }
4324
4325 #[test]
4326 fn builder_get_and_get_mut_are_none_out_of_range() {
4327 let sd = mixed_dom();
4328 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4329 let mut msgs = None;
4330 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4331 assert_eq!(root, 0);
4332 assert!(b.get(0).is_some());
4333 assert!(b.get_mut(0).is_some());
4334 for idx in [1, usize::MAX, usize::MAX - 1] {
4335 assert!(b.get(idx).is_none(), "get({idx})");
4336 assert!(b.get_mut(idx).is_none(), "get_mut({idx})");
4337 }
4338 }
4339
4340 #[test]
4341 fn create_anonymous_node_wires_up_parent_children_and_cold_defaults() {
4342 let sd = mixed_dom();
4343 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4344 let mut msgs = None;
4345 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4346 let root_fc = b.get(root).unwrap().formatting_context;
4347
4348 let anon = b.create_anonymous_node(root, AnonymousBoxType::TableCell, FormattingContext::TableCell);
4349 assert_eq!(anon, 1, "anon nodes are appended");
4350
4351 let n = b.get(anon).unwrap();
4352 assert_eq!(n.dom_node_id, None, "anonymous ⇒ no DOM node");
4353 assert_eq!(n.anonymous_type, Some(AnonymousBoxType::TableCell));
4354 assert_eq!(n.formatting_context, FormattingContext::TableCell);
4355 assert_eq!(n.parent, Some(root));
4356 assert_eq!(n.parent_formatting_context, Some(root_fc));
4357 assert_eq!(n.dirty_flag, DirtyFlag::Layout, "a fresh box needs layout");
4358 assert!(n.children.is_empty());
4359 assert_eq!(n.subtree_hash, SubtreeHash(0));
4360 assert!(n.ifc_id.is_none());
4361 assert_eq!(b.get(root).unwrap().children, vec![anon]);
4362 assert!(
4363 b.dom_to_layout.values().all(|v| !v.contains(&anon)),
4364 "anon boxes are never registered in dom_to_layout"
4365 );
4366 }
4367
4368 #[test]
4369 fn create_anonymous_node_appends_in_call_order() {
4370 let sd = mixed_dom();
4371 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4372 let mut msgs = None;
4373 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4374 let a = b.create_anonymous_node(root, AnonymousBoxType::TableRow, FormattingContext::TableRow);
4375 let c = b.create_anonymous_node(root, AnonymousBoxType::TableCell, FormattingContext::TableCell);
4376 assert_eq!((a, c), (1, 2));
4377 assert_eq!(b.get(root).unwrap().children, vec![a, c]);
4378 }
4379
4380 #[test]
4381 fn create_node_from_dom_registers_the_dom_mapping_and_the_parent_link() {
4382 let sd = mixed_dom();
4383 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4384 let mut msgs = None;
4385 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4386 let child = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4387
4388 assert_eq!(b.get(child).unwrap().dom_node_id, Some(NodeId::new(1)));
4389 assert_eq!(b.get(child).unwrap().parent, Some(root));
4390 assert_eq!(b.get(root).unwrap().children, vec![child]);
4391 assert_eq!(b.dom_to_layout[&NodeId::new(1)], vec![child]);
4392 assert_eq!(b.get(child).unwrap().dirty_flag, DirtyFlag::Layout);
4393 }
4394
4395 #[test]
4396 fn create_node_from_dom_turns_the_roots_visible_overflow_into_auto() {
4397 let sd = styled(Dom::create_body().with_child(div_class("d")), ".d { display: block; }");
4399 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4400 let mut msgs = None;
4401 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4402 let child = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4403
4404 let root_style = &b.get(root).unwrap().computed_style;
4405 assert_ne!(root_style.overflow_x, LayoutOverflow::Visible);
4406 assert_ne!(root_style.overflow_y, LayoutOverflow::Visible);
4407
4408 let child_style = &b.get(child).unwrap().computed_style;
4409 assert_eq!(
4410 child_style.overflow_x,
4411 LayoutOverflow::Visible,
4412 "the rule applies to the viewport only, not to every node"
4413 );
4414 }
4415
4416 #[test]
4417 fn clone_node_from_old_resets_children_and_dirty_state() {
4418 let sd = mixed_dom();
4419 let tree = build_tree(&sd);
4420 let old_root = tree.get_full_node(0).unwrap();
4421 let old_child = tree.get_full_node(1).unwrap();
4422 assert!(!old_root.children.is_empty(), "the source root has children");
4423
4424 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4425 let root = b.clone_node_from_old(&old_root, None);
4426 let child = b.clone_node_from_old(&old_child, Some(root));
4427 assert_eq!((root, child), (0, 1));
4428
4429 assert!(
4430 b.get(root).unwrap().children == vec![child],
4431 "the clone's children come only from later clone calls"
4432 );
4433 assert!(b.get(child).unwrap().children.is_empty());
4434 assert_eq!(b.get(child).unwrap().parent, Some(root));
4435 assert_eq!(b.get(child).unwrap().dirty_flag, DirtyFlag::None);
4436 let root_fc = b.get(root).unwrap().formatting_context;
4437 assert_eq!(b.get(child).unwrap().parent_formatting_context, Some(root_fc));
4438 }
4439
4440 #[test]
4441 fn clone_node_from_old_skips_dom_registration_for_anonymous_nodes() {
4442 let sd = mixed_dom();
4443 let tree = build_tree(&sd);
4444 let anon = (0..tree.nodes.len())
4445 .find(|&i| tree.cold(i).unwrap().anonymous_type.is_some())
4446 .expect("mixed_dom generates one anonymous wrapper");
4447 let old = tree.get_full_node(anon).unwrap();
4448 assert_eq!(old.dom_node_id, None);
4449
4450 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4451 let idx = b.clone_node_from_old(&old, None);
4452 assert_eq!(idx, 0);
4453 assert!(
4454 b.dom_to_layout.is_empty(),
4455 "a node with no dom_node_id must not create a mapping entry"
4456 );
4457 }
4458
4459 #[test]
4460 fn build_flattens_children_into_the_arena_losslessly() {
4461 let sd = mixed_dom();
4462 let mut builder = LayoutTreeBuilder::new(VIEWPORT);
4463 let mut msgs = None;
4464 let root_id = sd.root.into_crate_internal().unwrap_or(NodeId::ZERO);
4465 let root = builder.process_node(&sd, root_id, None, &mut msgs).unwrap();
4466 let expected: Vec<Vec<usize>> = builder.nodes.iter().map(|n| n.children.clone()).collect();
4467
4468 let tree = builder.build(root);
4469 assert_eq!(tree.nodes.len(), expected.len());
4470 assert_eq!(tree.warm.len(), expected.len());
4471 assert_eq!(tree.cold.len(), expected.len());
4472 for (i, want) in expected.iter().enumerate() {
4473 assert_eq!(tree.children(i), want.as_slice(), "node {i}");
4474 }
4475 }
4476
4477 #[test]
4478 fn build_on_an_empty_builder_yields_an_empty_tree() {
4479 let tree = LayoutTreeBuilder::new(VIEWPORT).build(0);
4480 assert!(tree.nodes.is_empty());
4481 assert!(tree.children_arena.is_empty());
4482 assert!(tree.children_offsets.is_empty());
4483 assert!(tree.subtree_needs_intrinsic.is_empty());
4484 assert!(tree.get(0).is_none());
4485 assert!(tree.children(0).is_empty());
4486 assert_eq!(tree.get_content_size(0), LogicalSize::default());
4487 assert_eq!(tree.memory_report().node_count, 0);
4488 }
4489
4490 #[test]
4491 fn build_with_an_out_of_range_root_index_does_not_panic() {
4492 let sd = mixed_dom();
4493 let mut builder = LayoutTreeBuilder::new(VIEWPORT);
4494 let mut msgs = None;
4495 builder.process_node(&sd, NodeId::ZERO, None, &mut msgs).unwrap();
4496
4497 let tree = builder.build(usize::MAX);
4498 assert_eq!(tree.root, usize::MAX, "build() stores the index verbatim");
4499 assert!(tree.get(tree.root).is_none());
4500 assert!(tree.children(tree.root).is_empty());
4501 assert_eq!(tree.get_ifc_root_layout_index(tree.root), usize::MAX);
4502 }
4503
4504 #[test]
4505 fn blockify_node_display_blockifies_an_inline_flex_item() {
4506 let sd = styled(
4507 Dom::create_body().with_child(div_class("f").with_child(div_class("i"))),
4508 ".f { display: flex; } .i { display: inline; }",
4509 );
4510 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4511 let mut msgs = None;
4512 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4513 let flex = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4514 let item = b.create_node_from_dom(&sd, NodeId::new(2), Some(flex), &mut msgs);
4515
4516 assert_eq!(b.get(flex).unwrap().formatting_context, FormattingContext::Flex);
4517 assert_eq!(b.get(item).unwrap().computed_style.display, LayoutDisplay::Inline);
4518
4519 b.blockify_node_display(&sd, NodeId::new(2), item, Some(flex));
4520
4521 assert_eq!(
4522 b.get(item).unwrap().computed_style.display,
4523 LayoutDisplay::Block,
4524 "CSS Display 3 §2.7: a flex item's inline display blockifies"
4525 );
4526 assert!(matches!(
4527 b.get(item).unwrap().formatting_context,
4528 FormattingContext::Block { .. }
4529 ));
4530 }
4531
4532 #[test]
4533 fn blockify_node_display_leaves_a_plain_block_child_alone() {
4534 let sd = styled(
4535 Dom::create_body().with_child(div_class("p").with_child(div_class("b"))),
4536 ".p { display: block; } .b { display: block; }",
4537 );
4538 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4539 let mut msgs = None;
4540 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4541 let p = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4542 let child = b.create_node_from_dom(&sd, NodeId::new(2), Some(p), &mut msgs);
4543 let before = b.get(child).unwrap().formatting_context;
4544
4545 b.blockify_node_display(&sd, NodeId::new(2), child, Some(p));
4546 assert_eq!(b.get(child).unwrap().computed_style.display, LayoutDisplay::Block);
4547 assert_eq!(b.get(child).unwrap().formatting_context, before);
4548 }
4549
4550 #[test]
4551 fn blockify_node_display_with_a_bogus_node_index_is_a_no_op() {
4552 let sd = mixed_dom();
4553 let mut b = LayoutTreeBuilder::new(VIEWPORT);
4554 let mut msgs = None;
4555 let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4556 b.blockify_node_display(&sd, NodeId::ZERO, usize::MAX, None);
4559 b.blockify_node_display(&sd, NodeId::ZERO, 999, Some(usize::MAX));
4560 assert_eq!(b.nodes.len(), 1);
4561 assert!(b.get(root).is_some());
4562 }
4563
4564 #[test]
4569 fn blockify_flex_item_rewrites_every_table_internal_context() {
4570 let tree = build_tree(&mixed_dom());
4571 let mut nodes = vec![tree.get_full_node(0).unwrap()];
4572
4573 for fc in [
4574 FormattingContext::TableCell,
4575 FormattingContext::TableRow,
4576 FormattingContext::TableRowGroup,
4577 FormattingContext::TableColumnGroup,
4578 FormattingContext::TableCaption,
4579 FormattingContext::Table,
4580 ] {
4581 nodes[0].formatting_context = fc;
4582 blockify_flex_item_if_table_internal(&mut nodes, 0);
4583 assert_eq!(
4584 nodes[0].formatting_context,
4585 FormattingContext::Block {
4586 establishes_new_context: true
4587 },
4588 "{fc:?} is table-internal and must blockify"
4589 );
4590 }
4591 }
4592
4593 #[test]
4594 fn blockify_flex_item_leaves_non_table_contexts_untouched() {
4595 let tree = build_tree(&mixed_dom());
4596 let mut nodes = vec![tree.get_full_node(0).unwrap()];
4597
4598 for fc in [
4599 FormattingContext::Inline,
4600 FormattingContext::InlineBlock,
4601 FormattingContext::Flex,
4602 FormattingContext::Grid,
4603 FormattingContext::None,
4604 FormattingContext::Contents,
4605 FormattingContext::Block {
4606 establishes_new_context: false,
4607 },
4608 ] {
4609 nodes[0].formatting_context = fc;
4610 blockify_flex_item_if_table_internal(&mut nodes, 0);
4611 assert_eq!(nodes[0].formatting_context, fc, "{fc:?} must be left alone");
4612 }
4613 }
4614
4615 #[test]
4616 fn blockify_flex_item_out_of_range_or_empty_is_a_no_op() {
4617 let tree = build_tree(&mixed_dom());
4618 let mut nodes = vec![tree.get_full_node(0).unwrap()];
4619 nodes[0].formatting_context = FormattingContext::TableCell;
4620
4621 blockify_flex_item_if_table_internal(&mut nodes, 1);
4622 blockify_flex_item_if_table_internal(&mut nodes, usize::MAX);
4623 blockify_flex_item_if_table_internal(&mut [], 0);
4624 blockify_flex_item_if_table_internal(&mut [], usize::MAX);
4625 assert_eq!(nodes[0].formatting_context, FormattingContext::TableCell);
4626 }
4627
4628 #[test]
4629 fn table_cell_flex_items_do_not_produce_anonymous_table_boxes() {
4630 let sd = styled(
4633 Dom::create_body().with_child(
4634 div_class("f")
4635 .with_child(div_class("c"))
4636 .with_child(div_class("c")),
4637 ),
4638 ".f { display: flex; } .c { display: table-cell; }",
4639 );
4640 let tree = build_tree(&sd);
4641 assert!(
4642 (0..tree.nodes.len()).all(|i| tree.cold(i).unwrap().anonymous_type.is_none()),
4643 "no anonymous table boxes for blockified flex items"
4644 );
4645 let flex = tree.children(tree.root)[0];
4646 for &c in tree.children(flex) {
4647 assert!(matches!(
4648 tree.get(c).unwrap().formatting_context,
4649 FormattingContext::Block { .. }
4650 ));
4651 }
4652 }
4653
4654 #[test]
4659 fn is_shrink_to_fit_context_is_true_for_the_intrinsic_reading_contexts() {
4660 let sd = mixed_dom();
4661 for fc in [
4662 FormattingContext::Flex,
4663 FormattingContext::Grid,
4664 FormattingContext::Table,
4665 FormattingContext::InlineBlock,
4666 ] {
4667 assert!(
4668 is_shrink_to_fit_context(&sd, None, fc),
4669 "{fc:?} sizes from children's intrinsics"
4670 );
4671 }
4672 }
4673
4674 #[test]
4675 fn is_shrink_to_fit_context_is_false_for_a_plain_block_with_no_dom_node() {
4676 let sd = mixed_dom();
4677 for fc in [
4678 FormattingContext::Block {
4679 establishes_new_context: false,
4680 },
4681 FormattingContext::Block {
4682 establishes_new_context: true,
4683 },
4684 FormattingContext::Inline,
4685 FormattingContext::None,
4686 FormattingContext::TableRow,
4687 ] {
4688 assert!(
4689 !is_shrink_to_fit_context(&sd, None, fc),
4690 "{fc:?} with no DOM node cannot be float/abspos ⇒ not STF"
4691 );
4692 }
4693 }
4694
4695 #[test]
4696 fn is_shrink_to_fit_context_catches_floats_and_abspos() {
4697 let sd = styled(
4698 Dom::create_body()
4699 .with_child(div_class("fl"))
4700 .with_child(div_class("ab"))
4701 .with_child(div_class("fx"))
4702 .with_child(div_class("plain")),
4703 ".fl { float: left; } .ab { position: absolute; } .fx { position: fixed; } .plain { \
4704 display: block; }",
4705 );
4706 let block = FormattingContext::Block {
4707 establishes_new_context: false,
4708 };
4709 assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(1)), block), "float:left");
4710 assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(2)), block), "position:absolute");
4711 assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(3)), block), "position:fixed");
4712 assert!(
4713 !is_shrink_to_fit_context(&sd, Some(NodeId::new(4)), block),
4714 "an in-flow static block is sized top-down ⇒ not STF"
4715 );
4716 }
4717
4718 #[test]
4719 fn compute_subtree_needs_intrinsic_is_one_bit_per_node() {
4720 let sd = mixed_dom();
4721 let tree = build_tree(&sd);
4722 let bits = compute_subtree_needs_intrinsic(&sd, &tree);
4723 assert_eq!(bits.len(), tree.nodes.len());
4724 assert_eq!(tree.subtree_needs_intrinsic.len(), tree.nodes.len());
4725 }
4726
4727 #[test]
4728 fn compute_subtree_needs_intrinsic_is_all_false_for_a_pure_block_tree() {
4729 let sd = mixed_dom();
4730 let tree = build_tree(&sd);
4731 assert!(
4732 compute_subtree_needs_intrinsic(&sd, &tree)
4733 .iter()
4734 .all(|b| !b),
4735 "nothing in mixed_dom() is flex/grid/table/float/abspos"
4736 );
4737 }
4738
4739 #[test]
4740 fn compute_subtree_needs_intrinsic_propagates_a_deep_flex_up_to_the_root() {
4741 let sd = styled(
4742 Dom::create_body().with_child(
4743 div_class("a")
4744 .with_child(div_class("b").with_child(div_class("f"))),
4745 ),
4746 ".a { display: block; } .b { display: block; } .f { display: flex; }",
4747 );
4748 let tree = build_tree(&sd);
4749 let bits = compute_subtree_needs_intrinsic(&sd, &tree);
4750 assert!(bits[tree.root], "out[i] = self || any(children) — must reach the root");
4751 assert!(bits.iter().all(|b| *b), "every node on the chain is on the flex path");
4752 }
4753
4754 #[test]
4755 fn compute_subtree_needs_intrinsic_leaves_a_flex_free_sibling_branch_false() {
4756 let sd = styled(
4757 Dom::create_body()
4758 .with_child(div_class("f"))
4759 .with_child(div_class("plain")),
4760 ".f { display: flex; } .plain { display: block; }",
4761 );
4762 let tree = build_tree(&sd);
4763 let bits = compute_subtree_needs_intrinsic(&sd, &tree);
4764 let kids = tree.children(tree.root);
4765 let flex = kids
4766 .iter()
4767 .copied()
4768 .find(|&i| tree.get(i).unwrap().formatting_context == FormattingContext::Flex)
4769 .expect("the flex child");
4770 let plain = kids.iter().copied().find(|&i| i != flex).expect("the plain child");
4771 assert!(bits[flex]);
4772 assert!(!bits[plain], "a sibling that reads no intrinsics stays false");
4773 assert!(bits[tree.root], "…but the root still sees the flex branch");
4774 }
4775
4776 #[test]
4777 fn compute_subtree_needs_intrinsic_on_an_empty_tree_is_empty() {
4778 let sd = mixed_dom();
4779 let tree = raw_tree(Vec::new(), &[]);
4780 assert!(compute_subtree_needs_intrinsic(&sd, &tree).is_empty());
4781 }
4782
4783 #[test]
4788 fn is_block_level_matches_the_block_level_display_values() {
4789 let sd = styled(
4790 Dom::create_body()
4791 .with_child(div_class("b"))
4792 .with_child(div_class("i")),
4793 ".b { display: block; } .i { display: inline; }",
4794 );
4795 assert!(is_block_level(&sd, NodeId::new(1)));
4796 assert!(!is_block_level(&sd, NodeId::new(2)));
4797 }
4798
4799 #[test]
4800 fn is_block_level_covers_the_table_and_list_item_families() {
4801 for (css_display, want) in [
4802 ("block", true),
4803 ("flow-root", true),
4804 ("flex", true),
4805 ("grid", true),
4806 ("table", true),
4807 ("table-row", true),
4808 ("table-cell", true),
4809 ("table-caption", true),
4810 ("list-item", true),
4811 ("inline", false),
4812 ("inline-block", false),
4813 ("inline-flex", false),
4814 ("inline-grid", false),
4815 ("inline-table", false),
4816 ("none", false),
4817 ] {
4818 let sd = styled(
4819 Dom::create_body().with_child(div_class("x")),
4820 &format!(".x {{ display: {css_display}; }}"),
4821 );
4822 assert_eq!(
4823 is_block_level(&sd, NodeId::new(1)),
4824 want,
4825 "display:{css_display}"
4826 );
4827 }
4828 }
4829
4830 #[test]
4831 fn is_inline_level_is_always_true_for_text_regardless_of_display() {
4832 let sd = styled(
4833 Dom::create_body().with_child(div_class("b").with_child(Dom::create_text("t"))),
4834 ".b { display: block; }",
4835 );
4836 let t = text_node(&sd, "t");
4837 assert!(is_inline_level(&sd, t), "text nodes are inline-level by definition");
4838 assert!(!is_inline_level(&sd, NodeId::new(1)), "the block div is not");
4839 }
4840
4841 #[test]
4842 fn is_inline_level_matches_the_inline_display_family() {
4843 for (css_display, want) in [
4844 ("inline", true),
4845 ("inline-block", true),
4846 ("inline-table", true),
4847 ("inline-flex", true),
4848 ("inline-grid", true),
4849 ("block", false),
4850 ("flex", false),
4851 ("table", false),
4852 ("list-item", false),
4853 ] {
4854 let sd = styled(
4855 Dom::create_body().with_child(div_class("x")),
4856 &format!(".x {{ display: {css_display}; }}"),
4857 );
4858 assert_eq!(
4859 is_inline_level(&sd, NodeId::new(1)),
4860 want,
4861 "display:{css_display}"
4862 );
4863 }
4864 }
4865
4866 #[test]
4867 fn block_and_inline_level_are_mutually_exclusive_for_element_nodes() {
4868 for css_display in [
4869 "block",
4870 "inline",
4871 "inline-block",
4872 "flex",
4873 "inline-flex",
4874 "grid",
4875 "table",
4876 "list-item",
4877 ] {
4878 let sd = styled(
4879 Dom::create_body().with_child(div_class("x")),
4880 &format!(".x {{ display: {css_display}; }}"),
4881 );
4882 let id = NodeId::new(1);
4883 assert!(
4884 !(is_block_level(&sd, id) && is_inline_level(&sd, id)),
4885 "display:{css_display} cannot be both block- and inline-level"
4886 );
4887 }
4888 }
4889
4890 #[test]
4891 fn has_only_inline_children_is_false_for_a_childless_node() {
4892 let sd = styled(Dom::create_body().with_child(div_class("e")), ".e { display: block; }");
4893 assert!(
4894 !has_only_inline_children(&sd, NodeId::new(1)),
4895 "no children ⇒ no IFC (it's empty, not inline)"
4896 );
4897 }
4898
4899 #[test]
4900 fn has_only_inline_children_is_true_for_an_all_inline_run() {
4901 let sd = mixed_dom();
4902 assert!(has_only_inline_children(&sd, NodeId::new(1)));
4904 }
4905
4906 #[test]
4907 fn has_only_inline_children_is_false_as_soon_as_one_block_child_appears() {
4908 let sd = mixed_dom();
4909 assert!(!has_only_inline_children(&sd, NodeId::new(5)));
4911 }
4912
4913 #[test]
4914 fn has_only_inline_children_is_false_for_an_out_of_range_node_id() {
4915 let sd = mixed_dom();
4916 let past_end = NodeId::new(sd.node_data.len() + 10);
4917 assert!(
4918 !has_only_inline_children(&sd, past_end),
4919 "the hierarchy lookup is a `.get`, so a bogus id must be false, not a panic"
4920 );
4921 assert!(!has_only_inline_children(&sd, NodeId::new(usize::MAX / 2)));
4922 }
4923
4924 fn ws_dom(text: &str, css: &str) -> StyledDom {
4929 styled(
4930 Dom::create_body().with_child(div_class("p").with_child(Dom::create_text(text))),
4931 css,
4932 )
4933 }
4934
4935 #[test]
4936 fn is_whitespace_only_text_recognises_the_css_document_whitespace_set() {
4937 for text in [" ", "\t", "\n", "\r", "\u{000C}", " \t\r\n\u{000C} "] {
4939 let sd = ws_dom(text, "");
4940 let id = NodeId::new(2);
4941 assert!(
4942 is_whitespace_only_text(&sd, id),
4943 "{text:?} is collapsible document whitespace"
4944 );
4945 }
4946 }
4947
4948 #[test]
4949 fn is_whitespace_only_text_rejects_unicode_spaces_that_css_does_not_collapse() {
4950 for text in [
4953 "\u{00A0}",
4954 "\u{3000}",
4955 "\u{2002}",
4956 "\u{2003}",
4957 "\u{200B}",
4958 "\u{2028}",
4959 " \u{00A0} ",
4960 ] {
4961 let sd = ws_dom(text, "");
4962 assert!(
4963 !is_whitespace_only_text(&sd, NodeId::new(2)),
4964 "{text:?} must NOT be treated as collapsible whitespace"
4965 );
4966 }
4967 }
4968
4969 #[test]
4970 fn is_whitespace_only_text_is_false_for_real_text() {
4971 for text in ["hi", " hi ", "\u{1F600}", "a\nb"] {
4972 let sd = ws_dom(text, "");
4973 assert!(!is_whitespace_only_text(&sd, NodeId::new(2)), "{text:?}");
4974 }
4975 }
4976
4977 #[test]
4978 fn is_whitespace_only_text_treats_the_empty_string_as_whitespace() {
4979 let sd = ws_dom("", "");
4982 assert!(is_whitespace_only_text(&sd, NodeId::new(2)));
4983 }
4984
4985 #[test]
4986 fn is_whitespace_only_text_respects_whitespace_preserving_modes() {
4987 for (ws, collapses) in [
4988 ("normal", true),
4989 ("nowrap", true),
4990 ("pre-line", true),
4991 ("pre", false),
4992 ("pre-wrap", false),
4993 ("break-spaces", false),
4994 ] {
4995 let sd = ws_dom(" \n ", &format!(".p {{ white-space: {ws}; }}"));
4996 assert_eq!(
4997 is_whitespace_only_text(&sd, NodeId::new(2)),
4998 collapses,
4999 "white-space:{ws} — preserved whitespace still generates a box"
5000 );
5001 }
5002 }
5003
5004 #[test]
5005 fn is_whitespace_only_text_is_false_for_non_text_and_bogus_nodes() {
5006 let sd = mixed_dom();
5007 assert!(!is_whitespace_only_text(&sd, NodeId::new(1)), "a div is not text");
5008 assert!(!is_whitespace_only_text(&sd, NodeId::ZERO), "the body is not text");
5009 let past_end = NodeId::new(sd.node_data.len() + 1);
5010 assert!(
5011 !is_whitespace_only_text(&sd, past_end),
5012 "an out-of-range id must return false, not panic"
5013 );
5014 assert!(!is_whitespace_only_text(&sd, NodeId::new(usize::MAX / 2)));
5015 }
5016
5017 #[test]
5022 fn should_skip_for_table_structure_only_fires_inside_table_parents() {
5023 let sd = ws_dom(" ", "");
5024 let ws = NodeId::new(2);
5025 for parent in [
5026 LayoutDisplay::Table,
5027 LayoutDisplay::InlineTable,
5028 LayoutDisplay::TableRowGroup,
5029 LayoutDisplay::TableHeaderGroup,
5030 LayoutDisplay::TableFooterGroup,
5031 LayoutDisplay::TableRow,
5032 ] {
5033 assert!(
5034 should_skip_for_table_structure(&sd, ws, parent),
5035 "whitespace under {parent:?} is an irrelevant box"
5036 );
5037 }
5038 for parent in [
5039 LayoutDisplay::Block,
5040 LayoutDisplay::Inline,
5041 LayoutDisplay::Flex,
5042 LayoutDisplay::TableCell,
5043 LayoutDisplay::TableCaption,
5044 LayoutDisplay::TableColumn,
5045 ] {
5046 assert!(
5047 !should_skip_for_table_structure(&sd, ws, parent),
5048 "whitespace under {parent:?} is NOT skipped by §17.2.1 stage 1"
5049 );
5050 }
5051 }
5052
5053 #[test]
5054 fn should_skip_for_table_structure_never_skips_real_content() {
5055 let sd = ws_dom("cell text", "");
5056 for parent in ALL_DISPLAYS {
5057 assert!(
5058 !should_skip_for_table_structure(&sd, NodeId::new(2), parent),
5059 "non-whitespace text must never be dropped (parent {parent:?})"
5060 );
5061 }
5062 }
5063
5064 #[test]
5065 fn is_proper_table_child_matches_exactly_the_seven_spec_values() {
5066 let proper = [
5067 LayoutDisplay::TableRowGroup,
5068 LayoutDisplay::TableHeaderGroup,
5069 LayoutDisplay::TableFooterGroup,
5070 LayoutDisplay::TableRow,
5071 LayoutDisplay::TableColumnGroup,
5072 LayoutDisplay::TableColumn,
5073 LayoutDisplay::TableCaption,
5074 ];
5075 for d in ALL_DISPLAYS {
5076 assert_eq!(
5077 is_proper_table_child(d),
5078 proper.contains(&d),
5079 "CSS 2.2 §17.2.1 proper-table-child set: {d:?}"
5080 );
5081 }
5082 assert!(
5083 !is_proper_table_child(LayoutDisplay::TableCell),
5084 "a cell is a proper child of a ROW, not of a table"
5085 );
5086 }
5087
5088 #[test]
5093 fn is_replaced_element_covers_the_css_display_3_appendix_b_set() {
5094 for nt in [
5095 NodeType::Br,
5096 NodeType::Wbr,
5097 NodeType::Meter,
5098 NodeType::Progress,
5099 NodeType::Canvas,
5100 NodeType::Embed,
5101 NodeType::Object,
5102 NodeType::Audio,
5103 NodeType::Video,
5104 NodeType::Input,
5105 NodeType::TextArea,
5106 NodeType::Select,
5107 NodeType::VirtualView,
5108 ] {
5109 let nd = NodeData::create_node(nt.clone());
5110 assert!(is_replaced_element(&nd), "{nt:?} is a replaced element");
5111 }
5112
5113 let img = NodeData::create_image(ImageRef::null_image(
5114 1,
5115 1,
5116 RawImageFormat::R8,
5117 Vec::new(),
5118 ));
5119 assert!(is_replaced_element(&img), "an <img> is the canonical replaced element");
5120 }
5121
5122 #[test]
5123 fn is_replaced_element_is_false_for_ordinary_containers_and_text() {
5124 for nt in [
5125 NodeType::Div,
5126 NodeType::Body,
5127 NodeType::Html,
5128 NodeType::P,
5129 NodeType::Span,
5130 NodeType::Table,
5131 NodeType::Button,
5132 NodeType::Label,
5133 NodeType::Hr,
5134 ] {
5135 let nd = NodeData::create_node(nt.clone());
5136 assert!(!is_replaced_element(&nd), "{nt:?} is not replaced");
5137 }
5138 assert!(!is_replaced_element(&NodeData::create_text("hello")));
5139 }
5140
5141 #[test]
5142 fn display_contents_on_a_replaced_element_degrades_to_display_none() {
5143 let sd = styled(
5145 Dom::create_body().with_child(
5146 Dom::create_from_data(NodeData::create_node(NodeType::Br))
5147 .with_ids_and_classes(vec![IdOrClass::Class("c".into())].into()),
5148 ),
5149 ".c { display: contents; }",
5150 );
5151 let tree = build_tree(&sd);
5152 assert!(
5153 tree.children(tree.root).is_empty(),
5154 "the <br> must be dropped from its parent's child list"
5155 );
5156 let br = (0..tree.nodes.len())
5157 .find(|&i| tree.get(i).unwrap().dom_node_id == Some(NodeId::new(1)))
5158 .expect("the node object still exists, just unparented");
5159 assert_eq!(
5160 tree.warm(br).unwrap().computed_style.display,
5161 LayoutDisplay::None
5162 );
5163 assert_eq!(tree.get(br).unwrap().formatting_context, FormattingContext::None);
5164 }
5165
5166 #[test]
5171 fn get_display_type_reads_the_computed_display() {
5172 for (css_display, want) in [
5173 ("none", LayoutDisplay::None),
5174 ("block", LayoutDisplay::Block),
5175 ("inline", LayoutDisplay::Inline),
5176 ("inline-block", LayoutDisplay::InlineBlock),
5177 ("flex", LayoutDisplay::Flex),
5178 ("grid", LayoutDisplay::Grid),
5179 ("table", LayoutDisplay::Table),
5180 ("table-row", LayoutDisplay::TableRow),
5181 ("table-cell", LayoutDisplay::TableCell),
5182 ("flow-root", LayoutDisplay::FlowRoot),
5183 ("list-item", LayoutDisplay::ListItem),
5184 ("contents", LayoutDisplay::Contents),
5185 ] {
5186 let sd = styled(
5187 Dom::create_body().with_child(div_class("x")),
5188 &format!(".x {{ display: {css_display}; }}"),
5189 );
5190 assert_eq!(
5191 get_display_type(&sd, NodeId::new(1)),
5192 want,
5193 "display:{css_display}"
5194 );
5195 }
5196 }
5197
5198 #[test]
5199 fn get_display_type_is_stable_across_repeated_calls() {
5200 let sd = mixed_dom();
5201 for i in 0..sd.node_data.len() {
5202 let id = NodeId::new(i);
5203 let a = get_display_type(&sd, id);
5204 let b = get_display_type(&sd, id);
5205 assert_eq!(a, b, "node {i} must be deterministic");
5206 }
5207 }
5208
5209 #[test]
5214 fn determine_formatting_context_is_inline_for_every_text_node() {
5215 let sd = mixed_dom();
5216 for needle in ["hello", "world", "tail", " \n\t"] {
5217 let id = text_node(&sd, needle);
5218 assert_eq!(
5219 determine_formatting_context(&sd, id),
5220 FormattingContext::Inline,
5221 "text node {needle:?}"
5222 );
5223 }
5224 }
5225
5226 #[test]
5227 fn determine_formatting_context_for_display_ignores_display_on_text_nodes() {
5228 let sd = mixed_dom();
5231 let t = text_node(&sd, "hello");
5232 for d in ALL_DISPLAYS {
5233 assert_eq!(
5234 determine_formatting_context_for_display(&sd, t, d),
5235 FormattingContext::Inline,
5236 "text + display:{d:?}"
5237 );
5238 }
5239 }
5240
5241 #[test]
5242 fn determine_formatting_context_for_display_maps_each_display_value() {
5243 let sd = styled(Dom::create_body().with_child(div_class("x")), ".x { display: block; }");
5244 let id = NodeId::new(1);
5245 for (d, want) in [
5246 (LayoutDisplay::Inline, FormattingContext::Inline),
5247 (
5248 LayoutDisplay::FlowRoot,
5249 FormattingContext::Block {
5250 establishes_new_context: true,
5251 },
5252 ),
5253 (LayoutDisplay::InlineBlock, FormattingContext::InlineBlock),
5254 (LayoutDisplay::Table, FormattingContext::Table),
5255 (LayoutDisplay::InlineTable, FormattingContext::Table),
5256 (LayoutDisplay::TableRowGroup, FormattingContext::TableRowGroup),
5257 (LayoutDisplay::TableHeaderGroup, FormattingContext::TableRowGroup),
5258 (LayoutDisplay::TableFooterGroup, FormattingContext::TableRowGroup),
5259 (LayoutDisplay::TableRow, FormattingContext::TableRow),
5260 (LayoutDisplay::TableCell, FormattingContext::TableCell),
5261 (LayoutDisplay::TableColumnGroup, FormattingContext::TableColumnGroup),
5262 (LayoutDisplay::TableCaption, FormattingContext::TableCaption),
5263 (LayoutDisplay::TableColumn, FormattingContext::None),
5264 (LayoutDisplay::None, FormattingContext::None),
5265 (LayoutDisplay::Flex, FormattingContext::Flex),
5266 (LayoutDisplay::InlineFlex, FormattingContext::Flex),
5267 (LayoutDisplay::Grid, FormattingContext::Grid),
5268 (LayoutDisplay::InlineGrid, FormattingContext::Grid),
5269 (LayoutDisplay::Contents, FormattingContext::Contents),
5270 (
5271 LayoutDisplay::RunIn,
5272 FormattingContext::Block {
5273 establishes_new_context: true,
5274 },
5275 ),
5276 (
5277 LayoutDisplay::Marker,
5278 FormattingContext::Block {
5279 establishes_new_context: true,
5280 },
5281 ),
5282 ] {
5283 assert_eq!(
5284 determine_formatting_context_for_display(&sd, id, d),
5285 want,
5286 "display:{d:?}"
5287 );
5288 }
5289 }
5290
5291 #[test]
5292 fn determine_formatting_context_for_display_never_panics_on_any_display_value() {
5293 let sd = styled(Dom::create_body().with_child(div_class("x")), ".x { display: block; }");
5294 for d in ALL_DISPLAYS {
5295 let _ = determine_formatting_context_for_display(&sd, NodeId::new(1), d);
5296 let _ = determine_formatting_context_for_display(&sd, NodeId::ZERO, d);
5297 }
5298 }
5299
5300 #[test]
5301 fn a_block_with_only_inline_children_establishes_an_ifc() {
5302 let sd = mixed_dom();
5303 assert_eq!(
5304 determine_formatting_context(&sd, NodeId::new(1)),
5305 FormattingContext::Inline,
5306 "CSS 2.2 §9.4.2: a block container with no block-level boxes establishes an IFC"
5307 );
5308 }
5309
5310 #[test]
5311 fn a_block_with_a_block_child_stays_a_bfc() {
5312 let sd = mixed_dom();
5313 assert!(matches!(
5314 determine_formatting_context(&sd, NodeId::new(5)),
5315 FormattingContext::Block { .. }
5316 ));
5317 }
5318
5319 #[test]
5320 fn establishes_new_bfc_for_the_unconditional_display_values() {
5321 for css_display in ["inline-block", "table-cell", "table-caption", "flow-root"] {
5322 let sd = styled(
5323 Dom::create_body().with_child(div_class("x")),
5324 &format!(".x {{ display: {css_display}; }}"),
5325 );
5326 assert!(
5327 establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5328 "display:{css_display} always establishes a BFC"
5329 );
5330 }
5331 }
5332
5333 #[test]
5334 fn establishes_new_bfc_for_non_visible_overflow_floats_and_abspos() {
5335 for css in [
5336 ".x { display: block; overflow-x: hidden; }",
5337 ".x { display: block; overflow-y: scroll; }",
5338 ".x { display: block; overflow: auto; }",
5339 ".x { display: block; float: left; }",
5340 ".x { display: block; float: right; }",
5341 ".x { display: block; position: absolute; }",
5342 ".x { display: block; position: fixed; }",
5343 ] {
5344 let sd = styled(Dom::create_body().with_child(div_class("x")), css);
5345 assert!(
5346 establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5347 "{css} must establish a BFC"
5348 );
5349 }
5350 }
5351
5352 #[test]
5353 fn establishes_new_bfc_is_false_for_a_plain_in_flow_block() {
5354 let sd = styled(
5355 Dom::create_body().with_child(div_class("x")),
5356 ".x { display: block; }",
5357 );
5358 assert!(
5359 !establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5360 "a static, visible-overflow, unfloated block does not open a BFC"
5361 );
5362 }
5363
5364 #[test]
5365 fn establishes_new_bfc_for_the_root_and_for_replaced_elements() {
5366 let sd = styled(
5367 Dom::create_body().with_child(Dom::create_from_data(NodeData::create_node(NodeType::Br))),
5368 "",
5369 );
5370 assert!(
5371 establishes_new_block_formatting_context(&sd, NodeId::ZERO),
5372 "the root element always establishes a BFC"
5373 );
5374 assert!(
5375 establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5376 "replaced elements always establish an independent formatting context"
5377 );
5378 }
5379
5380 #[test]
5385 fn compute_layout_style_captures_every_property_it_advertises() {
5386 let sd = styled(
5387 Dom::create_body().with_child(div_class("x")),
5388 ".x { display: flex; position: absolute; overflow-x: hidden; overflow-y: scroll; \
5389 width: 50px; height: 60px; min-width: 10px; min-height: 11px; max-width: 99px; \
5390 max-height: 98px; text-align: center; }",
5391 );
5392 let s = compute_layout_style(&sd, NodeId::new(1));
5393 assert_eq!(s.display, LayoutDisplay::Flex);
5394 assert_eq!(s.position, LayoutPosition::Absolute);
5395 assert_eq!(s.overflow_x, LayoutOverflow::Hidden);
5396 assert_eq!(s.overflow_y, LayoutOverflow::Scroll);
5397 assert_eq!(s.text_align, StyleTextAlign::Center);
5398 assert!(s.width.is_some());
5399 assert!(s.height.is_some());
5400 assert!(s.min_width.is_some());
5401 assert!(s.min_height.is_some());
5402 assert!(s.max_width.is_some());
5403 assert!(s.max_height.is_some());
5404 }
5405
5406 #[test]
5407 fn compute_layout_style_leaves_auto_sizes_as_none() {
5408 let sd = styled(
5409 Dom::create_body().with_child(div_class("x")),
5410 ".x { display: block; }",
5411 );
5412 let s = compute_layout_style(&sd, NodeId::new(1));
5413 assert!(s.width.is_none(), "auto width must be None, not 0px");
5414 assert!(s.height.is_none());
5415 assert!(s.max_width.is_none());
5416 assert!(s.max_height.is_none());
5417 assert_eq!(s.float, LayoutFloat::None);
5418 assert_eq!(s.position, LayoutPosition::Static);
5419 }
5420
5421 #[test]
5422 fn compute_layout_style_reads_float_left_and_right() {
5423 for (css, want) in [("left", LayoutFloat::Left), ("right", LayoutFloat::Right)] {
5424 let sd = styled(
5425 Dom::create_body().with_child(div_class("x")),
5426 &format!(".x {{ float: {css}; }}"),
5427 );
5428 assert_eq!(compute_layout_style(&sd, NodeId::new(1)).float, want);
5429 }
5430 }
5431
5432 #[test]
5433 fn compute_layout_style_never_panics_on_any_node_of_a_real_dom() {
5434 let sd = mixed_dom();
5435 for i in 0..sd.node_data.len() {
5436 let _ = compute_layout_style(&sd, NodeId::new(i));
5437 }
5438 }
5439
5440 #[test]
5445 fn font_size_helpers_fall_back_to_the_default_when_nothing_is_specified() {
5446 let sd = styled(Dom::create_body().with_child(div_class("x")), "");
5447 assert_eq!(get_root_font_size(&sd), DEFAULT_FONT_SIZE);
5448 assert_eq!(
5449 get_parent_font_size(&sd, NodeId::ZERO),
5450 DEFAULT_FONT_SIZE,
5451 "the root has no parent ⇒ documented DEFAULT_FONT_SIZE fallback"
5452 );
5453 assert_eq!(get_element_font_size(&sd, NodeId::new(1)), DEFAULT_FONT_SIZE);
5454 }
5455
5456 #[test]
5457 fn get_element_and_parent_font_size_track_the_cascade() {
5458 let sd = styled(
5459 Dom::create_body().with_child(div_class("big").with_child(div_class("small"))),
5460 ".big { font-size: 32px; } .small { font-size: 8px; }",
5461 );
5462 assert_eq!(get_element_font_size(&sd, NodeId::new(1)), 32.0);
5463 assert_eq!(get_element_font_size(&sd, NodeId::new(2)), 8.0);
5464 assert_eq!(
5465 get_parent_font_size(&sd, NodeId::new(2)),
5466 32.0,
5467 "the parent's size, not the element's own"
5468 );
5469 }
5470
5471 #[test]
5472 fn get_root_font_size_reads_node_zero() {
5473 let root = Dom::create_body()
5474 .with_ids_and_classes(vec![IdOrClass::Class("root".into())].into())
5475 .with_child(div_class("x"));
5476 let sd = styled(root, ".root { font-size: 20px; }");
5477 assert_eq!(get_root_font_size(&sd), 20.0, "get_root_font_size hard-codes NodeId(0)");
5478 assert_eq!(get_root_font_size(&sd), get_element_font_size(&sd, NodeId::ZERO));
5479 }
5480
5481 #[test]
5482 fn font_size_helpers_return_a_finite_positive_size_for_every_node() {
5483 let sd = mixed_dom();
5484 for i in 0..sd.node_data.len() {
5485 let id = NodeId::new(i);
5486 for size in [get_element_font_size(&sd, id), get_parent_font_size(&sd, id)] {
5487 assert!(size.is_finite(), "node {i}: {size}");
5488 assert!(size > 0.0, "node {i}: a zero/negative font-size breaks em math");
5489 }
5490 }
5491 }
5492
5493 #[test]
5498 fn create_resolution_context_zeroes_an_unknown_containing_block() {
5499 let sd = mixed_dom();
5502 let ctx = create_resolution_context(&sd, NodeId::new(1), None, VIEWPORT);
5503 assert_eq!(ctx.containing_block_size.width, 0.0);
5504 assert_eq!(ctx.containing_block_size.height, 0.0);
5505 assert!(ctx.element_size.is_none(), "not laid out yet");
5506 assert_eq!(ctx.viewport_size.width, VIEWPORT.width);
5507 assert_eq!(ctx.viewport_size.height, VIEWPORT.height);
5508 }
5509
5510 #[test]
5511 fn create_resolution_context_passes_a_known_containing_block_through() {
5512 let sd = mixed_dom();
5513 let cb = PhysicalSize::new(321.0, 123.0);
5514 let ctx = create_resolution_context(&sd, NodeId::new(1), Some(cb), VIEWPORT);
5515 assert_eq!(ctx.containing_block_size.width, 321.0);
5516 assert_eq!(ctx.containing_block_size.height, 123.0);
5517 }
5518
5519 #[test]
5520 fn create_resolution_context_survives_a_degenerate_viewport() {
5521 let sd = mixed_dom();
5522 for vp in [
5523 LogicalSize::new(0.0, 0.0),
5524 LogicalSize::new(-100.0, -100.0),
5525 LogicalSize::new(f32::MAX, f32::MAX),
5526 LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
5527 LogicalSize::new(f32::NAN, f32::NAN),
5528 ] {
5529 let ctx = create_resolution_context(&sd, NodeId::new(1), None, vp);
5530 assert!(ctx.element_font_size.is_finite());
5532 assert!(ctx.parent_font_size.is_finite());
5533 assert!(ctx.root_font_size.is_finite());
5534 }
5535 }
5536
5537 #[test]
5538 fn create_resolution_context_survives_a_degenerate_containing_block() {
5539 let sd = mixed_dom();
5540 for cb in [
5541 PhysicalSize::new(0.0, 0.0),
5542 PhysicalSize::new(-1.0, -1.0),
5543 PhysicalSize::new(f32::NAN, f32::INFINITY),
5544 PhysicalSize::new(f32::MAX, f32::MIN),
5545 ] {
5546 let ctx = create_resolution_context(&sd, NodeId::new(1), Some(cb), VIEWPORT);
5547 assert!(ctx.root_font_size.is_finite());
5548 }
5549 }
5550
5551 fn collect_for(css: &str, node: usize, viewport: LogicalSize) -> CollectedBoxProps {
5556 let sd = styled(Dom::create_body().with_child(div_class("x")), css);
5557 let mut msgs = None;
5558 collect_box_props(&sd, NodeId::new(node), &mut msgs, viewport)
5559 }
5560
5561 #[test]
5562 fn collect_box_props_resolves_plain_pixel_edges() {
5563 let c = collect_for(
5564 ".x { margin: 10px; padding: 5px; border: 2px solid black; }",
5565 1,
5566 VIEWPORT,
5567 );
5568 assert_eq!(c.resolved.margin.top, 10.0);
5569 assert_eq!(c.resolved.margin.left, 10.0);
5570 assert_eq!(c.resolved.padding.right, 5.0);
5571 assert_eq!(c.resolved.border.bottom, 2.0);
5572 }
5573
5574 #[test]
5575 fn collect_box_props_zeroes_a_border_whose_style_is_none() {
5576 let c = collect_for(".x { border-width: 9px; border-style: none; }", 1, VIEWPORT);
5578 assert_eq!(c.resolved.border.top, 0.0);
5579 assert_eq!(c.resolved.border.left, 0.0);
5580
5581 let c = collect_for(".x { border-width: 9px; border-style: hidden; }", 1, VIEWPORT);
5582 assert_eq!(c.resolved.border.right, 0.0);
5583 }
5584
5585 #[test]
5586 fn collect_box_props_strips_margins_and_padding_from_internal_table_boxes() {
5587 for display in [
5590 "table-row",
5591 "table-row-group",
5592 "table-header-group",
5593 "table-footer-group",
5594 "table-column",
5595 "table-column-group",
5596 ] {
5597 let c = collect_for(
5598 &format!(".x {{ display: {display}; margin: 10px; padding: 7px; }}"),
5599 1,
5600 VIEWPORT,
5601 );
5602 assert_eq!(c.resolved.margin.top, 0.0, "display:{display} margin");
5603 assert_eq!(c.resolved.padding.top, 0.0, "display:{display} padding");
5604 }
5605
5606 let c = collect_for(".x { display: table-cell; margin: 10px; padding: 7px; }", 1, VIEWPORT);
5608 assert_eq!(c.resolved.margin.left, 0.0, "cells have no margins");
5609 assert_eq!(c.resolved.padding.left, 7.0, "…but they do have padding");
5610 }
5611
5612 #[test]
5613 fn collect_box_props_zeroes_vertical_margins_on_a_non_replaced_inline() {
5614 let c = collect_for(".x { display: inline; margin: 10px; }", 1, VIEWPORT);
5615 assert_eq!(c.resolved.margin.top, 0.0);
5616 assert_eq!(c.resolved.margin.bottom, 0.0);
5617 assert_eq!(
5618 c.resolved.margin.left, 10.0,
5619 "horizontal margins still apply to inline boxes"
5620 );
5621 assert_eq!(c.resolved.margin.right, 10.0);
5622 }
5623
5624 #[test]
5625 fn collect_box_props_does_not_clamp_huge_lengths_before_packing() {
5626 let c = collect_for(".x { margin: 99999px; }", 1, VIEWPORT);
5629 assert_eq!(c.resolved.margin.top, 99_999.0);
5630
5631 let packed = PackedBoxProps::pack(&c.resolved);
5632 assert_eq!(packed.margin[0], i16::MAX, "the packing saturates, it does not wrap");
5633 }
5634
5635 #[test]
5636 fn collect_box_props_survives_a_degenerate_viewport() {
5637 for vp in [
5638 LogicalSize::new(0.0, 0.0),
5639 LogicalSize::new(-800.0, -600.0),
5640 LogicalSize::new(f32::MAX, f32::MAX),
5641 LogicalSize::new(f32::INFINITY, f32::INFINITY),
5642 LogicalSize::new(f32::NAN, f32::NAN),
5643 ] {
5644 let c = collect_for(".x { margin: 10vh; padding: 5vw; }", 1, vp);
5646 let packed = PackedBoxProps::pack(&c.resolved);
5647 for v in packed.margin.iter().chain(packed.padding.iter()) {
5648 assert!(
5649 (i16::MIN..=i16::MAX).contains(v),
5650 "packing must stay in range for viewport {vp:?}"
5651 );
5652 }
5653 }
5654 }
5655
5656 #[test]
5657 fn collect_box_props_fills_debug_messages_when_asked() {
5658 let sd = styled(
5659 Dom::create_body().with_child(div_class("x")),
5660 ".x { margin: 3px; }",
5661 );
5662 let mut msgs: Option<Vec<LayoutDebugMessage>> = Some(Vec::new());
5663 let _ = collect_box_props(&sd, NodeId::new(1), &mut msgs, VIEWPORT);
5664 assert!(
5665 !msgs.expect("still Some").is_empty(),
5666 "a Some(vec) sink must actually receive the [BOX] trace"
5667 );
5668
5669 let mut none_sink: Option<Vec<LayoutDebugMessage>> = None;
5671 let _ = collect_box_props(&sd, NodeId::new(1), &mut none_sink, VIEWPORT);
5672 assert!(none_sink.is_none());
5673 }
5674
5675 #[test]
5676 fn collect_box_props_unresolved_and_resolved_agree_after_a_re_resolve() {
5677 let c = collect_for(".x { margin: 4px; padding: 6px; }", 1, VIEWPORT);
5678 let params = crate::solver3::geometry::ResolutionParams {
5679 containing_block: VIEWPORT,
5680 viewport_size: VIEWPORT,
5681 element_font_size: DEFAULT_FONT_SIZE,
5682 root_font_size: DEFAULT_FONT_SIZE,
5683 };
5684 let again = c.unresolved.resolve(¶ms);
5685 assert_eq!(again.margin.top, c.resolved.margin.top);
5686 assert_eq!(again.padding.left, c.resolved.padding.left);
5687 assert_eq!(again.border.top, c.resolved.border.top);
5688 }
5689
5690 #[test]
5691 fn edge_sizes_default_is_all_zero() {
5692 let e = EdgeSizes::default();
5693 assert_eq!((e.top, e.right, e.bottom, e.left), (0.0, 0.0, 0.0, 0.0));
5694 }
5695
5696 #[test]
5701 fn a_freshly_built_tree_satisfies_every_structural_invariant() {
5702 for sd in [
5703 mixed_dom(),
5704 styled(Dom::create_body(), ""),
5705 styled(
5706 Dom::create_body().with_child(div_class("f").with_child(div_class("c"))),
5707 ".f { display: flex; } .c { display: table-cell; }",
5708 ),
5709 styled(
5710 Dom::create_body().with_child(div_class("t").with_child(div_class("c"))),
5711 ".t { display: table; } .c { display: table-cell; }",
5712 ),
5713 styled(
5714 Dom::create_body().with_child(div_class("li").with_child(Dom::create_text("x"))),
5715 ".li { display: list-item; }",
5716 ),
5717 ] {
5718 let tree = build_tree(&sd);
5719 let n = tree.nodes.len();
5720 assert!(n >= 1);
5721 assert_eq!(tree.warm.len(), n);
5722 assert_eq!(tree.cold.len(), n);
5723 assert_eq!(tree.children_offsets.len(), n);
5724 assert_eq!(tree.subtree_needs_intrinsic.len(), n);
5725 assert!(tree.root < n);
5726 assert_eq!(tree.get(tree.root).unwrap().parent, None);
5727
5728 for i in 0..n {
5729 if let Some(p) = tree.get(i).unwrap().parent {
5730 assert!(p < n, "node {i}'s parent {p} is out of range");
5731 }
5732 for &c in tree.children(i) {
5733 assert!(c < n, "node {i}'s child {c} is out of range");
5734 assert_ne!(c, i, "no node may be its own child");
5735 }
5736 }
5737 for (dom_id, indices) in &tree.dom_to_layout {
5738 for &i in indices {
5739 assert!(i < n, "dom_to_layout[{dom_id:?}] points at {i}, out of range");
5740 assert_eq!(tree.get(i).unwrap().dom_node_id, Some(*dom_id));
5741 }
5742 }
5743 }
5744 }
5745
5746 #[test]
5747 fn building_a_body_only_dom_yields_exactly_one_node() {
5748 let sd = styled(Dom::create_body(), "");
5749 let tree = build_tree(&sd);
5750 assert_eq!(tree.nodes.len(), 1);
5751 assert_eq!(tree.root, 0);
5752 assert!(tree.children(0).is_empty());
5753 assert!(tree.children_arena.is_empty());
5754 assert_eq!(tree.children_offsets, vec![(0, 0)]);
5755 assert_eq!(tree.memory_report().node_count, 1);
5756 }
5757
5758 #[test]
5759 fn the_root_box_always_establishes_a_new_block_formatting_context() {
5760 let tree = build_tree(&mixed_dom());
5761 match tree.get(tree.root).unwrap().formatting_context {
5762 FormattingContext::Block {
5763 establishes_new_context,
5764 } => assert!(establishes_new_context, "process_node forces this for the root"),
5765 other => panic!("the root should be a Block FC, got {other:?}"),
5766 }
5767 }
5768
5769 #[test]
5770 fn a_deeply_nested_dom_builds_without_blowing_the_stack() {
5771 let mut dom = div_class("d");
5774 for _ in 0..200 {
5775 dom = div_class("d").with_child(dom);
5776 }
5777 let sd = styled(Dom::create_body().with_child(dom), ".d { display: block; }");
5778 let tree = build_tree(&sd);
5779 assert_eq!(tree.nodes.len(), 202, "body + 201 divs");
5780 let mut i = tree.root;
5782 let mut depth = 0;
5783 while let Some(&next) = tree.children(i).first() {
5784 i = next;
5785 depth += 1;
5786 assert!(depth <= 202, "the parent/child links formed a cycle");
5787 }
5788 assert_eq!(depth, 201);
5789 }
5790}