1use std::{
16 collections::{BTreeMap, BTreeSet, HashMap},
17 hash::{DefaultHasher, Hash, Hasher},
18};
19
20const CACHE_SIZE_EPSILON: f32 = 0.1;
23
24use azul_core::{
25 diff::NodeDataFingerprint,
26 dom::{FormattingContext, NodeId, NodeType},
27 geom::{LogicalPosition, LogicalRect, LogicalSize},
28 styled_dom::{StyledDom, StyledNode},
29};
30use azul_css::{
31 css::CssPropertyValue,
32 props::{
33 layout::{
34 LayoutDisplay, LayoutHeight, LayoutOverflow,
35 LayoutPosition, LayoutWritingMode,
36 },
37 property::{CssProperty, CssPropertyType},
38 style::StyleTextAlign,
39 },
40 LayoutDebugMessage, LayoutDebugMessageType,
41};
42
43use crate::{
44 font_traits::{FontLoaderTrait, ParsedFontTrait, TextLayoutCache},
45 solver3::{
46 fc::{self, layout_formatting_context, LayoutConstraints, OverflowBehavior},
47 geometry::PositionedRectangle,
48 getters::{
49 get_css_height, get_display_property, get_overflow_x,
50 get_overflow_y, get_scrollbar_gutter_property, get_text_align, get_white_space_property, get_writing_mode,
51 MultiValue,
52 },
53 layout_tree::{
54 get_display_type, is_block_level, AnonymousBoxType, DirtyFlag, LayoutNode, LayoutNodeHot, LayoutTreeBuilder, SubtreeHash,
55 },
56 positioning::get_position_type,
57 scrollbar::ScrollbarRequirements,
58 sizing::calculate_used_size_for_node,
59 LayoutContext, LayoutError, LayoutTree, Result,
60 },
61 text3::cache::AvailableSpace as Text3AvailableSpace,
62};
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ComputeMode {
87 ComputeSize,
90 PerformLayout,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum AvailableWidthType {
103 Definite,
105 MinContent,
107 MaxContent,
109}
110
111#[derive(Copy, Debug, Clone)]
117pub struct SizingCacheEntry {
118 pub available_size: LogicalSize,
120 pub result_size: LogicalSize,
122 pub baseline: Option<f32>,
124 pub escaped_top_margin: Option<f32>,
126 pub escaped_bottom_margin: Option<f32>,
128}
129
130#[derive(Debug, Clone)]
135pub struct LayoutCacheEntry {
136 pub available_size: LogicalSize,
138 pub result_size: LogicalSize,
140 pub content_size: LogicalSize,
142 pub child_positions: Vec<(usize, LogicalPosition)>,
144 pub escaped_top_margin: Option<f32>,
146 pub escaped_bottom_margin: Option<f32>,
148 pub scrollbar_info: ScrollbarRequirements,
150}
151
152#[derive(Debug, Clone)]
161pub struct NodeCache {
162 pub measure_entries: [Option<SizingCacheEntry>; 9],
168
169 pub layout_entry: Option<LayoutCacheEntry>,
172
173 pub is_empty: bool,
176}
177
178impl Default for NodeCache {
179 fn default() -> Self {
180 Self {
181 measure_entries: [None, None, None, None, None, None, None, None, None],
182 layout_entry: None,
183 is_empty: true, }
185 }
186}
187
188impl NodeCache {
189 pub fn clear(&mut self) {
191 self.measure_entries = [None, None, None, None, None, None, None, None, None];
192 self.layout_entry = None;
193 self.is_empty = true;
194 }
195
196 #[must_use] pub fn slot_index(
206 width_known: bool,
207 height_known: bool,
208 width_type: AvailableWidthType,
209 height_type: AvailableWidthType,
210 ) -> usize {
211 match (width_known, height_known) {
212 (true, true) => 0,
213 (true, false) => {
214 if width_type == AvailableWidthType::MinContent { 2 } else { 1 }
215 }
216 (false, true) => {
217 if height_type == AvailableWidthType::MinContent { 4 } else { 3 }
218 }
219 (false, false) => {
220 let w = usize::from(width_type == AvailableWidthType::MinContent);
221 let h = usize::from(height_type == AvailableWidthType::MinContent);
222 5 + w * 2 + h
223 }
224 }
225 }
226
227 #[must_use] pub fn get_size(&self, slot: usize, known_dims: LogicalSize) -> Option<&SizingCacheEntry> {
231 let entry = self.measure_entries[slot].as_ref()?;
232 if (known_dims.width - entry.available_size.width).abs() < CACHE_SIZE_EPSILON
234 && (known_dims.height - entry.available_size.height).abs() < CACHE_SIZE_EPSILON
235 {
236 return Some(entry);
237 }
238 if (known_dims.width - entry.result_size.width).abs() < CACHE_SIZE_EPSILON
243 && (known_dims.height - entry.result_size.height).abs() < CACHE_SIZE_EPSILON
244 {
245 return Some(entry);
246 }
247 None
248 }
249
250 pub const fn store_size(&mut self, slot: usize, entry: SizingCacheEntry) {
252 self.measure_entries[slot] = Some(entry);
253 self.is_empty = false;
254 }
255
256 #[must_use] pub fn get_layout(&self, known_dims: LogicalSize) -> Option<&LayoutCacheEntry> {
258 let entry = self.layout_entry.as_ref()?;
259 if (known_dims.width - entry.available_size.width).abs() < CACHE_SIZE_EPSILON
260 && (known_dims.height - entry.available_size.height).abs() < CACHE_SIZE_EPSILON
261 {
262 return Some(entry);
263 }
264 if (known_dims.width - entry.result_size.width).abs() < CACHE_SIZE_EPSILON
266 && (known_dims.height - entry.result_size.height).abs() < CACHE_SIZE_EPSILON
267 {
268 return Some(entry);
269 }
270 None
271 }
272
273 pub fn store_layout(&mut self, entry: LayoutCacheEntry) {
275 self.layout_entry = Some(entry);
276 self.is_empty = false;
277 }
278}
279
280#[derive(Debug, Clone, Default)]
291pub struct LayoutCacheMap {
292 pub entries: Vec<NodeCache>,
293}
294
295impl LayoutCacheMap {
296 pub fn resize_to_tree(&mut self, tree_len: usize) {
299 self.entries.resize_with(tree_len, NodeCache::default);
300 }
301
302 #[inline]
304 #[must_use] pub fn get(&self, node_index: usize) -> &NodeCache {
305 &self.entries[node_index]
306 }
307
308 #[inline]
310 pub fn get_mut(&mut self, node_index: usize) -> &mut NodeCache {
311 &mut self.entries[node_index]
312 }
313
314 pub fn mark_dirty(&mut self, node_index: usize, tree: &[LayoutNodeHot]) {
321 if node_index >= self.entries.len() {
322 return;
323 }
324 let cache = &mut self.entries[node_index];
325 if cache.is_empty {
326 return; }
328 cache.clear();
329
330 let mut current = tree.get(node_index).and_then(|n| n.parent);
332 while let Some(parent_idx) = current {
333 if parent_idx >= self.entries.len() {
334 break;
335 }
336 let parent_cache = &mut self.entries[parent_idx];
337 if parent_cache.is_empty {
338 break; }
340 parent_cache.clear();
341 current = tree.get(parent_idx).and_then(|n| n.parent);
342 }
343 }
344}
345
346#[derive(Debug, Clone, Default)]
348pub struct LayoutCache {
349 pub tree: Option<LayoutTree>,
351 pub calculated_positions: super::PositionVec,
353 pub viewport: Option<LogicalRect>,
355 pub scroll_ids: HashMap<usize, u64>,
357 pub scroll_id_to_node_id: HashMap<u64, NodeId>,
359 pub counters: HashMap<(usize, String), i32>,
364 pub float_cache: HashMap<usize, fc::FloatingContext>,
369 pub cache_map: LayoutCacheMap,
373 pub previous_positions: super::PositionVec,
376 pub cached_display_list: Option<(SubtreeHash, LogicalRect, super::display_list::DisplayList)>,
384 pub prev_dom_ptr: usize,
388 pub prev_viewport: LogicalRect,
389}
390
391#[derive(Copy, Debug, Clone, Default)]
393pub struct Solver3CacheMemoryReport {
394 pub tree_bytes: usize,
395 pub tree_report: Option<super::layout_tree::LayoutTreeMemoryReport>,
396 pub calculated_positions_bytes: usize,
397 pub previous_positions_bytes: usize,
398 pub scroll_ids_bytes: usize,
399 pub scroll_id_to_node_id_bytes: usize,
400 pub counters_bytes: usize,
401 pub float_cache_bytes: usize,
402 pub cache_map_bytes: usize,
403 pub cached_display_list_bytes: usize,
404}
405
406impl Solver3CacheMemoryReport {
407 #[must_use] pub const fn total_bytes(&self) -> usize {
408 self.tree_bytes
409 + self.calculated_positions_bytes
410 + self.previous_positions_bytes
411 + self.scroll_ids_bytes
412 + self.scroll_id_to_node_id_bytes
413 + self.counters_bytes
414 + self.float_cache_bytes
415 + self.cache_map_bytes
416 + self.cached_display_list_bytes
417 }
418}
419
420impl LayoutCache {
421 pub fn reset_incremental(&mut self) {
433 self.tree = None;
434 self.cache_map = LayoutCacheMap::default();
435 self.cached_display_list = None;
436 self.prev_dom_ptr = 0;
437 self.counters.clear();
438 self.float_cache.clear();
439 }
440
441 #[must_use] pub fn memory_report(&self) -> Solver3CacheMemoryReport {
443 let tree_report = self.tree.as_ref().map(LayoutTree::memory_report);
444 let tree_bytes = tree_report.as_ref().map_or(0, super::layout_tree::LayoutTreeMemoryReport::total_bytes);
445 let mut cache_map_bytes = self.cache_map.entries.capacity()
448 * size_of::<NodeCache>();
449 for e in &self.cache_map.entries {
450 if let Some(le) = &e.layout_entry {
451 cache_map_bytes += le.child_positions.capacity()
452 * size_of::<(usize, LogicalPosition)>();
453 }
454 }
455 Solver3CacheMemoryReport {
456 tree_bytes,
457 tree_report,
458 calculated_positions_bytes: self.calculated_positions.len()
459 * size_of::<LogicalPosition>(),
460 previous_positions_bytes: self.previous_positions.len()
461 * size_of::<LogicalPosition>(),
462 scroll_ids_bytes: self.scroll_ids.len()
463 * (size_of::<usize>() + size_of::<u64>()),
464 scroll_id_to_node_id_bytes: self.scroll_id_to_node_id.len()
465 * (size_of::<u64>() + size_of::<NodeId>()),
466 counters_bytes: self.counters.iter().map(|((_, name), _)| {
467 size_of::<(usize, String)>()
468 + size_of::<i32>()
469 + name.capacity()
470 }).sum(),
471 float_cache_bytes: self.float_cache.len() * 256, cache_map_bytes,
473 cached_display_list_bytes: if self.cached_display_list.is_some() { 2048 } else { 0 },
474 }
475 }
476}
477
478#[derive(Debug, Default)]
480pub struct ReconciliationResult {
481 pub intrinsic_dirty: BTreeSet<usize>,
483 pub layout_roots: BTreeSet<usize>,
485 pub paint_dirty: BTreeSet<usize>,
487}
488
489impl ReconciliationResult {
490 #[must_use] pub fn is_clean(&self) -> bool {
492 self.intrinsic_dirty.is_empty()
493 && self.layout_roots.is_empty()
494 && self.paint_dirty.is_empty()
495 }
496
497 #[must_use] pub fn needs_layout(&self) -> bool {
499 !self.intrinsic_dirty.is_empty() || !self.layout_roots.is_empty()
500 }
501
502 #[must_use] pub fn needs_paint_only(&self) -> bool {
504 !self.needs_layout() && !self.paint_dirty.is_empty()
505 }
506}
507
508#[allow(clippy::match_same_arms)] pub fn reposition_clean_subtrees(
517 styled_dom: &StyledDom,
518 tree: &LayoutTree,
519 layout_roots: &BTreeSet<usize>,
520 calculated_positions: &mut super::PositionVec,
521) {
522 let mut parents_to_reposition = BTreeSet::new();
525 for &root_idx in layout_roots {
526 if let Some(parent_idx) = tree.get(root_idx).and_then(|n| n.parent) {
527 parents_to_reposition.insert(parent_idx);
528 }
529 }
530
531 for parent_idx in parents_to_reposition {
532 let Some(parent_node) = tree.get(parent_idx) else {
533 continue;
534 };
535
536 match parent_node.formatting_context {
538 FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
540 reposition_block_flow_siblings(
541 styled_dom,
542 parent_idx,
543 tree,
544 layout_roots,
545 calculated_positions,
546 );
547 }
548
549 FormattingContext::Flex | FormattingContext::Grid => {
550 }
554
555 FormattingContext::Table | FormattingContext::TableRow => {
556 }
560
561 _ => { }
565 }
566 }
567}
568
569fn to_overflow_behavior(overflow: MultiValue<LayoutOverflow>) -> fc::OverflowBehavior {
573 match overflow.unwrap_or(LayoutOverflow::Visible) {
574 LayoutOverflow::Visible => fc::OverflowBehavior::Visible,
575 LayoutOverflow::Hidden | LayoutOverflow::Clip => fc::OverflowBehavior::Hidden,
576 LayoutOverflow::Scroll => fc::OverflowBehavior::Scroll,
577 LayoutOverflow::Auto => fc::OverflowBehavior::Auto,
578 }
579}
580
581const fn style_text_align_to_fc(text_align: StyleTextAlign) -> fc::TextAlign {
584 match text_align {
585 StyleTextAlign::Start | StyleTextAlign::Left => fc::TextAlign::Start,
586 StyleTextAlign::End | StyleTextAlign::Right => fc::TextAlign::End,
587 StyleTextAlign::Center => fc::TextAlign::Center,
588 StyleTextAlign::Justify => fc::TextAlign::Justify,
589 }
590}
591
592#[allow(clippy::cast_possible_truncation)] #[must_use] pub fn collect_children_dom_ids(styled_dom: &StyledDom, parent_dom_id: NodeId) -> Vec<NodeId> {
598 let hierarchy_container = styled_dom.node_hierarchy.as_container();
599 let mut children = Vec::new();
600
601 let Some(hierarchy_item) = hierarchy_container.get(parent_dom_id) else {
602 return children;
603 };
604
605 let Some(mut child_id) = hierarchy_item.first_child_id(parent_dom_id) else {
606 unsafe {
609 let pi = parent_dom_id.index();
610 if pi < 8 { crate::az_mark((0x40540 + pi * 4) as u32, (0xC000_0000u32)); }
611 }
612 return children;
613 };
614
615 if get_display_type(styled_dom, child_id) != LayoutDisplay::None {
618 children.push(child_id);
619 }
620 while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
621 let Some(next) = hierarchy_item.next_sibling_id() else {
622 break;
623 };
624 if get_display_type(styled_dom, next) != LayoutDisplay::None {
625 children.push(next);
626 }
627 child_id = next;
628 }
629
630 unsafe {
634 let pi = parent_dom_id.index();
635 if pi < 8 {
636 crate::az_mark((0x40540 + pi * 4) as u32, (0xCC00_0000u32 | (children.len() as u32 & 0xffff)));
637 }
638 }
639 children
640}
641
642pub fn reposition_block_flow_siblings(
646 styled_dom: &StyledDom,
647 parent_idx: usize,
648 tree: &LayoutTree,
649 layout_roots: &BTreeSet<usize>,
650 calculated_positions: &mut super::PositionVec,
651) {
652 let Some(parent_node) = tree.get(parent_idx) else {
653 return;
654 };
655 let dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
656 let styled_node_state = styled_dom
657 .styled_nodes
658 .as_container()
659 .get(dom_id)
660 .map(|n| n.styled_node_state)
661 .unwrap_or_default();
662
663 let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
664
665 let parent_pos = calculated_positions
666 .get(parent_idx)
667 .copied()
668 .unwrap_or_default();
669
670 let parent_bp = parent_node.box_props.unpack();
671 let content_box_origin = LogicalPosition::new(
672 parent_pos.x + parent_bp.padding.left,
673 parent_pos.y + parent_bp.padding.top,
674 );
675
676 let mut main_pen = 0.0;
677
678 for &child_idx in tree.children(parent_idx) {
679 let Some(child_node) = tree.get(child_idx) else {
680 continue;
681 };
682
683 let child_size = child_node.used_size.unwrap_or_default();
684 let child_bp = child_node.box_props.unpack();
685 let child_main_sum = child_bp.margin.main_sum(writing_mode);
686 let margin_box_main_size = child_size.main(writing_mode) + child_main_sum;
687
688 if layout_roots.contains(&child_idx) {
689 let new_pos = match calculated_positions.get(child_idx) {
692 Some(p) => *p,
693 None => continue,
694 };
695
696 let main_axis_offset = if writing_mode.is_vertical() {
697 new_pos.x - content_box_origin.x
698 } else {
699 new_pos.y - content_box_origin.y
700 };
701
702 main_pen = main_axis_offset
703 + child_size.main(writing_mode)
704 + child_bp.margin.main_end(writing_mode);
705 } else {
706 let old_pos = match calculated_positions.get(child_idx) {
709 Some(p) => *p,
710 None => continue,
711 };
712
713 let child_main_start = child_bp.margin.main_start(writing_mode);
714 let new_main_pos = main_pen + child_main_start;
715 let old_relative_pos = tree.warm(child_idx)
716 .and_then(|w| w.relative_position)
717 .unwrap_or_default();
718 let cross_pos = if writing_mode.is_vertical() {
719 old_relative_pos.y
720 } else {
721 old_relative_pos.x
722 };
723 let new_relative_pos =
724 LogicalPosition::from_main_cross(new_main_pos, cross_pos, writing_mode);
725
726 let new_absolute_pos = LogicalPosition::new(
727 content_box_origin.x + new_relative_pos.x,
728 content_box_origin.y + new_relative_pos.y,
729 );
730
731 if old_pos != new_absolute_pos {
732 let delta = LogicalPosition::new(
733 new_absolute_pos.x - old_pos.x,
734 new_absolute_pos.y - old_pos.y,
735 );
736 shift_subtree_position(child_idx, delta, tree, calculated_positions);
737 }
738
739 main_pen += margin_box_main_size;
740 }
741 }
742}
743
744fn shift_subtree_position(
746 node_idx: usize,
747 delta: LogicalPosition,
748 tree: &LayoutTree,
749 calculated_positions: &mut super::PositionVec,
750) {
751 if let Some(pos) = calculated_positions.get_mut(node_idx) {
752 pos.x += delta.x;
753 pos.y += delta.y;
754 }
755
756 if let Some(node) = tree.get(node_idx) {
757 let children = tree.children(node_idx).to_vec();
758 for &child_idx in &children {
759 shift_subtree_position(child_idx, delta, tree, calculated_positions);
760 }
761 }
762}
763
764fn layout_relevant_child_count(
783 styled_dom: &StyledDom,
784 children: &[NodeId],
785 parent_id: NodeId,
786) -> usize {
787 use super::getters::{get_display_property, MultiValue};
788 use super::layout_tree::{is_block_level, is_whitespace_only_text};
789
790 let parent_display = match get_display_property(styled_dom, Some(parent_id)) {
791 MultiValue::Exact(d) => d,
792 _ => LayoutDisplay::Block,
793 };
794 let is_table_structural = matches!(
795 parent_display,
796 LayoutDisplay::Table
797 | LayoutDisplay::InlineTable
798 | LayoutDisplay::TableRowGroup
799 | LayoutDisplay::TableHeaderGroup
800 | LayoutDisplay::TableFooterGroup
801 | LayoutDisplay::TableRow
802 );
803
804 let has_any_block_child = children
805 .iter()
806 .any(|&id| is_block_level(styled_dom, id));
807
808 let mut count = 0usize;
809 let collapse_inline_whitespace = has_any_block_child;
813 for &id in children {
814 let display = match get_display_property(styled_dom, Some(id)) {
816 MultiValue::Exact(d) => d,
817 _ => LayoutDisplay::Block,
818 };
819 if matches!(display, LayoutDisplay::None) {
820 continue;
821 }
822 if is_table_structural && is_whitespace_only_text(styled_dom, id) {
824 continue;
825 }
826 if collapse_inline_whitespace
828 && !is_block_level(styled_dom, id)
829 && is_whitespace_only_text(styled_dom, id)
830 {
831 continue;
832 }
833 count += 1;
834 }
835 count
836}
837
838pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
842 ctx: &mut LayoutContext<'_, T>,
843 cache: &LayoutCache,
844 viewport: LogicalRect,
845) -> Result<(LayoutTree, ReconciliationResult)> {
846 let _probe_outer = crate::probe::Probe::span("reconcile_and_invalidate");
847 let mut new_tree_builder = LayoutTreeBuilder::new(ctx.viewport_size);
848 let mut recon_result = ReconciliationResult::default();
849 let viewport_resized = cache.viewport.is_none_or(|v| v.size != viewport.size);
859 let old_tree = if viewport_resized {
860 None
861 } else {
862 cache.tree.as_ref()
863 };
864
865 if viewport_resized {
866 recon_result.layout_roots.insert(0); }
868
869 let root_dom_id = ctx
870 .styled_dom
871 .root
872 .into_crate_internal()
873 .unwrap_or(NodeId::ZERO);
874 let root_idx = reconcile_recursive(
875 ctx.styled_dom,
876 root_dom_id,
877 old_tree.map(|t| t.root),
878 None,
879 old_tree,
880 &mut new_tree_builder,
881 &mut recon_result,
882 ctx.debug_messages,
883 )?;
884
885 let final_layout_roots = recon_result
887 .layout_roots
888 .iter()
889 .filter(|&&idx| {
890 let mut current = new_tree_builder.get(idx).and_then(|n| n.parent);
891 while let Some(p_idx) = current {
892 if recon_result.layout_roots.contains(&p_idx) {
893 return false;
894 }
895 current = new_tree_builder.get(p_idx).and_then(|n| n.parent);
896 }
897 true
898 })
899 .copied()
900 .collect();
901 recon_result.layout_roots = final_layout_roots;
902
903 let new_tree = new_tree_builder.build(root_idx);
904 { let _ = (0xCC00_0001u32); }
907 Ok((new_tree, recon_result))
908}
909
910fn is_whitespace_only_inline_run(
921 styled_dom: &StyledDom,
922 inline_run: &[(usize, NodeId)],
923 parent_dom_id: NodeId,
924) -> bool {
925 use azul_css::props::style::text::StyleWhiteSpace;
926
927 if inline_run.is_empty() {
928 return true;
929 }
930
931 let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
933 let white_space = match get_white_space_property(styled_dom, parent_dom_id, parent_state) {
934 MultiValue::Exact(ws) => Some(ws),
935 _ => None,
936 };
937
938 if matches!(
940 white_space,
941 Some(StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap |
942StyleWhiteSpace::PreLine)
943 ) {
944 return false;
945 }
946
947 let binding = styled_dom.node_data.as_container();
949 for &(_, dom_id) in inline_run {
950 if let Some(data) = binding.get(dom_id) {
951 match data.get_node_type() {
952 NodeType::Text(text) => {
953 let s = text.as_str();
954 if !s.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
955 return false; }
957 }
958 _ => {
959 return false; }
961 }
962 }
963 }
964
965 true }
967
968#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn reconcile_recursive(
975 styled_dom: &StyledDom,
976 new_dom_id: NodeId,
977 old_tree_idx: Option<usize>,
978 new_parent_idx: Option<usize>,
979 old_tree: Option<&LayoutTree>,
980 new_tree_builder: &mut LayoutTreeBuilder,
981 recon: &mut ReconciliationResult,
982 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
983) -> Result<usize> {
984 static FP_DUMP_ENABLED: std::sync::OnceLock<bool> =
989 std::sync::OnceLock::new();
990 let node_data = &styled_dom.node_data.as_container()[new_dom_id];
991
992 let old_cold = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.cold(idx)));
993 match (old_tree.is_some(), old_tree_idx.is_some(), old_cold.is_some()) {
994 (false, _, _) => drop(crate::probe::Probe::span("recon_old_tree_none")),
995 (true, false, _) => drop(crate::probe::Probe::span("recon_old_idx_none")),
996 (true, true, false) => drop(crate::probe::Probe::span("recon_cold_none")),
997 (true, true, true) => drop(crate::probe::Probe::span("recon_cold_some")),
998 }
999
1000 let new_fingerprint = {
1002 let _p = crate::probe::Probe::span("fingerprint_compute");
1003 NodeDataFingerprint::compute(
1004 node_data,
1005 styled_dom.styled_nodes.as_container().get(new_dom_id).map(|n| &n.styled_node_state),
1006 )
1007 };
1008
1009 let dirty_flag = old_cold.map_or_else(|| {
1011 drop(crate::probe::Probe::span("fp_new_node"));
1012 DirtyFlag::Layout }, |old_c| {
1014 let change_set = old_c.node_data_fingerprint.diff(&new_fingerprint);
1015 if change_set.needs_layout() {
1016 drop(crate::probe::Probe::span("fp_needs_layout"));
1017 let enabled = *FP_DUMP_ENABLED.get_or_init(|| {
1018 std::env::var_os("AZ_FP_DUMP").is_some()
1019 });
1020 if enabled {
1021 use std::sync::atomic::{AtomicUsize, Ordering};
1022 static DUMPED: AtomicUsize = AtomicUsize::new(0);
1023 let n = DUMPED.fetch_add(1, Ordering::Relaxed);
1024 if n < 10 {
1025 eprintln!(
1026 "[fp_diff {n}] dom={} old={:?} new={:?}",
1027 new_dom_id.index(),
1028 old_c.node_data_fingerprint,
1029 new_fingerprint,
1030 );
1031 }
1032 }
1033 DirtyFlag::Layout
1034 } else if change_set.needs_paint() {
1035 drop(crate::probe::Probe::span("fp_needs_paint"));
1036 DirtyFlag::Paint
1037 } else {
1038 drop(crate::probe::Probe::span("fp_clean"));
1039 DirtyFlag::None
1040 }
1041 });
1042 let is_dirty = dirty_flag >= DirtyFlag::Paint;
1043
1044 let new_node_idx = if dirty_flag >= DirtyFlag::Layout || old_tree.is_none() {
1050 { let _ = (0xBB00_0001u32); }
1051 let idx = new_tree_builder.create_node_from_dom(
1052 styled_dom,
1053 new_dom_id,
1054 new_parent_idx,
1055 debug_messages,
1056 );
1057 new_tree_builder.blockify_node_display(styled_dom, new_dom_id, idx, new_parent_idx);
1062 idx
1063 } else {
1064 { let _ = (0xBB00_0002u32); }
1065 let old_full_node = old_tree
1067 .and_then(|t| old_tree_idx.and_then(|idx| t.get_full_node(idx)))
1068 .ok_or(LayoutError::InvalidTree)?;
1069 let mut idx = new_tree_builder.clone_node_from_old(&old_full_node, new_parent_idx);
1070 if dirty_flag == DirtyFlag::Paint {
1072 if let Some(cloned) = new_tree_builder.get_mut(idx) {
1073 cloned.node_data_fingerprint = new_fingerprint;
1074 cloned.dirty_flag = DirtyFlag::Paint;
1075 }
1076 }
1077 idx
1078 };
1079
1080 { let _ = (0xAB00_0000u32 | (new_node_idx as u32 & 0xffff)); }
1083
1084 {
1088 use crate::solver3::getters::get_display_property;
1089 let display = get_display_property(styled_dom, Some(new_dom_id))
1090 .exact();
1091
1092 if matches!(display, Some(LayoutDisplay::ListItem)) {
1093 new_tree_builder.create_marker_pseudo_element(styled_dom, new_dom_id, new_node_idx);
1095 }
1096 }
1097
1098 let mut new_children_dom_ids: Vec<_> = collect_children_dom_ids(styled_dom, new_dom_id);
1100
1101 {
1106 use super::getters::{get_display_property, MultiValue};
1107 let parent_display = match get_display_property(styled_dom, Some(new_dom_id)) {
1108 MultiValue::Exact(d) => d,
1109 _ => LayoutDisplay::Block,
1110 };
1111 if matches!(parent_display,
1112 LayoutDisplay::Table
1113 | LayoutDisplay::InlineTable
1114 | LayoutDisplay::TableRowGroup
1115 | LayoutDisplay::TableHeaderGroup
1116 | LayoutDisplay::TableFooterGroup
1117 | LayoutDisplay::TableRow
1118 ) {
1119 new_children_dom_ids.retain(|&id| {
1120 !super::layout_tree::is_whitespace_only_text(styled_dom, id)
1121 });
1122 }
1123 }
1124
1125 let old_children_indices: Vec<usize> = old_tree
1132 .and_then(|t| old_tree_idx.map(|idx| t.children(idx).to_vec()))
1133 .unwrap_or_default();
1134 let old_children_by_dom: alloc::collections::BTreeMap<NodeId, usize> = old_tree
1135 .and_then(|t| old_tree_idx.map(|idx| {
1136 t.children(idx).iter()
1137 .filter_map(|&cidx| t.get(cidx).and_then(|n| n.dom_node_id).map(|did| (did, cidx)))
1138 .collect()
1139 }))
1140 .unwrap_or_default();
1141
1142 let old_layout_relevant_count = old_children_by_dom.len();
1147
1148 let new_layout_relevant_count = layout_relevant_child_count(styled_dom, &new_children_dom_ids, new_dom_id);
1155
1156 let mut children_are_different = new_layout_relevant_count != old_layout_relevant_count;
1157 let mut new_child_hashes = Vec::new();
1158
1159 let has_block_child = new_children_dom_ids
1166 .iter()
1167 .any(|&id| is_block_level(styled_dom, id));
1168
1169 let parent_is_flex_or_grid = matches!(
1178 get_display_type(styled_dom, new_dom_id),
1179 LayoutDisplay::Flex
1180 | LayoutDisplay::InlineFlex
1181 | LayoutDisplay::Grid
1182 | LayoutDisplay::InlineGrid
1183 );
1184
1185 if !has_block_child || parent_is_flex_or_grid {
1186 for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
1190 let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied();
1197
1198 let reconciled_child_idx = reconcile_recursive(
1199 styled_dom,
1200 new_child_dom_id,
1201 old_child_idx,
1202 Some(new_node_idx),
1203 old_tree,
1204 new_tree_builder,
1205 recon,
1206 debug_messages,
1207 )?;
1208 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
1209 new_child_hashes.push(child_node.subtree_hash.0);
1210 }
1211
1212 if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
1213 != new_tree_builder
1214 .get(reconciled_child_idx)
1215 .map(|n| n.subtree_hash)
1216 {
1217 children_are_different = true;
1218 }
1219 }
1220 } else {
1221 if let Some(msgs) = debug_messages.as_mut() {
1225 msgs.push(LayoutDebugMessage::info(format!(
1226 "[reconcile_recursive] Mixed content in node {}: creating anonymous IFC wrappers",
1227 new_dom_id.index()
1228 )));
1229 }
1230
1231 let mut inline_run: Vec<(usize, NodeId)> = Vec::new(); for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
1234 if is_block_level(styled_dom, new_child_dom_id) {
1235 if !inline_run.is_empty() {
1237 if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
1243 if let Some(msgs) = debug_messages.as_mut() {
1244 msgs.push(LayoutDebugMessage::info(format!(
1245 "[reconcile_recursive] Skipping whitespace-only inline run ({} nodes) between blocks in node {}",
1246 inline_run.len(),
1247 new_dom_id.index()
1248 )));
1249 }
1250 inline_run.clear();
1251 } else {
1252 let anon_idx = new_tree_builder.create_anonymous_node(
1255 new_node_idx,
1256 AnonymousBoxType::InlineWrapper,
1257 FormattingContext::Inline, );
1259
1260 if let Some(msgs) = debug_messages.as_mut() {
1261 msgs.push(LayoutDebugMessage::info(format!(
1262 "[reconcile_recursive] Created anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
1263 anon_idx,
1264 inline_run.len(),
1265 inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
1266 )));
1267 }
1268
1269 #[allow(clippy::iter_with_drain)] for (pos, inline_dom_id) in inline_run.drain(..) {
1272 let old_child_idx = old_children_by_dom.get(&inline_dom_id).copied()
1280 .or_else(|| old_tree
1281 .and_then(|t| t.dom_to_layout.get(&inline_dom_id))
1282 .and_then(|v| v.first().copied()));
1283 let reconciled_child_idx = reconcile_recursive(
1284 styled_dom,
1285 inline_dom_id,
1286 old_child_idx,
1287 Some(anon_idx), old_tree,
1289 new_tree_builder,
1290 recon,
1291 debug_messages,
1292 )?;
1293 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
1294 new_child_hashes.push(child_node.subtree_hash.0);
1295 }
1296 }
1297
1298 children_are_different = true;
1309 } }
1311
1312 let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied()
1314 .or_else(|| old_children_indices.get(i).copied());
1315 let reconciled_child_idx = reconcile_recursive(
1316 styled_dom,
1317 new_child_dom_id,
1318 old_child_idx,
1319 Some(new_node_idx),
1320 old_tree,
1321 new_tree_builder,
1322 recon,
1323 debug_messages,
1324 )?;
1325 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
1326 new_child_hashes.push(child_node.subtree_hash.0);
1327 }
1328
1329 if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
1330 != new_tree_builder
1331 .get(reconciled_child_idx)
1332 .map(|n| n.subtree_hash)
1333 {
1334 children_are_different = true;
1335 }
1336 } else {
1337 inline_run.push((i, new_child_dom_id));
1339 }
1340 }
1341
1342 if !inline_run.is_empty() {
1344 if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
1346 if let Some(msgs) = debug_messages.as_mut() {
1347 msgs.push(LayoutDebugMessage::info(format!(
1348 "[reconcile_recursive] Skipping trailing whitespace-only inline run ({} nodes) in node {}",
1349 inline_run.len(),
1350 new_dom_id.index()
1351 )));
1352 }
1353 } else {
1355 let anon_idx = new_tree_builder.create_anonymous_node(
1356 new_node_idx,
1357 AnonymousBoxType::InlineWrapper,
1358 FormattingContext::Inline, );
1360
1361 if let Some(msgs) = debug_messages.as_mut() {
1362 msgs.push(LayoutDebugMessage::info(format!(
1363 "[reconcile_recursive] Created trailing anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
1364 anon_idx,
1365 inline_run.len(),
1366 inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
1367 )));
1368 }
1369
1370 #[allow(clippy::iter_with_drain)] for (pos, inline_dom_id) in inline_run.drain(..) {
1372 let old_child_idx = old_children_by_dom.get(&inline_dom_id).copied();
1373 let reconciled_child_idx = reconcile_recursive(
1374 styled_dom,
1375 inline_dom_id,
1376 old_child_idx,
1377 Some(anon_idx),
1378 old_tree,
1379 new_tree_builder,
1380 recon,
1381 debug_messages,
1382 )?;
1383 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
1384 new_child_hashes.push(child_node.subtree_hash.0);
1385 }
1386 }
1387
1388 children_are_different = true;
1392 } }
1394 }
1395
1396 let node_self_hash = {
1399 use std::hash::{DefaultHasher, Hash, Hasher};
1400 let mut h = DefaultHasher::new();
1401 new_fingerprint.hash(&mut h);
1402 h.finish()
1403 };
1404 let final_subtree_hash = calculate_subtree_hash(node_self_hash, &new_child_hashes);
1405 if let Some(current_node) = new_tree_builder.get_mut(new_node_idx) {
1406 current_node.subtree_hash = final_subtree_hash;
1407 }
1408
1409 if dirty_flag >= DirtyFlag::Layout || children_are_different {
1411 recon.intrinsic_dirty.insert(new_node_idx);
1412 recon.layout_roots.insert(new_node_idx);
1413 } else if dirty_flag == DirtyFlag::Paint {
1414 recon.paint_dirty.insert(new_node_idx);
1415 }
1416
1417 Ok(new_node_idx)
1418}
1419
1420struct PreparedLayoutContext<'a> {
1423 constraints: LayoutConstraints<'a>,
1424 dom_id: Option<NodeId>,
1426 writing_mode: LayoutWritingMode,
1427 final_used_size: LogicalSize,
1428 box_props: crate::solver3::geometry::BoxProps,
1429}
1430
1431fn prepare_layout_context<'a, T: ParsedFontTrait>(
1437 ctx: &LayoutContext<'a, T>,
1438 tree: &LayoutTree,
1439 node_index: usize,
1440 containing_block_size: LogicalSize,
1441) -> Result<PreparedLayoutContext<'a>> {
1442 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1443 let warm = tree.warm(node_index).ok_or(LayoutError::InvalidTree)?;
1444 let dom_id = node.dom_node_id; let intrinsic = warm.intrinsic_sizes.unwrap_or_default();
1451 let final_used_size = calculate_used_size_for_node(
1452 ctx.styled_dom,
1453 dom_id, &containing_block_size,
1455 intrinsic,
1456 &node.box_props.unpack(),
1457 &ctx.viewport_size,
1458 )?;
1459
1460 let writing_mode = warm.computed_style.writing_mode;
1463 let text_align = warm.computed_style.text_align;
1464 let display = warm.computed_style.display;
1465 let overflow_y = warm.computed_style.overflow_y;
1466
1467 let height_is_auto = warm.computed_style.height.is_none();
1469
1470 let available_size_for_children = if height_is_auto {
1471 let inner_size = node.box_props.inner_size(final_used_size, writing_mode);
1473
1474 let available_width = match display {
1478 LayoutDisplay::Inline => containing_block_size.width,
1479 _ => inner_size.width,
1480 };
1481
1482 LogicalSize {
1483 width: available_width,
1484 height: containing_block_size.height,
1486 }
1487 } else {
1488 node.box_props.inner_size(final_used_size, writing_mode)
1490 };
1491
1492 let wm_ctx = crate::solver3::geometry::WritingModeContext::new(
1498 writing_mode,
1499 warm.computed_style.direction,
1500 warm.computed_style.text_orientation,
1501 );
1502 let constraints = LayoutConstraints {
1503 available_size: available_size_for_children,
1504 bfc_state: None,
1505 writing_mode,
1506 writing_mode_ctx: wm_ctx,
1507 text_align: style_text_align_to_fc(text_align),
1508 containing_block_size,
1509 available_width_type: Text3AvailableSpace::Definite(available_size_for_children.width),
1510 };
1511
1512 Ok(PreparedLayoutContext {
1513 constraints,
1514 dom_id,
1515 writing_mode,
1516 final_used_size,
1517 box_props: node.box_props.unpack(),
1518 })
1519}
1520
1521pub fn compute_scrollbar_info_core<T: ParsedFontTrait>(
1530 ctx: &LayoutContext<'_, T>,
1531 dom_id: NodeId,
1532 styled_node_state: &azul_core::styled_dom::StyledNodeState,
1533 content_size: LogicalSize,
1534 container_size: LogicalSize,
1535) -> ScrollbarRequirements {
1536 if ctx.fragmentation_context.is_some() {
1538 return ScrollbarRequirements::default();
1539 }
1540
1541 let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, styled_node_state);
1542 let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, styled_node_state);
1543
1544 let scrollbar_style = crate::solver3::getters::get_scrollbar_style_cached(
1555 ctx, dom_id, styled_node_state,
1556 );
1557 let scrollbar_width_px = scrollbar_style.reserve_width_px;
1558
1559 let mut reqs = fc::check_scrollbar_necessity(
1560 content_size,
1561 container_size,
1562 to_overflow_behavior(overflow_x),
1563 to_overflow_behavior(overflow_y),
1564 scrollbar_width_px,
1565 );
1566 reqs.visual_width_px = scrollbar_style.visual_width_px;
1567
1568 let scrollbar_gutter = get_scrollbar_gutter_property(ctx.styled_dom, dom_id, styled_node_state)
1576 .unwrap_or(azul_css::props::layout::overflow::StyleScrollbarGutter::Auto);
1577 let ob_y = to_overflow_behavior(overflow_y);
1578 let is_scroll_container = matches!(ob_y, fc::OverflowBehavior::Scroll | fc::OverflowBehavior::Auto);
1579
1580 if is_scroll_container {
1581 use azul_css::props::layout::overflow::StyleScrollbarGutter;
1582 match scrollbar_gutter {
1583 StyleScrollbarGutter::Stable => {
1584 if !reqs.needs_vertical {
1586 reqs.scrollbar_width = scrollbar_width_px;
1587 }
1588 }
1589 StyleScrollbarGutter::StableBothEdges => {
1590 reqs.scrollbar_width = scrollbar_width_px * 2.0;
1592 }
1593 StyleScrollbarGutter::Auto => {
1594 }
1596 }
1597 }
1598
1599 reqs
1600}
1601
1602fn compute_scrollbar_info<T: ParsedFontTrait>(
1607 ctx: &LayoutContext<'_, T>,
1608 dom_id: NodeId,
1609 styled_node_state: &azul_core::styled_dom::StyledNodeState,
1610 content_size: LogicalSize,
1611 box_props: &crate::solver3::geometry::BoxProps,
1612 final_used_size: LogicalSize,
1613 writing_mode: LayoutWritingMode,
1614) -> ScrollbarRequirements {
1615 let container_size = box_props.inner_size(final_used_size, writing_mode);
1616 compute_scrollbar_info_core(ctx, dom_id, styled_node_state, content_size, container_size)
1617}
1618
1619fn check_scrollbar_change(
1626 tree: &LayoutTree,
1627 node_index: usize,
1628 scrollbar_info: &ScrollbarRequirements,
1629 skip_scrollbar_check: bool,
1630) -> bool {
1631 if skip_scrollbar_check {
1632 return false;
1633 }
1634
1635 let Some(warm_node) = tree.warm(node_index) else {
1636 return false;
1637 };
1638
1639 warm_node.scrollbar_info.as_ref().map_or_else(|| scrollbar_info.needs_reflow(), |old_info| {
1640 let horizontal_changed = old_info.needs_horizontal != scrollbar_info.needs_horizontal;
1642 let vertical_changed = old_info.needs_vertical != scrollbar_info.needs_vertical;
1643 horizontal_changed || vertical_changed
1644 })
1645}
1646
1647fn calculate_content_box_pos(
1652 containing_block_pos: LogicalPosition,
1653 box_props: &crate::solver3::geometry::BoxProps,
1654) -> LogicalPosition {
1655 LogicalPosition::new(
1656 containing_block_pos.x + box_props.border.left + box_props.padding.left,
1657 containing_block_pos.y + box_props.border.top + box_props.padding.top,
1658 )
1659}
1660
1661fn log_content_box_calculation<T: ParsedFontTrait>(
1663 ctx: &mut LayoutContext<'_, T>,
1664 node_index: usize,
1665 current_node: &LayoutNodeHot,
1666 containing_block_pos: LogicalPosition,
1667 self_content_box_pos: LogicalPosition,
1668) {
1669 let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
1670 return;
1671 };
1672
1673 let dom_name = current_node
1674 .dom_node_id
1675 .and_then(|id| {
1676 ctx.styled_dom
1677 .node_data
1678 .as_container()
1679 .internal
1680 .get(id.index())
1681 }).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
1682
1683 let cbp = current_node.box_props.unpack();
1684 debug_msgs.push(LayoutDebugMessage::new(
1685 LayoutDebugMessageType::PositionCalculation,
1686 format!(
1687 "[CONTENT BOX {}] {} - margin-box pos=({:.2}, {:.2}) + border=({:.2},{:.2}) + \
1688 padding=({:.2},{:.2}) = content-box pos=({:.2}, {:.2})",
1689 node_index,
1690 dom_name,
1691 containing_block_pos.x,
1692 containing_block_pos.y,
1693 cbp.border.left,
1694 cbp.border.top,
1695 cbp.padding.left,
1696 cbp.padding.top,
1697 self_content_box_pos.x,
1698 self_content_box_pos.y
1699 ),
1700 ));
1701}
1702
1703fn log_child_positioning<T: ParsedFontTrait>(
1705 ctx: &mut LayoutContext<'_, T>,
1706 child_index: usize,
1707 child_node: &LayoutNodeHot,
1708 self_content_box_pos: LogicalPosition,
1709 child_relative_pos: LogicalPosition,
1710 child_absolute_pos: LogicalPosition,
1711) {
1712 let child_dom_name = child_node
1714 .dom_node_id
1715 .and_then(|id| {
1716 ctx.styled_dom
1717 .node_data
1718 .as_container()
1719 .internal
1720 .get(id.index())
1721 }).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
1722
1723 let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
1724 return;
1725 };
1726
1727 debug_msgs.push(LayoutDebugMessage::new(
1728 LayoutDebugMessageType::PositionCalculation,
1729 format!(
1730 "[CHILD POS {}] {} - parent content-box=({:.2}, {:.2}) + relative=({:.2}, {:.2}) + \
1731 margin=({:.2}, {:.2}) = absolute=({:.2}, {:.2})",
1732 child_index,
1733 child_dom_name,
1734 self_content_box_pos.x,
1735 self_content_box_pos.y,
1736 child_relative_pos.x,
1737 child_relative_pos.y,
1738 child_node.box_props.unpack().margin.left,
1739 child_node.box_props.unpack().margin.top,
1740 child_absolute_pos.x,
1741 child_absolute_pos.y
1742 ),
1743 ));
1744}
1745
1746fn process_inflow_child<T: ParsedFontTrait>(
1753 ctx: &mut LayoutContext<'_, T>,
1754 tree: &mut LayoutTree,
1755 text_cache: &TextLayoutCache,
1756 child_index: usize,
1757 child_relative_pos: LogicalPosition,
1758 self_content_box_pos: LogicalPosition,
1759 inner_size_after_scrollbars: LogicalSize,
1760 writing_mode: LayoutWritingMode,
1761 is_flex_or_grid: bool,
1762 calculated_positions: &mut super::PositionVec,
1763 reflow_needed_for_scrollbars: bool,
1764 float_cache: &HashMap<usize, fc::FloatingContext>,
1765) -> Result<()> {
1766 let child_warm = tree.warm_mut(child_index).ok_or(LayoutError::InvalidTree)?;
1769 child_warm.relative_position = Some(child_relative_pos);
1770
1771 let child_absolute_pos = LogicalPosition::new(
1775 self_content_box_pos.x + child_relative_pos.x,
1776 self_content_box_pos.y + child_relative_pos.y,
1777 );
1778
1779 {
1781 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1782 log_child_positioning(
1783 ctx,
1784 child_index,
1785 child_node,
1786 self_content_box_pos,
1787 child_relative_pos,
1788 child_absolute_pos,
1789 );
1790 }
1791
1792 super::pos_set(calculated_positions, child_index, child_absolute_pos);
1794
1795 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1797 let child_bp = child_node.box_props.unpack();
1798 let child_content_box_pos =
1799 calculate_content_box_pos(child_absolute_pos, &child_bp);
1800 let child_inner_size = child_bp
1801 .inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
1802 let child_children: Vec<usize> = tree.children(child_index).to_vec();
1803 let child_fc = child_node.formatting_context;
1804
1805 if !child_children.is_empty() {
1810 if is_flex_or_grid {
1811 position_flex_child_descendants(
1813 tree,
1814 child_index,
1815 child_content_box_pos,
1816 child_inner_size,
1817 calculated_positions,
1818 )?;
1819 } else {
1820 position_bfc_child_descendants(
1823 tree,
1824 child_index,
1825 child_content_box_pos,
1826 calculated_positions,
1827 );
1828 }
1829 }
1830
1831 Ok(())
1832}
1833
1834fn position_bfc_child_descendants(
1838 tree: &LayoutTree,
1839 node_index: usize,
1840 content_box_pos: LogicalPosition,
1841 calculated_positions: &mut super::PositionVec,
1842) {
1843 let Some(node) = tree.get(node_index) else { return };
1844
1845 for &child_index in tree.children(node_index) {
1846 let Some(child_node) = tree.get(child_index) else { continue };
1847
1848 let child_rel_pos = tree.warm(child_index)
1850 .and_then(|w| w.relative_position)
1851 .unwrap_or_default();
1852 let child_abs_pos = LogicalPosition::new(
1853 content_box_pos.x + child_rel_pos.x,
1854 content_box_pos.y + child_rel_pos.y,
1855 );
1856
1857 super::pos_set(calculated_positions, child_index, child_abs_pos);
1858
1859 let cbp = child_node.box_props.unpack();
1861 let child_content_box_pos = LogicalPosition::new(
1862 child_abs_pos.x + cbp.border.left + cbp.padding.left,
1863 child_abs_pos.y + cbp.border.top + cbp.padding.top,
1864 );
1865
1866 position_bfc_child_descendants(tree, child_index, child_content_box_pos, calculated_positions);
1868 }
1869}
1870
1871fn process_out_of_flow_children<T: ParsedFontTrait>(
1877 ctx: &mut LayoutContext<'_, T>,
1878 tree: &mut LayoutTree,
1879 text_cache: &mut TextLayoutCache,
1880 node_index: usize,
1881 self_content_box_pos: LogicalPosition,
1882 containing_block_size: LogicalSize,
1883 calculated_positions: &mut super::PositionVec,
1884 reflow_needed_for_scrollbars: &mut bool,
1885 float_cache: &mut HashMap<usize, fc::FloatingContext>,
1886) -> Result<()> {
1887 let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
1889 let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1890 tree.children(node_index)
1891 .iter()
1892 .filter_map(|&child_index| {
1893 if super::pos_contains(calculated_positions, child_index) {
1894 return None;
1895 }
1896 let child = tree.get(child_index)?;
1897 Some((child_index, child.dom_node_id))
1898 })
1899 .collect()
1900 };
1901
1902 for (child_index, child_dom_id_opt) in out_of_flow_children {
1903 let Some(child_dom_id) = child_dom_id_opt else {
1904 continue;
1905 };
1906
1907 let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
1908 if position_type != LayoutPosition::Absolute && position_type != LayoutPosition::Fixed {
1909 continue;
1910 }
1911
1912 super::pos_set(calculated_positions, child_index, self_content_box_pos);
1914
1915 calculate_layout_for_subtree(
1919 ctx,
1920 tree,
1921 text_cache,
1922 child_index,
1923 self_content_box_pos,
1924 containing_block_size,
1925 calculated_positions,
1926 reflow_needed_for_scrollbars,
1927 float_cache,
1928 ComputeMode::PerformLayout,
1929 )?;
1930 }
1931
1932 Ok(())
1933}
1934
1935#[allow(clippy::implicit_hasher)] #[allow(clippy::too_many_lines)] pub fn calculate_layout_for_subtree<T: ParsedFontTrait>(
1968 ctx: &mut LayoutContext<'_, T>,
1969 tree: &mut LayoutTree,
1970 text_cache: &mut TextLayoutCache,
1971 node_index: usize,
1972 containing_block_pos: LogicalPosition,
1973 containing_block_size: LogicalSize,
1974 calculated_positions: &mut super::PositionVec,
1975 reflow_needed_for_scrollbars: &mut bool,
1976 float_cache: &mut HashMap<usize, fc::FloatingContext>,
1977 compute_mode: ComputeMode,
1978) -> Result<()> {
1979 #[cfg(feature = "web_lift")]
1984 unsafe {
1985 let m = match compute_mode { ComputeMode::PerformLayout => 0xC0DE0002u32, _ => 0xC0DE0001u32 };
1986 crate::az_mark(((0x60980 + (node_index & 7) * 4)) as u32, (m) as u32);
1987 }
1988 let _probe = match compute_mode {
1989 ComputeMode::ComputeSize => crate::probe::Probe::span("size_node"),
1990 ComputeMode::PerformLayout => crate::probe::Probe::span("pos_node"),
1991 };
1992 if node_index < ctx.cache_map.entries.len() {
2011 match compute_mode {
2012 ComputeMode::ComputeSize => {
2013 let sizing_hit = ctx.cache_map.entries[node_index]
2020 .get_size(0, containing_block_size)
2021 .copied();
2022 if let Some(cached_sizing) = sizing_hit {
2023 drop(crate::probe::Probe::span("size_cache_hit_sizing"));
2026 if let Some(node) = tree.get_mut(node_index) {
2027 node.used_size = Some(cached_sizing.result_size);
2028 }
2029 if let Some(warm) = tree.warm_mut(node_index) {
2030 warm.escaped_top_margin = cached_sizing.escaped_top_margin;
2031 warm.escaped_bottom_margin = cached_sizing.escaped_bottom_margin;
2032 warm.baseline = cached_sizing.baseline;
2033 }
2034 return Ok(());
2035 }
2036 let layout_hit = ctx.cache_map.entries[node_index]
2038 .get_layout(containing_block_size)
2039 .cloned();
2040 if let Some(cached_layout) = layout_hit {
2041 drop(crate::probe::Probe::span("size_cache_hit_layout"));
2043 if let Some(node) = tree.get_mut(node_index) {
2044 node.used_size = Some(cached_layout.result_size);
2045 }
2046 if let Some(warm) = tree.warm_mut(node_index) {
2047 warm.overflow_content_size = Some(cached_layout.content_size);
2048 warm.scrollbar_info = Some(cached_layout.scrollbar_info);
2049 }
2050 return Ok(());
2051 }
2052 #[cfg(feature = "web_lift")]
2057 unsafe { crate::az_mark(((0x60A60 + (node_index & 7) * 4)) as u32, (0xC0DE0001) as u32); }
2058 drop(crate::probe::Probe::span("size_cache_miss"));
2059 }
2060 ComputeMode::PerformLayout => {
2061 let layout_hit = ctx.cache_map.entries[node_index]
2063 .get_layout(containing_block_size)
2064 .cloned();
2065 if let Some(cached_layout) = layout_hit {
2066 drop(crate::probe::Probe::span("pos_cache_hit"));
2067 if let Some(node) = tree.get_mut(node_index) {
2069 node.used_size = Some(cached_layout.result_size);
2070 }
2071 if let Some(warm) = tree.warm_mut(node_index) {
2072 warm.overflow_content_size = Some(cached_layout.content_size);
2073 warm.scrollbar_info = Some(cached_layout.scrollbar_info);
2074 }
2075
2076 let box_props = tree.get(node_index)
2077 .map(|n| n.box_props.unpack())
2078 .unwrap_or_default();
2079 let writing_mode = tree
2080 .warm(node_index)
2081 .map(|w| w.computed_style.writing_mode)
2082 .unwrap_or_default();
2083 let self_content_box_pos = calculate_content_box_pos(containing_block_pos, &box_props);
2084
2085 let result_size = cached_layout.result_size;
2087 for (child_index, child_relative_pos) in &cached_layout.child_positions {
2088 let child_abs_pos = LogicalPosition::new(
2089 self_content_box_pos.x + child_relative_pos.x,
2090 self_content_box_pos.y + child_relative_pos.y,
2091 );
2092 super::pos_set(calculated_positions, *child_index, child_abs_pos);
2093
2094 let inner = box_props.inner_size(
2095 result_size,
2096 writing_mode,
2097 );
2098 let child_available_size =
2104 cached_layout.scrollbar_info.shrink_size(inner);
2105 calculate_layout_for_subtree(
2106 ctx,
2107 tree,
2108 text_cache,
2109 *child_index,
2110 child_abs_pos,
2111 child_available_size,
2112 calculated_positions,
2113 reflow_needed_for_scrollbars,
2114 float_cache,
2115 compute_mode,
2116 )?;
2117 }
2118
2119 return Ok(());
2120 }
2121 }
2122 }
2123 }
2124
2125 if compute_mode == ComputeMode::PerformLayout {
2127 drop(crate::probe::Probe::span("pos_cache_miss"));
2128 }
2129
2130 let PreparedLayoutContext {
2134 constraints,
2135 dom_id,
2136 writing_mode,
2137 mut final_used_size,
2138 box_props,
2139 } = {
2140 let _p = crate::probe::Probe::span("prepare_layout_context");
2141 prepare_layout_context(ctx, tree, node_index, containing_block_size)?
2142 };
2143
2144 {
2152 let is_table_cell = tree.get(node_index).is_some_and(|n| {
2153 matches!(n.formatting_context, FormattingContext::TableCell)
2154 });
2155 if !is_table_cell {
2156 if let Some(node) = tree.get_mut(node_index) {
2157 node.used_size = Some(final_used_size);
2158 }
2159 }
2160 }
2161
2162 let layout_result = {
2164 let _p = crate::probe::Probe::span("layout_formatting_context");
2165 layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?
2166 };
2167 let content_size = layout_result.output.overflow_size;
2168
2169 if let Some(adjusted) = tree.get(node_index).and_then(|n| n.used_size) {
2175 final_used_size = adjusted;
2176 }
2177
2178 let styled_node_state = dom_id
2181 .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
2182 .map(|n| n.styled_node_state)
2183 .unwrap_or_default();
2184
2185 let css_height: MultiValue<LayoutHeight> = match dom_id {
2186 Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
2187 None => MultiValue::Auto, };
2189
2190 let is_scroll_container = dom_id.is_some_and(|id| {
2198 let ov_x = get_overflow_x(ctx.styled_dom, id, &styled_node_state);
2199 let ov_y = get_overflow_y(ctx.styled_dom, id, &styled_node_state);
2200 matches!(ov_x, MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto))
2201 || matches!(ov_y, MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto))
2202 });
2203
2204 if should_use_content_height(&css_height) {
2205 let skip_expansion = is_scroll_container
2206 && containing_block_size.height.is_finite()
2207 && containing_block_size.height > 0.0;
2208
2209 if !skip_expansion {
2210 final_used_size = apply_content_based_height(
2211 final_used_size,
2212 content_size,
2213 tree,
2214 node_index,
2215 writing_mode,
2216 )?;
2217 }
2218 }
2219
2220 let skip_scrollbar_check = ctx.fragmentation_context.is_some();
2223 let scrollbar_info = dom_id.map_or_else(ScrollbarRequirements::default, |id| {
2224 compute_scrollbar_info(
2225 ctx,
2226 id,
2227 &styled_node_state,
2228 content_size,
2229 &box_props,
2230 final_used_size,
2231 writing_mode,
2232 )
2233 });
2234
2235 if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
2236 *reflow_needed_for_scrollbars = true;
2237 }
2238
2239 let merged_scrollbar_info = scrollbar_info;
2240 let content_box_size = box_props.inner_size(final_used_size, writing_mode);
2241 let inner_size_after_scrollbars = merged_scrollbar_info.shrink_size(content_box_size);
2242
2243 let self_content_box_pos = {
2245 {
2246 let current_node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
2247
2248 let is_table_cell = matches!(
2250 current_node.formatting_context,
2251 FormattingContext::TableCell
2252 );
2253 if !is_table_cell || current_node.used_size.is_none() {
2254 current_node.used_size = Some(final_used_size);
2255 }
2256 }
2257
2258 if let Some(warm) = tree.warm_mut(node_index) {
2260 warm.scrollbar_info = Some(merged_scrollbar_info);
2261 warm.overflow_content_size = Some(content_size);
2264 }
2265
2266 let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
2268 let current_bp = current_node.box_props.unpack();
2269 let pos = calculate_content_box_pos(containing_block_pos, ¤t_bp);
2270 log_content_box_calculation(ctx, node_index, current_node, containing_block_pos, pos);
2271 pos
2272 };
2273
2274 let is_flex_or_grid = {
2276 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
2277 matches!(
2278 node.formatting_context,
2279 FormattingContext::Flex | FormattingContext::Grid
2280 )
2281 };
2282
2283 let positions: Vec<_> = layout_result
2286 .output
2287 .positions
2288 .iter()
2289 .map(|(&idx, &pos)| (idx, pos))
2290 .collect();
2291
2292 let child_positions_for_cache: Vec<(usize, LogicalPosition)> = positions.clone();
2294
2295 for (child_index, child_relative_pos) in positions {
2296 process_inflow_child(
2297 ctx,
2298 tree,
2299 text_cache,
2300 child_index,
2301 child_relative_pos,
2302 self_content_box_pos,
2303 inner_size_after_scrollbars,
2304 writing_mode,
2305 is_flex_or_grid,
2306 calculated_positions,
2307 *reflow_needed_for_scrollbars,
2308 float_cache,
2309 )?;
2310 }
2311
2312 process_out_of_flow_children(
2314 ctx,
2315 tree,
2316 text_cache,
2317 node_index,
2318 self_content_box_pos,
2319 inner_size_after_scrollbars,
2320 calculated_positions,
2321 reflow_needed_for_scrollbars,
2322 float_cache,
2323 )?;
2324
2325 if node_index < ctx.cache_map.entries.len() {
2329 let warm_ref = tree.warm(node_index);
2330 let baseline = warm_ref.and_then(|n| n.baseline);
2331 let escaped_top = warm_ref.and_then(|n| n.escaped_top_margin);
2332 let escaped_bottom = warm_ref.and_then(|n| n.escaped_bottom_margin);
2333
2334 ctx.cache_map.get_mut(node_index).store_layout(LayoutCacheEntry {
2336 available_size: containing_block_size,
2337 result_size: final_used_size,
2338 content_size,
2339 child_positions: child_positions_for_cache,
2340 escaped_top_margin: escaped_top,
2341 escaped_bottom_margin: escaped_bottom,
2342 scrollbar_info: merged_scrollbar_info,
2343 });
2344
2345 ctx.cache_map.get_mut(node_index).store_size(0, SizingCacheEntry {
2351 available_size: containing_block_size,
2352 result_size: final_used_size,
2353 baseline,
2354 escaped_top_margin: escaped_top,
2355 escaped_bottom_margin: escaped_bottom,
2356 });
2357 }
2358
2359 Ok(())
2360}
2361
2362fn position_flex_child_descendants(
2370 tree: &mut LayoutTree,
2371 node_index: usize,
2372 content_box_pos: LogicalPosition,
2373 available_size: LogicalSize,
2374 calculated_positions: &mut super::PositionVec,
2375) -> Result<()> {
2376 let children: Vec<usize> = tree.children(node_index).to_vec();
2377
2378 for &child_index in &children {
2379 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
2380 let child_rel_pos = tree.warm(child_index)
2381 .and_then(|w| w.relative_position)
2382 .unwrap_or_default();
2383 let child_abs_pos = LogicalPosition::new(
2384 content_box_pos.x + child_rel_pos.x,
2385 content_box_pos.y + child_rel_pos.y,
2386 );
2387
2388 super::pos_set(calculated_positions, child_index, child_abs_pos);
2390
2391 let cbp = child_node.box_props.unpack();
2393 let child_writing_mode = tree
2394 .warm(child_index)
2395 .map(|w| w.computed_style.writing_mode)
2396 .unwrap_or_default();
2397 let child_content_box = LogicalPosition::new(
2398 child_abs_pos.x
2399 + cbp.border.left
2400 + cbp.padding.left,
2401 child_abs_pos.y
2402 + cbp.border.top
2403 + cbp.padding.top,
2404 );
2405 let child_inner_size = cbp.inner_size(
2406 child_node.used_size.unwrap_or_default(),
2407 child_writing_mode,
2408 );
2409
2410 position_flex_child_descendants(
2412 tree,
2413 child_index,
2414 child_content_box,
2415 child_inner_size,
2416 calculated_positions,
2417 )?;
2418 }
2419
2420 Ok(())
2421}
2422
2423#[allow(clippy::match_same_arms)] fn should_use_content_height(css_height: &MultiValue<LayoutHeight>) -> bool {
2426 match css_height {
2427 MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
2428 true
2430 }
2431 MultiValue::Exact(height) => match height {
2432 LayoutHeight::Auto => {
2433 true
2435 }
2436 LayoutHeight::Px(px) => {
2437 use azul_css::props::basic::{pixel::PixelValue, SizeMetric};
2440 px == &PixelValue::zero()
2441 || (px.metric != SizeMetric::Px
2442 && px.metric != SizeMetric::Percent
2443 && px.metric != SizeMetric::Em
2444 && px.metric != SizeMetric::Rem)
2445 }
2446 LayoutHeight::MinContent | LayoutHeight::MaxContent | LayoutHeight::FitContent(_) => {
2447 true
2449 }
2450 LayoutHeight::Calc(_) => {
2451 false
2453 }
2454 },
2455 }
2456}
2457
2458fn apply_content_based_height(
2470 mut used_size: LogicalSize,
2471 content_size: LogicalSize,
2472 tree: &LayoutTree,
2473 node_index: usize,
2474 writing_mode: LayoutWritingMode,
2475) -> Result<LogicalSize> {
2476 let node_props = tree.get(node_index).ok_or(LayoutError::InvalidTree)?.box_props.unpack();
2477 let main_axis_padding_border =
2478 node_props.padding.main_sum(writing_mode) + node_props.border.main_sum(writing_mode);
2479
2480 let old_main_size = used_size.main(writing_mode);
2482 let new_main_size = content_size.main(writing_mode) + main_axis_padding_border;
2483
2484 let final_main_size = old_main_size.max(new_main_size);
2487
2488 used_size = used_size.with_main(writing_mode, final_main_size);
2489
2490 Ok(used_size)
2491}
2492
2493fn calculate_subtree_hash(node_self_hash: u64, child_hashes: &[u64]) -> SubtreeHash {
2496 let mut hasher = DefaultHasher::new();
2497 node_self_hash.hash(&mut hasher);
2498 child_hashes.hash(&mut hasher);
2499 SubtreeHash(hasher.finish())
2500}
2501
2502#[allow(clippy::implicit_hasher)] pub fn compute_counters(
2513 styled_dom: &StyledDom,
2514 tree: &LayoutTree,
2515 counters: &mut HashMap<(usize, String), i32>,
2516) {
2517 let mut counter_stacks: HashMap<String, Vec<i32>> = HashMap::new();
2520
2521 let mut scope_stack: Vec<Vec<String>> = Vec::new();
2524
2525 compute_counters_recursive(
2526 styled_dom,
2527 tree,
2528 tree.root,
2529 counters,
2530 &mut counter_stacks,
2531 &mut scope_stack,
2532 );
2533}
2534
2535#[allow(clippy::too_many_lines)] fn compute_counters_recursive(
2537 styled_dom: &StyledDom,
2538 tree: &LayoutTree,
2539 node_idx: usize,
2540 counters: &mut HashMap<(usize, String), i32>,
2541 counter_stacks: &mut HashMap<String, Vec<i32>>,
2542 scope_stack: &mut Vec<Vec<String>>,
2543) {
2544 let Some(node) = tree.get(node_idx) else {
2545 return;
2546 };
2547
2548 if tree.warm(node_idx).and_then(|w| w.pseudo_element.as_ref()).is_some() {
2552 if let Some(parent_idx) = node.parent {
2555 let parent_counters: Vec<_> = counters
2557 .iter()
2558 .filter(|((idx, _), _)| *idx == parent_idx)
2559 .map(|((_, name), &value)| (name.clone(), value))
2560 .collect();
2561
2562 for (counter_name, value) in parent_counters {
2563 counters.insert((node_idx, counter_name), value);
2564 }
2565 }
2566
2567 return;
2570 }
2571
2572 let Some(dom_id) = node.dom_node_id else {
2574 for &child_idx in tree.children(node_idx) {
2576 compute_counters_recursive(
2577 styled_dom,
2578 tree,
2579 child_idx,
2580 counters,
2581 counter_stacks,
2582 scope_stack,
2583 );
2584 }
2585 return;
2586 };
2587
2588 let node_data = &styled_dom.node_data.as_container()[dom_id];
2589 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2590 let cache = &styled_dom.css_property_cache.ptr;
2591
2592 let mut reset_counters_at_this_level = Vec::new();
2594
2595 let display = {
2598 use crate::solver3::getters::get_display_property;
2599 get_display_property(styled_dom, Some(dom_id)).exact()
2600 };
2601 let is_list_item = matches!(display, Some(LayoutDisplay::ListItem));
2602
2603 let has_counter_css = node_state.is_normal()
2606 && cache.compact_cache.as_ref().is_none_or(|cc| cc.has_counter(dom_id.index()));
2607
2608 let counter_reset = if has_counter_css {
2610 cache
2611 .get_counter_reset(node_data, &dom_id, node_state)
2612 .and_then(|v| v.get_property())
2613 } else {
2614 None
2615 };
2616
2617 if let Some(counter_reset) = counter_reset {
2618 let counter_name_str = counter_reset.counter_name.as_str();
2619 if counter_name_str != "none" {
2620 let counter_name = counter_name_str.to_string();
2621 let reset_value = counter_reset.value;
2622
2623 counter_stacks
2625 .entry(counter_name.clone())
2626 .or_default()
2627 .push(reset_value);
2628 reset_counters_at_this_level.push(counter_name);
2629 }
2630 }
2631
2632 let counter_inc = if has_counter_css {
2634 cache
2635 .get_counter_increment(node_data, &dom_id, node_state)
2636 .and_then(|v| v.get_property())
2637 } else {
2638 None
2639 };
2640
2641 if let Some(counter_inc) = counter_inc {
2642 let counter_name_str = counter_inc.counter_name.as_str();
2643 if counter_name_str != "none" {
2644 let counter_name = counter_name_str.to_string();
2645 let inc_value = counter_inc.value;
2646
2647 let stack = counter_stacks.entry(counter_name).or_default();
2649 if stack.is_empty() {
2650 stack.push(inc_value);
2652 } else if let Some(current) = stack.last_mut() {
2653 *current += inc_value;
2654 }
2655 }
2656 }
2657
2658 if is_list_item {
2660 let counter_name = "list-item".to_string();
2661 let stack = counter_stacks.entry(counter_name).or_default();
2662 if stack.is_empty() {
2663 stack.push(1);
2665 } else if let Some(current) = stack.last_mut() {
2666 *current += 1;
2667 }
2668 }
2669
2670 for (counter_name, stack) in counter_stacks.iter() {
2672 if let Some(&value) = stack.last() {
2673 counters.insert((node_idx, counter_name.clone()), value);
2674 }
2675 }
2676
2677 scope_stack.push(reset_counters_at_this_level.clone());
2679
2680 for &child_idx in tree.children(node_idx) {
2682 compute_counters_recursive(
2683 styled_dom,
2684 tree,
2685 child_idx,
2686 counters,
2687 counter_stacks,
2688 scope_stack,
2689 );
2690 }
2691
2692 if let Some(reset_counters) = scope_stack.pop() {
2694 for counter_name in reset_counters {
2695 if let Some(stack) = counter_stacks.get_mut(&counter_name) {
2696 stack.pop();
2697 }
2698 }
2699 }
2700}
2701
2702#[cfg(test)]
2703#[allow(clippy::float_cmp)]
2704mod autotest_generated {
2705 use azul_core::dom::{Dom, IdOrClass};
2706 use azul_css::props::{
2707 basic::{pixel::PixelValue, SizeMetric},
2708 layout::dimensions::CalcAstItemVec,
2709 };
2710
2711 use super::*;
2712 use crate::solver3::{
2713 display_list::DisplayList,
2714 geometry::{EdgeSizes, MarginAuto, PackedBoxProps, ResolvedBoxProps},
2715 layout_tree::{LayoutNodeCold, LayoutNodeWarm},
2716 pos_get, PositionVec, POSITION_UNSET,
2717 };
2718
2719 fn size(w: f32, h: f32) -> LogicalSize {
2724 LogicalSize::new(w, h)
2725 }
2726
2727 fn pos(x: f32, y: f32) -> LogicalPosition {
2728 LogicalPosition::new(x, y)
2729 }
2730
2731 fn sizing_entry(available: LogicalSize, result: LogicalSize) -> SizingCacheEntry {
2732 SizingCacheEntry {
2733 available_size: available,
2734 result_size: result,
2735 baseline: None,
2736 escaped_top_margin: None,
2737 escaped_bottom_margin: None,
2738 }
2739 }
2740
2741 fn layout_entry(available: LogicalSize, result: LogicalSize) -> LayoutCacheEntry {
2742 LayoutCacheEntry {
2743 available_size: available,
2744 result_size: result,
2745 content_size: result,
2746 child_positions: Vec::new(),
2747 escaped_top_margin: None,
2748 escaped_bottom_margin: None,
2749 scrollbar_info: ScrollbarRequirements::default(),
2750 }
2751 }
2752
2753 fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
2754 EdgeSizes {
2755 top,
2756 right,
2757 bottom,
2758 left,
2759 }
2760 }
2761
2762 fn box_props(margin: EdgeSizes, border: EdgeSizes, padding: EdgeSizes) -> ResolvedBoxProps {
2763 ResolvedBoxProps {
2764 margin,
2765 padding,
2766 border,
2767 margin_auto: MarginAuto::default(),
2768 }
2769 }
2770
2771 fn zero_box_props() -> ResolvedBoxProps {
2772 box_props(
2773 edges(0.0, 0.0, 0.0, 0.0),
2774 edges(0.0, 0.0, 0.0, 0.0),
2775 edges(0.0, 0.0, 0.0, 0.0),
2776 )
2777 }
2778
2779 fn hot(
2780 parent: Option<usize>,
2781 dom_node_id: Option<NodeId>,
2782 used_size: Option<LogicalSize>,
2783 bp: &ResolvedBoxProps,
2784 ) -> LayoutNodeHot {
2785 LayoutNodeHot {
2786 box_props: PackedBoxProps::pack(bp),
2787 dom_node_id,
2788 used_size,
2789 formatting_context: FormattingContext::Block {
2790 establishes_new_context: false,
2791 },
2792 parent,
2793 }
2794 }
2795
2796 fn plain(parent: Option<usize>) -> LayoutNodeHot {
2798 hot(parent, None, Some(size(0.0, 0.0)), &zero_box_props())
2799 }
2800
2801 fn build_tree(
2804 nodes: Vec<LayoutNodeHot>,
2805 warm: Vec<LayoutNodeWarm>,
2806 child_lists: &[Vec<usize>],
2807 ) -> LayoutTree {
2808 let n = nodes.len();
2809 let mut children_arena: Vec<usize> = Vec::new();
2810 let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
2811 for cl in child_lists {
2812 let start = u32::try_from(children_arena.len()).unwrap();
2813 children_arena.extend_from_slice(cl);
2814 children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
2815 }
2816 while children_offsets.len() < n {
2817 children_offsets.push((0, 0));
2818 }
2819 LayoutTree {
2820 nodes,
2821 warm,
2822 cold: vec![LayoutNodeCold::default(); n],
2823 root: 0,
2824 dom_to_layout: BTreeMap::new(),
2825 children_arena,
2826 children_offsets,
2827 subtree_needs_intrinsic: Vec::new(),
2828 }
2829 }
2830
2831 fn warm_default(n: usize) -> Vec<LayoutNodeWarm> {
2832 vec![LayoutNodeWarm::default(); n]
2833 }
2834
2835 fn div_class(class: &str) -> Dom {
2836 Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
2837 }
2838
2839 fn styled(dom: Dom, css_str: &str) -> StyledDom {
2840 let mut dom = dom;
2841 let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
2842 StyledDom::create(&mut dom, css)
2843 }
2844
2845 fn whitespace_dom(css_str: &str) -> StyledDom {
2849 styled(
2850 Dom::create_body().with_child(
2851 div_class("p")
2852 .with_child(Dom::create_text(" \n\t"))
2853 .with_child(Dom::create_text("hi"))
2854 .with_child(Dom::create_div())
2855 .with_child(Dom::create_text("\u{00A0}")),
2856 ),
2857 css_str,
2858 )
2859 }
2860
2861 #[test]
2866 fn nodecache_default_is_empty_and_never_hits() {
2867 let c = NodeCache::default();
2868 assert!(c.is_empty);
2869 assert!(c.layout_entry.is_none());
2870 assert!(c.measure_entries.iter().all(Option::is_none));
2871 for slot in 0..9 {
2872 assert!(c.get_size(slot, size(0.0, 0.0)).is_none());
2873 assert!(c.get_size(slot, size(100.0, 100.0)).is_none());
2874 }
2875 assert!(c.get_layout(size(0.0, 0.0)).is_none());
2876 }
2877
2878 #[test]
2879 fn nodecache_store_size_then_exact_lookup_round_trips() {
2880 let mut c = NodeCache::default();
2881 c.store_size(0, sizing_entry(size(200.0, 100.0), size(50.0, 40.0)));
2882 assert!(!c.is_empty);
2883
2884 let hit = c.get_size(0, size(200.0, 100.0)).expect("exact hit");
2885 assert_eq!(hit.available_size, size(200.0, 100.0));
2886 assert_eq!(hit.result_size, size(50.0, 40.0));
2887 }
2888
2889 #[test]
2890 fn nodecache_get_size_result_matches_request() {
2891 let mut c = NodeCache::default();
2893 c.store_size(0, sizing_entry(size(200.0, 100.0), size(50.0, 40.0)));
2894
2895 let hit = c.get_size(0, size(50.0, 40.0)).expect("result-matches-request hit");
2896 assert_eq!(hit.result_size, size(50.0, 40.0));
2897 assert!(c.get_size(0, size(51.0, 41.0)).is_none());
2899 }
2900
2901 #[test]
2902 fn nodecache_get_size_epsilon_boundary() {
2903 let mut c = NodeCache::default();
2904 c.store_size(0, sizing_entry(size(100.0, 100.0), size(10.0, 10.0)));
2905
2906 assert!(c.get_size(0, size(100.05, 99.95)).is_some());
2908 assert!(c.get_size(0, size(100.2, 100.0)).is_none());
2912 assert!(c.get_size(0, size(100.0, 99.5)).is_none());
2913 }
2914
2915 #[test]
2916 fn nodecache_get_size_nan_request_misses_instead_of_panicking() {
2917 let mut c = NodeCache::default();
2918 c.store_size(0, sizing_entry(size(100.0, 100.0), size(10.0, 10.0)));
2919
2920 assert!(c.get_size(0, size(f32::NAN, 100.0)).is_none());
2922 assert!(c.get_size(0, size(100.0, f32::NAN)).is_none());
2923 assert!(c.get_size(0, size(f32::NAN, f32::NAN)).is_none());
2924 }
2925
2926 #[test]
2927 fn nodecache_nan_and_infinite_entries_are_unreachable() {
2928 let mut c = NodeCache::default();
2932 c.store_size(
2933 0,
2934 sizing_entry(size(f32::INFINITY, f32::INFINITY), size(f32::INFINITY, f32::INFINITY)),
2935 );
2936 assert!(c.get_size(0, size(f32::INFINITY, f32::INFINITY)).is_none());
2937
2938 c.store_size(1, sizing_entry(size(f32::NAN, f32::NAN), size(f32::NAN, f32::NAN)));
2939 assert!(c.get_size(1, size(f32::NAN, f32::NAN)).is_none());
2940 assert!(!c.is_empty);
2942 }
2943
2944 #[test]
2945 fn nodecache_handles_zero_and_negative_sizes() {
2946 let mut c = NodeCache::default();
2947 c.store_size(0, sizing_entry(size(0.0, 0.0), size(0.0, 0.0)));
2948 assert!(c.get_size(0, size(0.0, 0.0)).is_some());
2949 assert!(c.get_size(0, size(-0.0, -0.0)).is_some());
2950
2951 c.store_size(1, sizing_entry(size(-100.0, -50.0), size(-1.0, -1.0)));
2952 let hit = c.get_size(1, size(-100.0, -50.0)).expect("negative sizes are deterministic");
2953 assert_eq!(hit.result_size, size(-1.0, -1.0));
2954 }
2955
2956 #[test]
2957 fn nodecache_extreme_finite_sizes_do_not_panic() {
2958 let mut c = NodeCache::default();
2959 c.store_size(0, sizing_entry(size(f32::MAX, f32::MIN), size(f32::MAX, f32::MIN)));
2960 assert!(c.get_size(0, size(f32::MAX, f32::MIN)).is_some());
2962 assert!(c.get_size(0, size(f32::MIN, f32::MAX)).is_none());
2964 }
2965
2966 #[test]
2967 fn nodecache_slots_are_independent() {
2968 let mut c = NodeCache::default();
2969 c.store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
2970 c.store_size(8, sizing_entry(size(2.0, 2.0), size(2.0, 2.0)));
2971
2972 assert!(c.get_size(0, size(1.0, 1.0)).is_some());
2973 assert!(c.get_size(8, size(2.0, 2.0)).is_some());
2974 assert!(c.get_size(0, size(2.0, 2.0)).is_none());
2976 assert!(c.get_size(8, size(1.0, 1.0)).is_none());
2977 assert!(c.get_size(4, size(1.0, 1.0)).is_none());
2978 }
2979
2980 #[test]
2981 #[should_panic(expected = "index out of bounds")]
2982 fn nodecache_get_size_slot_out_of_range_panics() {
2983 let c = NodeCache::default();
2986 let _ = c.get_size(9, size(0.0, 0.0));
2987 }
2988
2989 #[test]
2990 #[should_panic(expected = "index out of bounds")]
2991 fn nodecache_store_size_slot_out_of_range_panics() {
2992 let mut c = NodeCache::default();
2993 c.store_size(9, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
2994 }
2995
2996 #[test]
2997 fn nodecache_layout_slot_round_trips_and_matches_result() {
2998 let mut c = NodeCache::default();
2999 c.store_layout(layout_entry(size(800.0, 600.0), size(800.0, 123.0)));
3000 assert!(!c.is_empty);
3001
3002 assert!(c.get_layout(size(800.0, 600.0)).is_some());
3003 let hit = c.get_layout(size(800.0, 123.0)).expect("result-matches-request");
3005 assert_eq!(hit.result_size, size(800.0, 123.0));
3006 assert!(c.get_layout(size(400.0, 300.0)).is_none());
3007 assert!(c.get_layout(size(f32::NAN, f32::NAN)).is_none());
3008 }
3009
3010 #[test]
3011 fn nodecache_clear_wipes_every_slot() {
3012 let mut c = NodeCache::default();
3013 for slot in 0..9 {
3014 c.store_size(slot, sizing_entry(size(10.0, 10.0), size(10.0, 10.0)));
3015 }
3016 c.store_layout(layout_entry(size(10.0, 10.0), size(10.0, 10.0)));
3017 assert!(!c.is_empty);
3018
3019 c.clear();
3020
3021 assert!(c.is_empty);
3022 assert!(c.layout_entry.is_none());
3023 assert!(c.measure_entries.iter().all(Option::is_none));
3024 assert!(c.get_size(0, size(10.0, 10.0)).is_none());
3025 assert!(c.get_layout(size(10.0, 10.0)).is_none());
3026
3027 c.clear();
3029 assert!(c.is_empty);
3030 }
3031
3032 #[test]
3033 fn slot_index_always_lands_in_range_and_partitions_the_unknown_case() {
3034 use AvailableWidthType::{Definite, MaxContent, MinContent};
3035 let types = [Definite, MinContent, MaxContent];
3036
3037 for &wt in &types {
3038 for &ht in &types {
3039 for wk in [true, false] {
3040 for hk in [true, false] {
3041 let slot = NodeCache::slot_index(wk, hk, wt, ht);
3042 assert!(slot < 9, "slot {slot} out of the 9-slot range");
3043 }
3044 }
3045 }
3046 }
3047
3048 for &wt in &types {
3050 for &ht in &types {
3051 assert_eq!(NodeCache::slot_index(true, true, wt, ht), 0);
3052 }
3053 }
3054
3055 let mut neither: Vec<usize> = Vec::new();
3057 for &wt in &[Definite, MinContent] {
3058 for &ht in &[Definite, MinContent] {
3059 neither.push(NodeCache::slot_index(false, false, wt, ht));
3060 }
3061 }
3062 neither.sort_unstable();
3063 assert_eq!(neither, vec![5, 6, 7, 8]);
3064 }
3065
3066 #[test]
3067 fn slot_index_collapses_definite_and_maxcontent_onto_one_slot() {
3068 use AvailableWidthType::{Definite, MaxContent, MinContent};
3069 assert_eq!(
3071 NodeCache::slot_index(true, false, Definite, Definite),
3072 NodeCache::slot_index(true, false, MaxContent, Definite)
3073 );
3074 assert_ne!(
3075 NodeCache::slot_index(true, false, Definite, Definite),
3076 NodeCache::slot_index(true, false, MinContent, Definite)
3077 );
3078 }
3079
3080 #[test]
3081 fn slot_index_keys_the_single_unknown_axis_off_the_known_axis_type() {
3082 use AvailableWidthType::{Definite, MinContent};
3083 assert_eq!(NodeCache::slot_index(true, false, Definite, MinContent), 1);
3092 assert_eq!(NodeCache::slot_index(true, false, MinContent, Definite), 2);
3093 assert_eq!(NodeCache::slot_index(false, true, MinContent, Definite), 3);
3094 assert_eq!(NodeCache::slot_index(false, true, Definite, MinContent), 4);
3095 }
3096
3097 #[test]
3102 fn cachemap_resize_to_tree_grows_shrinks_and_zeroes() {
3103 let mut m = LayoutCacheMap::default();
3104 assert!(m.entries.is_empty());
3105
3106 m.resize_to_tree(0);
3107 assert!(m.entries.is_empty());
3108
3109 m.resize_to_tree(3);
3110 assert_eq!(m.entries.len(), 3);
3111 assert!(m.entries.iter().all(|e| e.is_empty));
3112
3113 m.get_mut(1).store_size(0, sizing_entry(size(5.0, 5.0), size(5.0, 5.0)));
3115 m.resize_to_tree(5);
3116 assert_eq!(m.entries.len(), 5);
3117 assert!(!m.get(1).is_empty);
3118 assert!(m.get(4).is_empty);
3119
3120 m.resize_to_tree(1);
3122 assert_eq!(m.entries.len(), 1);
3123 m.resize_to_tree(0);
3124 assert!(m.entries.is_empty());
3125 }
3126
3127 #[test]
3128 #[should_panic(expected = "index out of bounds")]
3129 fn cachemap_get_out_of_range_panics() {
3130 let m = LayoutCacheMap::default();
3131 let _ = m.get(0);
3132 }
3133
3134 #[test]
3135 fn cachemap_mark_dirty_out_of_range_index_is_a_noop() {
3136 let mut m = LayoutCacheMap::default();
3137 m.resize_to_tree(2);
3138 m.get_mut(0).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
3139
3140 m.mark_dirty(99, &[]);
3141 m.mark_dirty(usize::MAX, &[]);
3142
3143 assert!(!m.get(0).is_empty);
3145 assert_eq!(m.entries.len(), 2);
3146 }
3147
3148 #[test]
3149 fn cachemap_mark_dirty_propagates_up_the_ancestor_chain() {
3150 let tree = vec![plain(None), plain(Some(0)), plain(Some(1))];
3152 let mut m = LayoutCacheMap::default();
3153 m.resize_to_tree(3);
3154 for i in 0..3 {
3155 m.get_mut(i).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
3156 }
3157
3158 m.mark_dirty(2, &tree);
3159
3160 assert!(m.get(2).is_empty);
3161 assert!(m.get(1).is_empty);
3162 assert!(m.get(0).is_empty);
3163 }
3164
3165 #[test]
3166 fn cachemap_mark_dirty_stops_at_the_first_dirty_ancestor() {
3167 let tree = vec![plain(None), plain(Some(0)), plain(Some(1))];
3169 let mut m = LayoutCacheMap::default();
3170 m.resize_to_tree(3);
3171 m.get_mut(0).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
3172 m.get_mut(2).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
3173
3174 m.mark_dirty(2, &tree);
3175
3176 assert!(m.get(2).is_empty);
3177 assert!(m.get(1).is_empty);
3178 assert!(!m.get(0).is_empty);
3180 }
3181
3182 #[test]
3183 fn cachemap_mark_dirty_on_an_already_dirty_node_leaves_ancestors_alone() {
3184 let tree = vec![plain(None), plain(Some(0))];
3185 let mut m = LayoutCacheMap::default();
3186 m.resize_to_tree(2);
3187 m.get_mut(0).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
3188 m.mark_dirty(1, &tree);
3191
3192 assert!(m.get(1).is_empty);
3193 assert!(!m.get(0).is_empty);
3194 }
3195
3196 #[test]
3197 fn cachemap_mark_dirty_terminates_on_cyclic_parent_links() {
3198 let tree = vec![plain(Some(1)), plain(Some(0)), plain(Some(2))];
3201 let mut m = LayoutCacheMap::default();
3202 m.resize_to_tree(3);
3203 for i in 0..3 {
3204 m.get_mut(i).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
3205 }
3206
3207 m.mark_dirty(0, &tree);
3208 assert!(m.get(0).is_empty);
3209 assert!(m.get(1).is_empty);
3210
3211 m.mark_dirty(2, &tree);
3212 assert!(m.get(2).is_empty);
3213 }
3214
3215 #[test]
3216 fn cachemap_mark_dirty_survives_a_tree_that_disagrees_with_the_cache() {
3217 let mut m = LayoutCacheMap::default();
3218 m.resize_to_tree(3);
3219 for i in 0..3 {
3220 m.get_mut(i).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
3221 }
3222
3223 let short_tree = vec![plain(None)];
3225 m.mark_dirty(2, &short_tree);
3226 assert!(m.get(2).is_empty);
3227 assert!(!m.get(0).is_empty);
3228
3229 let dangling = vec![plain(Some(usize::MAX)), plain(None), plain(None)];
3231 m.mark_dirty(0, &dangling);
3232 assert!(m.get(0).is_empty);
3233 }
3234
3235 #[test]
3240 fn memory_report_total_bytes_sums_every_field_exactly_once() {
3241 assert_eq!(Solver3CacheMemoryReport::default().total_bytes(), 0);
3242
3243 let r = Solver3CacheMemoryReport {
3246 tree_bytes: 1,
3247 tree_report: None,
3248 calculated_positions_bytes: 2,
3249 previous_positions_bytes: 4,
3250 scroll_ids_bytes: 8,
3251 scroll_id_to_node_id_bytes: 16,
3252 counters_bytes: 32,
3253 float_cache_bytes: 64,
3254 cache_map_bytes: 128,
3255 cached_display_list_bytes: 256,
3256 };
3257 assert_eq!(r.total_bytes(), 511);
3258 }
3259
3260 #[test]
3261 fn memory_report_of_a_default_cache_is_all_zero() {
3262 let cache = LayoutCache::default();
3263 let r = cache.memory_report();
3264 assert_eq!(r.total_bytes(), 0);
3265 assert_eq!(r.tree_bytes, 0);
3266 assert!(r.tree_report.is_none());
3267 assert_eq!(r.cache_map_bytes, 0);
3268 assert_eq!(r.cached_display_list_bytes, 0);
3269 }
3270
3271 #[test]
3272 fn memory_report_accounts_for_populated_state() {
3273 let mut cache = LayoutCache::default();
3274 cache.cache_map.resize_to_tree(4);
3275 cache.calculated_positions = vec![pos(1.0, 2.0), pos(3.0, 4.0), pos(5.0, 6.0)];
3276 cache.previous_positions = vec![pos(0.0, 0.0)];
3277 cache.counters.insert((0, "list-item".to_string()), 7);
3278 cache.float_cache.insert(0, fc::FloatingContext::default());
3279 cache.scroll_ids.insert(0, 42);
3280 cache.scroll_id_to_node_id.insert(42, NodeId::ZERO);
3281 cache.cached_display_list = Some((
3282 SubtreeHash(1),
3283 LogicalRect::new(pos(0.0, 0.0), size(10.0, 10.0)),
3284 DisplayList::default(),
3285 ));
3286
3287 let r = cache.memory_report();
3288
3289 assert!(r.cache_map_bytes >= 4 * size_of::<NodeCache>());
3290 assert_eq!(r.calculated_positions_bytes, 3 * size_of::<LogicalPosition>());
3291 assert_eq!(r.previous_positions_bytes, size_of::<LogicalPosition>());
3292 assert!(r.counters_bytes >= "list-item".len());
3293 assert_eq!(r.float_cache_bytes, 256);
3294 assert_eq!(r.cached_display_list_bytes, 2048);
3295 assert_eq!(
3296 r.total_bytes(),
3297 r.tree_bytes
3298 + r.calculated_positions_bytes
3299 + r.previous_positions_bytes
3300 + r.scroll_ids_bytes
3301 + r.scroll_id_to_node_id_bytes
3302 + r.counters_bytes
3303 + r.float_cache_bytes
3304 + r.cache_map_bytes
3305 + r.cached_display_list_bytes
3306 );
3307 }
3308
3309 #[test]
3310 fn reset_incremental_drops_reuse_state_keeps_the_rest_and_is_idempotent() {
3311 let mut cache = LayoutCache {
3312 tree: Some(build_tree(vec![plain(None)], warm_default(1), &[vec![]])),
3313 ..Default::default()
3314 };
3315 cache.cache_map.resize_to_tree(2);
3316 cache.cached_display_list = Some((
3317 SubtreeHash(9),
3318 LogicalRect::new(pos(0.0, 0.0), size(1.0, 1.0)),
3319 DisplayList::default(),
3320 ));
3321 cache.prev_dom_ptr = 0xDEAD_BEEF;
3322 cache.counters.insert((0, "c".to_string()), 1);
3323 cache.float_cache.insert(0, fc::FloatingContext::default());
3324 cache.calculated_positions = vec![pos(1.0, 2.0)];
3326 cache.scroll_ids.insert(0, 5);
3327 cache.viewport = Some(LogicalRect::new(pos(0.0, 0.0), size(800.0, 600.0)));
3328
3329 cache.reset_incremental();
3330
3331 assert!(cache.tree.is_none());
3332 assert!(cache.cache_map.entries.is_empty());
3333 assert!(cache.cached_display_list.is_none());
3334 assert_eq!(cache.prev_dom_ptr, 0);
3335 assert!(cache.counters.is_empty());
3336 assert!(cache.float_cache.is_empty());
3337 assert_eq!(cache.calculated_positions.len(), 1);
3338 assert_eq!(cache.scroll_ids.len(), 1);
3339 assert!(cache.viewport.is_some());
3340
3341 cache.reset_incremental();
3343 assert!(cache.tree.is_none());
3344 assert_eq!(
3346 cache.memory_report().total_bytes(),
3347 size_of::<LogicalPosition>() + size_of::<usize>() + size_of::<u64>()
3348 );
3349 }
3350
3351 #[test]
3356 fn reconciliation_result_default_is_clean() {
3357 let r = ReconciliationResult::default();
3358 assert!(r.is_clean());
3359 assert!(!r.needs_layout());
3360 assert!(!r.needs_paint_only());
3361 }
3362
3363 #[test]
3364 fn reconciliation_result_predicates_hold_over_every_combination() {
3365 for intrinsic in [false, true] {
3366 for roots in [false, true] {
3367 for paint in [false, true] {
3368 let mut r = ReconciliationResult::default();
3369 if intrinsic {
3370 r.intrinsic_dirty.insert(0);
3371 }
3372 if roots {
3373 r.layout_roots.insert(usize::MAX);
3374 }
3375 if paint {
3376 r.paint_dirty.insert(7);
3377 }
3378
3379 let expect_layout = intrinsic || roots;
3380 assert_eq!(r.needs_layout(), expect_layout);
3381 assert_eq!(r.is_clean(), !intrinsic && !roots && !paint);
3382 assert_eq!(r.needs_paint_only(), !expect_layout && paint);
3383 assert!(!(r.is_clean() && (r.needs_layout() || r.needs_paint_only())));
3385 assert!(!(r.needs_layout() && r.needs_paint_only()));
3386 }
3387 }
3388 }
3389 }
3390
3391 #[test]
3396 fn to_overflow_behavior_maps_every_layout_overflow_variant() {
3397 assert_eq!(
3398 to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Visible)),
3399 fc::OverflowBehavior::Visible
3400 );
3401 assert_eq!(
3402 to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Hidden)),
3403 fc::OverflowBehavior::Hidden
3404 );
3405 assert_eq!(
3406 to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Scroll)),
3407 fc::OverflowBehavior::Scroll
3408 );
3409 assert_eq!(
3410 to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Auto)),
3411 fc::OverflowBehavior::Auto
3412 );
3413 assert_eq!(
3418 to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Clip)),
3419 fc::OverflowBehavior::Hidden
3420 );
3421 }
3422
3423 #[test]
3424 fn to_overflow_behavior_falls_back_to_the_initial_value() {
3425 for mv in [MultiValue::Auto, MultiValue::Initial, MultiValue::Inherit] {
3428 assert_eq!(to_overflow_behavior(mv), fc::OverflowBehavior::Visible);
3429 }
3430 }
3431
3432 #[test]
3433 fn overflow_auto_keyword_is_a_typed_value_not_a_css_wide_keyword() {
3434 let sd = styled(
3439 Dom::create_body().with_child(div_class("s")),
3440 ".s { overflow-x: auto; overflow-y: scroll; }",
3441 );
3442 let id = NodeId::new(1);
3443 let state = sd.styled_nodes.as_container()[id].styled_node_state;
3444
3445 assert_eq!(
3446 to_overflow_behavior(get_overflow_x(&sd, id, &state)),
3447 fc::OverflowBehavior::Auto
3448 );
3449 assert_eq!(
3450 to_overflow_behavior(get_overflow_y(&sd, id, &state)),
3451 fc::OverflowBehavior::Scroll
3452 );
3453 }
3454
3455 #[test]
3456 fn style_text_align_to_fc_maps_every_variant() {
3457 assert!(matches!(
3459 style_text_align_to_fc(StyleTextAlign::Start),
3460 fc::TextAlign::Start
3461 ));
3462 assert!(matches!(
3463 style_text_align_to_fc(StyleTextAlign::Left),
3464 fc::TextAlign::Start
3465 ));
3466 assert!(matches!(
3467 style_text_align_to_fc(StyleTextAlign::End),
3468 fc::TextAlign::End
3469 ));
3470 assert!(matches!(
3471 style_text_align_to_fc(StyleTextAlign::Right),
3472 fc::TextAlign::End
3473 ));
3474 assert!(matches!(
3475 style_text_align_to_fc(StyleTextAlign::Center),
3476 fc::TextAlign::Center
3477 ));
3478 assert!(matches!(
3479 style_text_align_to_fc(StyleTextAlign::Justify),
3480 fc::TextAlign::Justify
3481 ));
3482 }
3483
3484 #[test]
3489 fn should_use_content_height_for_css_wide_keywords_and_auto() {
3490 assert!(should_use_content_height(&MultiValue::Auto));
3491 assert!(should_use_content_height(&MultiValue::Initial));
3492 assert!(should_use_content_height(&MultiValue::Inherit));
3493 assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::Auto)));
3494 }
3495
3496 #[test]
3497 fn should_use_content_height_is_false_for_definite_lengths() {
3498 for pv in [
3499 PixelValue::px(100.0),
3500 PixelValue::px(-10.0),
3501 PixelValue::percent(50.0),
3502 PixelValue::percent(0.0),
3503 PixelValue::em(2.0),
3504 PixelValue::rem(2.0),
3505 ] {
3506 assert!(
3507 !should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(pv))),
3508 "expected a definite height for {pv:?}"
3509 );
3510 }
3511 }
3512
3513 #[test]
3514 fn should_use_content_height_treats_zero_px_as_content_based() {
3515 assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(
3518 PixelValue::zero()
3519 ))));
3520 assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(
3521 PixelValue::px(0.0)
3522 ))));
3523 assert!(!should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(
3525 PixelValue::percent(0.0)
3526 ))));
3527 }
3528
3529 #[test]
3530 fn should_use_content_height_treats_non_px_metrics_as_content_based() {
3531 for metric in [
3537 SizeMetric::Pt,
3538 SizeMetric::Vh,
3539 SizeMetric::Vw,
3540 SizeMetric::Cm,
3541 SizeMetric::Mm,
3542 SizeMetric::In,
3543 ] {
3544 let pv = PixelValue::from_metric(metric, 100.0);
3545 assert!(
3546 should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(pv))),
3547 "{metric:?} is currently treated as content-based"
3548 );
3549 }
3550 }
3551
3552 #[test]
3553 fn should_use_content_height_for_intrinsic_keywords_but_not_calc() {
3554 assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::MinContent)));
3555 assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::MaxContent)));
3556 assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::FitContent(
3557 PixelValue::px(10.0)
3558 ))));
3559 assert!(!should_use_content_height(&MultiValue::Exact(LayoutHeight::Calc(
3561 CalcAstItemVec::from_vec(Vec::new())
3562 ))));
3563 }
3564
3565 fn one_node_tree_with(bp: &ResolvedBoxProps) -> LayoutTree {
3570 build_tree(
3571 vec![hot(None, None, Some(size(100.0, 100.0)), bp)],
3572 warm_default(1),
3573 &[vec![]],
3574 )
3575 }
3576
3577 #[test]
3578 fn apply_content_based_height_keeps_the_larger_of_min_height_and_content() {
3579 let bp = box_props(
3580 edges(0.0, 0.0, 0.0, 0.0),
3581 edges(0.0, 0.0, 0.0, 0.0),
3582 edges(10.0, 0.0, 10.0, 0.0), );
3584 let tree = one_node_tree_with(&bp);
3585 let wm = LayoutWritingMode::HorizontalTb;
3586
3587 let out = apply_content_based_height(size(100.0, 100.0), size(0.0, 50.0), &tree, 0, wm)
3590 .expect("valid node");
3591 assert_eq!(out, size(100.0, 100.0));
3592
3593 let out = apply_content_based_height(size(100.0, 40.0), size(0.0, 50.0), &tree, 0, wm)
3595 .expect("valid node");
3596 assert_eq!(out, size(100.0, 70.0));
3597 }
3598
3599 #[test]
3600 fn apply_content_based_height_at_zero_and_in_vertical_writing_modes() {
3601 let tree = one_node_tree_with(&zero_box_props());
3602
3603 let out = apply_content_based_height(
3604 size(0.0, 0.0),
3605 size(0.0, 0.0),
3606 &tree,
3607 0,
3608 LayoutWritingMode::HorizontalTb,
3609 )
3610 .expect("valid node");
3611 assert_eq!(out, size(0.0, 0.0));
3612
3613 let out = apply_content_based_height(
3615 size(10.0, 80.0),
3616 size(50.0, 0.0),
3617 &tree,
3618 0,
3619 LayoutWritingMode::VerticalRl,
3620 )
3621 .expect("valid node");
3622 assert_eq!(out, size(50.0, 80.0));
3623 }
3624
3625 #[test]
3626 fn apply_content_based_height_does_not_propagate_nan() {
3627 let tree = one_node_tree_with(&zero_box_props());
3628 let wm = LayoutWritingMode::HorizontalTb;
3629
3630 let out = apply_content_based_height(size(100.0, 40.0), size(0.0, f32::NAN), &tree, 0, wm)
3633 .expect("valid node");
3634 assert!(!out.height.is_nan());
3635 assert_eq!(out.height, 40.0);
3636
3637 let out =
3639 apply_content_based_height(size(100.0, f32::NAN), size(0.0, 50.0), &tree, 0, wm)
3640 .expect("valid node");
3641 assert!(!out.height.is_nan());
3642 assert_eq!(out.height, 50.0);
3643 }
3644
3645 #[test]
3646 fn apply_content_based_height_saturates_at_infinity_and_rejects_bad_indices() {
3647 let tree = one_node_tree_with(&zero_box_props());
3648 let wm = LayoutWritingMode::HorizontalTb;
3649
3650 let out =
3651 apply_content_based_height(size(10.0, 10.0), size(0.0, f32::INFINITY), &tree, 0, wm)
3652 .expect("valid node");
3653 assert!(out.height.is_infinite() && out.height.is_sign_positive());
3654
3655 let out = apply_content_based_height(size(10.0, 10.0), size(0.0, f32::MAX), &tree, 0, wm)
3657 .expect("valid node");
3658 assert_eq!(out.height, f32::MAX);
3659
3660 assert!(apply_content_based_height(size(1.0, 1.0), size(1.0, 1.0), &tree, 1, wm).is_err());
3662 assert!(
3663 apply_content_based_height(size(1.0, 1.0), size(1.0, 1.0), &tree, usize::MAX, wm)
3664 .is_err()
3665 );
3666 }
3667
3668 #[test]
3673 fn subtree_hash_is_deterministic() {
3674 let a = calculate_subtree_hash(42, &[1, 2, 3]);
3675 let b = calculate_subtree_hash(42, &[1, 2, 3]);
3676 assert_eq!(a, b);
3677 assert_eq!(a, calculate_subtree_hash(42, &[1, 2, 3]));
3678 }
3679
3680 #[test]
3681 fn subtree_hash_depends_on_child_order_and_arity() {
3682 let base = calculate_subtree_hash(1, &[10, 20]);
3683 assert_ne!(base, calculate_subtree_hash(1, &[20, 10]), "order must matter");
3684 assert_ne!(base, calculate_subtree_hash(1, &[10, 20, 30]), "arity must matter");
3685 assert_ne!(base, calculate_subtree_hash(1, &[10]), "a dropped child must matter");
3686 assert_ne!(base, calculate_subtree_hash(2, &[10, 20]), "self hash must matter");
3687 }
3688
3689 #[test]
3690 fn subtree_hash_distinguishes_no_children_from_a_zero_child() {
3691 assert_ne!(
3694 calculate_subtree_hash(0, &[]),
3695 calculate_subtree_hash(0, &[0])
3696 );
3697 assert_ne!(
3698 calculate_subtree_hash(0, &[0]),
3699 calculate_subtree_hash(0, &[0, 0])
3700 );
3701 }
3702
3703 #[test]
3704 fn subtree_hash_does_not_swap_self_hash_and_child_hash() {
3705 assert_ne!(
3706 calculate_subtree_hash(7, &[9]),
3707 calculate_subtree_hash(9, &[7])
3708 );
3709 }
3710
3711 #[test]
3712 fn subtree_hash_handles_extremes_and_large_child_lists() {
3713 let extremes = calculate_subtree_hash(u64::MAX, &[u64::MAX, 0, u64::MAX]);
3714 assert_eq!(
3715 extremes,
3716 calculate_subtree_hash(u64::MAX, &[u64::MAX, 0, u64::MAX])
3717 );
3718 assert_ne!(extremes, calculate_subtree_hash(0, &[0, 0, 0]));
3719
3720 let many: Vec<u64> = (0..10_000).collect();
3722 assert_eq!(
3723 calculate_subtree_hash(1, &many),
3724 calculate_subtree_hash(1, &many)
3725 );
3726 }
3727
3728 #[test]
3733 fn content_box_pos_adds_border_and_padding_but_not_margin() {
3734 let bp = box_props(
3735 edges(999.0, 999.0, 999.0, 999.0), edges(2.0, 0.0, 0.0, 3.0), edges(5.0, 0.0, 0.0, 7.0), );
3739 let out = calculate_content_box_pos(pos(10.0, 20.0), &bp);
3740 assert_eq!(out, pos(10.0 + 3.0 + 7.0, 20.0 + 2.0 + 5.0));
3741
3742 assert_eq!(
3744 calculate_content_box_pos(pos(4.0, 5.0), &zero_box_props()),
3745 pos(4.0, 5.0)
3746 );
3747 }
3748
3749 #[test]
3750 fn content_box_pos_with_negative_edges_shifts_backwards() {
3751 let bp = box_props(
3752 edges(0.0, 0.0, 0.0, 0.0),
3753 edges(-1.0, 0.0, 0.0, -2.0),
3754 edges(-3.0, 0.0, 0.0, -4.0),
3755 );
3756 assert_eq!(
3757 calculate_content_box_pos(pos(0.0, 0.0), &bp),
3758 pos(-6.0, -4.0)
3759 );
3760 }
3761
3762 #[test]
3763 fn content_box_pos_with_non_finite_edges_is_deterministic() {
3764 let nan_bp = box_props(
3765 edges(0.0, 0.0, 0.0, 0.0),
3766 edges(f32::NAN, 0.0, 0.0, f32::NAN),
3767 edges(0.0, 0.0, 0.0, 0.0),
3768 );
3769 let out = calculate_content_box_pos(pos(1.0, 1.0), &nan_bp);
3770 assert!(out.x.is_nan() && out.y.is_nan(), "NaN edges propagate, no panic");
3771
3772 let inf_bp = box_props(
3774 edges(0.0, 0.0, 0.0, 0.0),
3775 edges(f32::INFINITY, 0.0, 0.0, f32::INFINITY),
3776 edges(0.0, 0.0, 0.0, 0.0),
3777 );
3778 let out = calculate_content_box_pos(pos(f32::NEG_INFINITY, f32::NEG_INFINITY), &inf_bp);
3779 assert!(out.x.is_nan() && out.y.is_nan());
3780
3781 let max_bp = box_props(
3783 edges(0.0, 0.0, 0.0, 0.0),
3784 edges(f32::MAX, 0.0, 0.0, f32::MAX),
3785 edges(f32::MAX, 0.0, 0.0, f32::MAX),
3786 );
3787 let out = calculate_content_box_pos(pos(f32::MAX, f32::MAX), &max_bp);
3788 assert!(out.x.is_infinite() && out.x.is_sign_positive());
3789 assert!(out.y.is_infinite() && out.y.is_sign_positive());
3790 }
3791
3792 fn scrollbars(h: bool, v: bool, w: f32) -> ScrollbarRequirements {
3797 ScrollbarRequirements {
3798 needs_horizontal: h,
3799 needs_vertical: v,
3800 scrollbar_width: w,
3801 scrollbar_height: w,
3802 visual_width_px: w,
3803 }
3804 }
3805
3806 fn tree_with_scrollbar_info(info: Option<ScrollbarRequirements>) -> LayoutTree {
3807 let mut warm = warm_default(1);
3808 warm[0].scrollbar_info = info;
3809 build_tree(vec![plain(None)], warm, &[vec![]])
3810 }
3811
3812 #[test]
3813 fn check_scrollbar_change_skip_flag_short_circuits_everything() {
3814 let tree = tree_with_scrollbar_info(Some(scrollbars(false, false, 0.0)));
3815 assert!(!check_scrollbar_change(&tree, 0, &scrollbars(true, true, 16.0), true));
3817 }
3818
3819 #[test]
3820 fn check_scrollbar_change_out_of_range_node_is_false() {
3821 let tree = tree_with_scrollbar_info(None);
3822 assert!(!check_scrollbar_change(&tree, 1, &scrollbars(true, true, 16.0), false));
3823 assert!(!check_scrollbar_change(&tree, usize::MAX, &scrollbars(true, true, 16.0), false));
3824 }
3825
3826 #[test]
3827 fn check_scrollbar_change_without_previous_info_uses_needs_reflow() {
3828 let tree = tree_with_scrollbar_info(None);
3829 assert!(!check_scrollbar_change(&tree, 0, &scrollbars(true, true, 0.0), false));
3831 assert!(check_scrollbar_change(&tree, 0, &scrollbars(false, false, 16.0), false));
3833 assert!(!check_scrollbar_change(&tree, 0, &scrollbars(true, true, f32::NAN), false));
3835 }
3836
3837 #[test]
3838 fn check_scrollbar_change_detects_both_addition_and_removal() {
3839 let had_vertical = tree_with_scrollbar_info(Some(scrollbars(false, true, 16.0)));
3840 assert!(check_scrollbar_change(&had_vertical, 0, &scrollbars(false, false, 0.0), false));
3842 assert!(check_scrollbar_change(&had_vertical, 0, &scrollbars(true, true, 16.0), false));
3844 assert!(!check_scrollbar_change(&had_vertical, 0, &scrollbars(false, true, 16.0), false));
3846 }
3847
3848 #[test]
3849 fn check_scrollbar_change_ignores_width_only_changes() {
3850 let tree = tree_with_scrollbar_info(Some(scrollbars(false, true, 16.0)));
3855 assert!(!check_scrollbar_change(&tree, 0, &scrollbars(false, true, 4.0), false));
3856 }
3857
3858 fn shift_fixture() -> (LayoutTree, PositionVec) {
3864 let tree = build_tree(
3865 vec![plain(None), plain(Some(0)), plain(Some(0)), plain(Some(1))],
3866 warm_default(4),
3867 &[vec![1, 2], vec![3], vec![], vec![]],
3868 );
3869 let positions = vec![
3870 pos(0.0, 0.0),
3871 pos(10.0, 10.0),
3872 pos(20.0, 20.0),
3873 pos(30.0, 30.0),
3874 ];
3875 (tree, positions)
3876 }
3877
3878 #[test]
3879 fn shift_subtree_position_zero_delta_is_identity() {
3880 let (tree, mut positions) = shift_fixture();
3881 let before = positions.clone();
3882 shift_subtree_position(0, pos(0.0, 0.0), &tree, &mut positions);
3883 assert_eq!(positions, before);
3884 }
3885
3886 #[test]
3887 fn shift_subtree_position_moves_the_subtree_and_leaves_siblings_alone() {
3888 let (tree, mut positions) = shift_fixture();
3889 shift_subtree_position(1, pos(5.0, -3.0), &tree, &mut positions);
3890
3891 assert_eq!(positions[0], pos(0.0, 0.0), "parent untouched");
3892 assert_eq!(positions[1], pos(15.0, 7.0), "shifted node");
3893 assert_eq!(positions[2], pos(20.0, 20.0), "sibling untouched");
3894 assert_eq!(positions[3], pos(35.0, 27.0), "descendant follows");
3895 }
3896
3897 #[test]
3898 fn shift_subtree_position_out_of_range_node_is_a_noop() {
3899 let (tree, mut positions) = shift_fixture();
3900 let before = positions.clone();
3901 shift_subtree_position(99, pos(5.0, 5.0), &tree, &mut positions);
3902 shift_subtree_position(usize::MAX, pos(5.0, 5.0), &tree, &mut positions);
3903 assert_eq!(positions, before);
3904 }
3905
3906 #[test]
3907 fn shift_subtree_position_skips_nodes_without_a_stored_position() {
3908 let (tree, _) = shift_fixture();
3909 let mut positions = vec![pos(1.0, 1.0)];
3911 shift_subtree_position(0, pos(2.0, 2.0), &tree, &mut positions);
3912 assert_eq!(positions.len(), 1, "the vec is not grown by the shift");
3913 assert_eq!(positions[0], pos(3.0, 3.0));
3914 }
3915
3916 #[test]
3917 fn shift_subtree_position_leaves_the_unset_sentinel_unset() {
3918 let (tree, _) = shift_fixture();
3922 let mut positions = vec![pos(0.0, 0.0), POSITION_UNSET, POSITION_UNSET, POSITION_UNSET];
3923 shift_subtree_position(0, pos(7.0, 9.0), &tree, &mut positions);
3924
3925 assert_eq!(pos_get(&positions, 0), Some(pos(7.0, 9.0)));
3926 assert!(pos_get(&positions, 1).is_none());
3927 assert!(pos_get(&positions, 3).is_none());
3928 }
3929
3930 #[test]
3931 fn shift_subtree_position_nan_delta_is_confined_to_the_subtree() {
3932 let (tree, mut positions) = shift_fixture();
3933 shift_subtree_position(1, pos(f32::NAN, f32::NAN), &tree, &mut positions);
3934
3935 assert!(positions[1].x.is_nan() && positions[1].y.is_nan());
3936 assert!(positions[3].x.is_nan(), "NaN reaches the descendant");
3937 assert!(!positions[2].x.is_nan(), "the sibling is untouched");
3938 assert_eq!(positions[0], pos(0.0, 0.0));
3939 }
3940
3941 #[test]
3942 fn shift_subtree_position_walks_a_deep_chain_without_blowing_the_stack() {
3943 const DEPTH: usize = 500;
3944 let mut nodes = vec![plain(None)];
3945 let mut child_lists: Vec<Vec<usize>> = vec![vec![1]];
3946 for i in 1..DEPTH {
3947 nodes.push(plain(Some(i - 1)));
3948 child_lists.push(if i + 1 < DEPTH { vec![i + 1] } else { vec![] });
3949 }
3950 let tree = build_tree(nodes, warm_default(DEPTH), &child_lists);
3951 let mut positions = vec![pos(0.0, 0.0); DEPTH];
3952
3953 shift_subtree_position(0, pos(1.0, 2.0), &tree, &mut positions);
3954
3955 assert_eq!(positions[0], pos(1.0, 2.0));
3956 assert_eq!(positions[DEPTH - 1], pos(1.0, 2.0));
3957 }
3958
3959 #[test]
3964 fn position_bfc_child_descendants_out_of_range_node_is_a_noop() {
3965 let (tree, mut positions) = shift_fixture();
3966 let before = positions.clone();
3967 position_bfc_child_descendants(&tree, 99, pos(1.0, 1.0), &mut positions);
3968 assert_eq!(positions, before);
3969 }
3970
3971 #[test]
3972 fn position_bfc_child_descendants_converts_relative_to_absolute() {
3973 let bp = box_props(
3975 edges(0.0, 0.0, 0.0, 0.0),
3976 edges(2.0, 0.0, 0.0, 2.0),
3977 edges(3.0, 0.0, 0.0, 3.0),
3978 );
3979 let mut warm = warm_default(3);
3980 warm[1].relative_position = Some(pos(10.0, 20.0));
3981 warm[2].relative_position = Some(pos(1.0, 1.0));
3982 let tree = build_tree(
3983 vec![
3984 plain(None),
3985 hot(Some(0), None, Some(size(50.0, 50.0)), &bp),
3986 plain(Some(1)),
3987 ],
3988 warm,
3989 &[vec![1], vec![2], vec![]],
3990 );
3991
3992 let mut positions: PositionVec = Vec::new();
3993 position_bfc_child_descendants(&tree, 0, pos(100.0, 200.0), &mut positions);
3994
3995 assert_eq!(pos_get(&positions, 1), Some(pos(110.0, 220.0)));
3997 assert_eq!(pos_get(&positions, 2), Some(pos(110.0 + 5.0 + 1.0, 220.0 + 5.0 + 1.0)));
3999 }
4000
4001 #[test]
4002 fn position_bfc_child_descendants_defaults_a_missing_relative_position_to_the_origin() {
4003 let tree = build_tree(
4004 vec![plain(None), plain(Some(0))],
4005 warm_default(2), &[vec![1], vec![]],
4007 );
4008 let mut positions: PositionVec = Vec::new();
4009 position_bfc_child_descendants(&tree, 0, pos(7.0, 8.0), &mut positions);
4010 assert_eq!(pos_get(&positions, 1), Some(pos(7.0, 8.0)));
4011 }
4012
4013 #[test]
4014 fn position_flex_child_descendants_rejects_a_dangling_child_index() {
4015 let mut tree = build_tree(vec![plain(None)], warm_default(1), &[vec![99]]);
4017 let mut positions: PositionVec = Vec::new();
4018 let r = position_flex_child_descendants(
4019 &mut tree,
4020 0,
4021 pos(0.0, 0.0),
4022 size(100.0, 100.0),
4023 &mut positions,
4024 );
4025 assert!(r.is_err());
4026 }
4027
4028 #[test]
4029 fn position_flex_child_descendants_positions_children_and_grandchildren() {
4030 let mut warm = warm_default(3);
4031 warm[1].relative_position = Some(pos(5.0, 5.0));
4032 warm[2].relative_position = Some(pos(2.0, 2.0));
4033 let mut tree = build_tree(
4034 vec![plain(None), plain(Some(0)), plain(Some(1))],
4035 warm,
4036 &[vec![1], vec![2], vec![]],
4037 );
4038
4039 let mut positions: PositionVec = Vec::new();
4040 position_flex_child_descendants(
4041 &mut tree,
4042 0,
4043 pos(50.0, 60.0),
4044 size(100.0, 100.0),
4045 &mut positions,
4046 )
4047 .expect("well-formed tree");
4048
4049 assert_eq!(pos_get(&positions, 1), Some(pos(55.0, 65.0)));
4050 assert_eq!(pos_get(&positions, 2), Some(pos(57.0, 67.0)));
4051 }
4052
4053 #[test]
4058 fn collect_children_dom_ids_skips_display_none_children() {
4059 let sd = styled(
4061 Dom::create_body()
4062 .with_child(Dom::create_div())
4063 .with_child(div_class("hide"))
4064 .with_child(Dom::create_div()),
4065 ".hide { display: none; }",
4066 );
4067
4068 let children = collect_children_dom_ids(&sd, NodeId::ZERO);
4069 assert_eq!(children, vec![NodeId::new(1), NodeId::new(3)]);
4070 }
4071
4072 #[test]
4073 fn collect_children_dom_ids_for_leaves_and_unknown_parents_is_empty() {
4074 let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
4075
4076 assert!(collect_children_dom_ids(&sd, NodeId::new(1)).is_empty());
4078 assert!(collect_children_dom_ids(&sd, NodeId::new(999)).is_empty());
4080 assert!(collect_children_dom_ids(&sd, NodeId::new(usize::MAX)).is_empty());
4081 }
4082
4083 #[test]
4084 fn layout_relevant_child_count_counts_only_boxes_the_builder_would_emit() {
4085 let sd = styled(
4087 Dom::create_body()
4088 .with_child(Dom::create_div())
4089 .with_child(Dom::create_text(" "))
4090 .with_child(div_class("hide")),
4091 ".hide { display: none; }",
4092 );
4093 let children = [NodeId::new(1), NodeId::new(2), NodeId::new(3)];
4094
4095 assert_eq!(layout_relevant_child_count(&sd, &children, NodeId::ZERO), 1);
4098 assert_eq!(layout_relevant_child_count(&sd, &[], NodeId::ZERO), 0);
4100 }
4101
4102 #[test]
4107 fn is_whitespace_only_inline_run_empty_run_is_true() {
4108 let sd = whitespace_dom("");
4109 assert!(is_whitespace_only_inline_run(&sd, &[], NodeId::new(1)));
4110 }
4111
4112 #[test]
4113 fn is_whitespace_only_inline_run_detects_collapsible_whitespace_text() {
4114 let sd = whitespace_dom("");
4115 assert!(is_whitespace_only_inline_run(&sd, &[(0, NodeId::new(2))], NodeId::new(1)));
4117 assert!(!is_whitespace_only_inline_run(&sd, &[(1, NodeId::new(3))], NodeId::new(1)));
4119 assert!(!is_whitespace_only_inline_run(
4121 &sd,
4122 &[(0, NodeId::new(2)), (1, NodeId::new(3))],
4123 NodeId::new(1)
4124 ));
4125 }
4126
4127 #[test]
4128 fn is_whitespace_only_inline_run_rejects_elements_and_non_ascii_spaces() {
4129 let sd = whitespace_dom("");
4130 assert!(!is_whitespace_only_inline_run(&sd, &[(2, NodeId::new(4))], NodeId::new(1)));
4132 assert!(!is_whitespace_only_inline_run(&sd, &[(3, NodeId::new(5))], NodeId::new(1)));
4135 }
4136
4137 #[test]
4138 fn is_whitespace_only_inline_run_respects_white_space_pre() {
4139 let sd = whitespace_dom(".p { white-space: pre; }");
4142 assert!(!is_whitespace_only_inline_run(&sd, &[(0, NodeId::new(2))], NodeId::new(1)));
4143
4144 let sd = whitespace_dom(".p { white-space: normal; }");
4146 assert!(is_whitespace_only_inline_run(&sd, &[(0, NodeId::new(2))], NodeId::new(1)));
4147 }
4148
4149 #[test]
4154 fn reposition_clean_subtrees_ignores_empty_and_dangling_roots() {
4155 let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
4156 let tree = build_tree(
4157 vec![
4158 hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
4159 plain(Some(0)),
4160 ],
4161 warm_default(2),
4162 &[vec![1], vec![]],
4163 );
4164 let mut positions = vec![pos(0.0, 0.0), pos(1.0, 1.0)];
4165 let before = positions.clone();
4166
4167 reposition_clean_subtrees(&sd, &tree, &BTreeSet::new(), &mut positions);
4169 assert_eq!(positions, before);
4170
4171 let mut roots = BTreeSet::new();
4173 roots.insert(99);
4174 roots.insert(usize::MAX);
4175 reposition_clean_subtrees(&sd, &tree, &roots, &mut positions);
4176 assert_eq!(positions, before);
4177 }
4178
4179 #[test]
4180 fn reposition_clean_subtrees_skips_flex_parents() {
4181 let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
4182 let mut nodes = vec![
4183 hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
4184 plain(Some(0)),
4185 ];
4186 nodes[0].formatting_context = FormattingContext::Flex;
4187 let tree = build_tree(nodes, warm_default(2), &[vec![1], vec![]]);
4188
4189 let mut positions = vec![pos(0.0, 0.0), pos(1.0, 1.0)];
4190 let before = positions.clone();
4191 let mut roots = BTreeSet::new();
4192 roots.insert(1);
4193
4194 reposition_clean_subtrees(&sd, &tree, &roots, &mut positions);
4196 assert_eq!(positions, before);
4197 }
4198
4199 #[test]
4200 fn reposition_block_flow_siblings_stacks_clean_children_from_the_content_origin() {
4201 let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
4203 let parent_bp = box_props(
4204 edges(0.0, 0.0, 0.0, 0.0),
4205 edges(5.0, 5.0, 5.0, 5.0), edges(3.0, 3.0, 3.0, 3.0), );
4208 let tree = build_tree(
4209 vec![
4210 hot(None, Some(NodeId::ZERO), Some(size(200.0, 200.0)), &parent_bp),
4211 hot(Some(0), None, Some(size(200.0, 50.0)), &zero_box_props()),
4212 hot(Some(0), None, Some(size(200.0, 30.0)), &zero_box_props()),
4213 plain(Some(1)),
4214 ],
4215 warm_default(4),
4216 &[vec![1, 2], vec![3], vec![], vec![]],
4217 );
4218 let mut positions = vec![
4219 pos(10.0, 20.0), pos(0.0, 0.0),
4221 pos(0.0, 0.0),
4222 pos(0.0, 0.0),
4223 ];
4224
4225 reposition_block_flow_siblings(&sd, 0, &tree, &BTreeSet::new(), &mut positions);
4226
4227 assert_eq!(positions[1], pos(13.0, 23.0));
4233 assert_eq!(positions[2], pos(13.0, 73.0), "second child stacked below the first");
4234 assert_eq!(positions[3], pos(13.0, 23.0));
4236 }
4237
4238 #[test]
4243 fn compute_counters_on_an_empty_or_anonymous_tree_does_not_panic() {
4244 let sd = styled(Dom::create_body(), "");
4245 let mut counters: HashMap<(usize, String), i32> = HashMap::new();
4246
4247 let empty = build_tree(Vec::new(), Vec::new(), &[]);
4249 compute_counters(&sd, &empty, &mut counters);
4250 assert!(counters.is_empty());
4251
4252 let anon = build_tree(
4254 vec![plain(None), plain(Some(0))],
4255 warm_default(2),
4256 &[vec![1], vec![]],
4257 );
4258 compute_counters(&sd, &anon, &mut counters);
4259 assert!(counters.is_empty());
4260 }
4261
4262 #[test]
4263 fn compute_counters_increments_the_list_item_counter_in_document_order() {
4264 let sd = styled(
4266 Dom::create_body()
4267 .with_child(div_class("li"))
4268 .with_child(div_class("li")),
4269 ".li { display: list-item; }",
4270 );
4271 let tree = build_tree(
4272 vec![
4273 hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
4274 hot(Some(0), Some(NodeId::new(1)), Some(size(100.0, 10.0)), &zero_box_props()),
4275 hot(Some(0), Some(NodeId::new(2)), Some(size(100.0, 10.0)), &zero_box_props()),
4276 ],
4277 warm_default(3),
4278 &[vec![1, 2], vec![], vec![]],
4279 );
4280
4281 let mut counters: HashMap<(usize, String), i32> = HashMap::new();
4282 compute_counters(&sd, &tree, &mut counters);
4283
4284 assert_eq!(counters.get(&(1, "list-item".to_string())), Some(&1));
4286 assert_eq!(counters.get(&(2, "list-item".to_string())), Some(&2));
4287 assert_eq!(counters.get(&(0, "list-item".to_string())), None);
4289 }
4290
4291 #[test]
4292 fn compute_counters_leaves_plain_elements_alone() {
4293 let sd = styled(
4294 Dom::create_body().with_child(Dom::create_div()),
4295 "",
4296 );
4297 let tree = build_tree(
4298 vec![
4299 hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
4300 hot(Some(0), Some(NodeId::new(1)), Some(size(100.0, 10.0)), &zero_box_props()),
4301 ],
4302 warm_default(2),
4303 &[vec![1], vec![]],
4304 );
4305
4306 let mut counters: HashMap<(usize, String), i32> = HashMap::new();
4307 compute_counters(&sd, &tree, &mut counters);
4308 assert!(counters.is_empty(), "no counter-reset/increment → no counters");
4309 }
4310}