1use std::{
17 collections::{BTreeMap, BTreeSet},
18 hash::{DefaultHasher, Hash, Hasher},
19};
20
21use azul_core::{
22 dom::{FormattingContext, NodeId, NodeType},
23 geom::{LogicalPosition, LogicalRect, LogicalSize},
24 styled_dom::{StyledDom, StyledNode},
25};
26use azul_css::{
27 css::CssPropertyValue,
28 props::{
29 layout::{
30 LayoutDisplay, LayoutFlexWrap, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
31 LayoutPosition, LayoutWrap, LayoutWritingMode,
32 },
33 property::{CssProperty, CssPropertyType},
34 style::StyleTextAlign,
35 },
36 LayoutDebugMessage, LayoutDebugMessageType,
37};
38
39use crate::{
40 font_traits::{FontLoaderTrait, ParsedFontTrait, TextLayoutCache},
41 solver3::{
42 fc::{self, layout_formatting_context, LayoutConstraints, OverflowBehavior},
43 geometry::PositionedRectangle,
44 getters::{
45 get_css_height, get_display_property, get_justify_content, get_overflow_x,
46 get_overflow_y, get_text_align, get_wrap, get_writing_mode, MultiValue,
47 },
48 layout_tree::{
49 is_block_level, AnonymousBoxType, LayoutNode, LayoutTreeBuilder, SubtreeHash,
50 },
51 positioning::get_position_type,
52 scrollbar::ScrollbarRequirements,
53 sizing::calculate_used_size_for_node,
54 LayoutContext, LayoutError, LayoutTree, Result,
55 },
56 text3::cache::AvailableSpace as Text3AvailableSpace,
57};
58
59#[derive(Debug, Clone, Default)]
61pub struct LayoutCache {
62 pub tree: Option<LayoutTree>,
64 pub calculated_positions: BTreeMap<usize, LogicalPosition>,
66 pub viewport: Option<LogicalRect>,
68 pub scroll_ids: BTreeMap<usize, u64>,
70 pub scroll_id_to_node_id: BTreeMap<u64, NodeId>,
72 pub counters: BTreeMap<(usize, String), i32>,
77 pub float_cache: BTreeMap<usize, fc::FloatingContext>,
82}
83
84#[derive(Debug, Default)]
86pub struct ReconciliationResult {
87 pub intrinsic_dirty: BTreeSet<usize>,
89 pub layout_roots: BTreeSet<usize>,
91}
92
93impl ReconciliationResult {
94 pub fn is_clean(&self) -> bool {
96 self.intrinsic_dirty.is_empty() && self.layout_roots.is_empty()
97 }
98}
99
100pub fn reposition_clean_subtrees(
108 styled_dom: &StyledDom,
109 tree: &LayoutTree,
110 layout_roots: &BTreeSet<usize>,
111 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
112) {
113 let mut parents_to_reposition = BTreeSet::new();
116 for &root_idx in layout_roots {
117 if let Some(parent_idx) = tree.get(root_idx).and_then(|n| n.parent) {
118 parents_to_reposition.insert(parent_idx);
119 }
120 }
121
122 for parent_idx in parents_to_reposition {
123 let parent_node = match tree.get(parent_idx) {
124 Some(n) => n,
125 None => continue,
126 };
127
128 match parent_node.formatting_context {
130 FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
132 reposition_block_flow_siblings(
133 styled_dom,
134 parent_idx,
135 parent_node,
136 tree,
137 layout_roots,
138 calculated_positions,
139 );
140 }
141
142 FormattingContext::Flex | FormattingContext::Grid => {
143 }
147
148 FormattingContext::Table | FormattingContext::TableRow => {
149 }
153
154 _ => { }
158 }
159 }
160}
161
162pub fn to_overflow_behavior(overflow: MultiValue<LayoutOverflow>) -> fc::OverflowBehavior {
164 match overflow.unwrap_or_default() {
165 LayoutOverflow::Visible => fc::OverflowBehavior::Visible,
166 LayoutOverflow::Hidden | LayoutOverflow::Clip => fc::OverflowBehavior::Hidden,
167 LayoutOverflow::Scroll => fc::OverflowBehavior::Scroll,
168 LayoutOverflow::Auto => fc::OverflowBehavior::Auto,
169 }
170}
171
172pub const fn style_text_align_to_fc(text_align: StyleTextAlign) -> fc::TextAlign {
174 match text_align {
175 StyleTextAlign::Start | StyleTextAlign::Left => fc::TextAlign::Start,
176 StyleTextAlign::End | StyleTextAlign::Right => fc::TextAlign::End,
177 StyleTextAlign::Center => fc::TextAlign::Center,
178 StyleTextAlign::Justify => fc::TextAlign::Justify,
179 }
180}
181
182pub fn collect_children_dom_ids(styled_dom: &StyledDom, parent_dom_id: NodeId) -> Vec<NodeId> {
186 let hierarchy_container = styled_dom.node_hierarchy.as_container();
187 let mut children = Vec::new();
188
189 let Some(hierarchy_item) = hierarchy_container.get(parent_dom_id) else {
190 return children;
191 };
192
193 let Some(mut child_id) = hierarchy_item.first_child_id(parent_dom_id) else {
194 return children;
195 };
196
197 children.push(child_id);
198 while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
199 let Some(next) = hierarchy_item.next_sibling_id() else {
200 break;
201 };
202 children.push(next);
203 child_id = next;
204 }
205
206 children
207}
208
209pub fn is_simple_flex_stack(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> bool {
212 let Some(id) = dom_id else { return false };
213 let binding = styled_dom.styled_nodes.as_container();
214 let styled_node = match binding.get(id) {
215 Some(styled_node) => styled_node,
216 None => return false,
217 };
218
219 let wrap = get_wrap(styled_dom, id, &styled_node.styled_node_state);
221
222 if wrap.unwrap_or_default() != LayoutFlexWrap::NoWrap {
223 return false;
224 }
225
226 let justify = get_justify_content(styled_dom, id, &styled_node.styled_node_state);
228
229 if !matches!(
230 justify.unwrap_or_default(),
231 LayoutJustifyContent::FlexStart | LayoutJustifyContent::Start
232 ) {
233 return false;
234 }
235
236 true
245}
246
247pub fn reposition_block_flow_siblings(
251 styled_dom: &StyledDom,
252 parent_idx: usize,
253 parent_node: &LayoutNode,
254 tree: &LayoutTree,
255 layout_roots: &BTreeSet<usize>,
256 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
257) {
258 let dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
259 let styled_node_state = styled_dom
260 .styled_nodes
261 .as_container()
262 .get(dom_id)
263 .map(|n| n.styled_node_state.clone())
264 .unwrap_or_default();
265
266 let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
267
268 let parent_pos = calculated_positions
269 .get(&parent_idx)
270 .copied()
271 .unwrap_or_default();
272
273 let content_box_origin = LogicalPosition::new(
274 parent_pos.x + parent_node.box_props.padding.left,
275 parent_pos.y + parent_node.box_props.padding.top,
276 );
277
278 let mut main_pen = 0.0;
279
280 for &child_idx in &parent_node.children {
281 let child_node = match tree.get(child_idx) {
282 Some(n) => n,
283 None => continue,
284 };
285
286 let child_size = child_node.used_size.unwrap_or_default();
287 let child_main_sum = child_node.box_props.margin.main_sum(writing_mode);
288 let margin_box_main_size = child_size.main(writing_mode) + child_main_sum;
289
290 if layout_roots.contains(&child_idx) {
291 let new_pos = match calculated_positions.get(&child_idx) {
294 Some(p) => *p,
295 None => continue,
296 };
297
298 let main_axis_offset = if writing_mode.is_vertical() {
299 new_pos.x - content_box_origin.x
300 } else {
301 new_pos.y - content_box_origin.y
302 };
303
304 main_pen = main_axis_offset
305 + child_size.main(writing_mode)
306 + child_node.box_props.margin.main_end(writing_mode);
307 } else {
308 let old_pos = match calculated_positions.get(&child_idx) {
311 Some(p) => *p,
312 None => continue,
313 };
314
315 let child_main_start = child_node.box_props.margin.main_start(writing_mode);
316 let new_main_pos = main_pen + child_main_start;
317 let old_relative_pos = child_node.relative_position.unwrap_or_default();
318 let cross_pos = if writing_mode.is_vertical() {
319 old_relative_pos.y
320 } else {
321 old_relative_pos.x
322 };
323 let new_relative_pos =
324 LogicalPosition::from_main_cross(new_main_pos, cross_pos, writing_mode);
325
326 let new_absolute_pos = LogicalPosition::new(
327 content_box_origin.x + new_relative_pos.x,
328 content_box_origin.y + new_relative_pos.y,
329 );
330
331 if old_pos != new_absolute_pos {
332 let delta = LogicalPosition::new(
333 new_absolute_pos.x - old_pos.x,
334 new_absolute_pos.y - old_pos.y,
335 );
336 shift_subtree_position(child_idx, delta, tree, calculated_positions);
337 }
338
339 main_pen += margin_box_main_size;
340 }
341 }
342}
343
344pub fn shift_subtree_position(
346 node_idx: usize,
347 delta: LogicalPosition,
348 tree: &LayoutTree,
349 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
350) {
351 if let Some(pos) = calculated_positions.get_mut(&node_idx) {
352 pos.x += delta.x;
353 pos.y += delta.y;
354 }
355
356 if let Some(node) = tree.get(node_idx) {
357 for &child_idx in &node.children {
358 shift_subtree_position(child_idx, delta, tree, calculated_positions);
359 }
360 }
361}
362
363pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
366 ctx: &mut LayoutContext<'_, T>,
367 cache: &LayoutCache,
368 viewport: LogicalRect,
369) -> Result<(LayoutTree, ReconciliationResult)> {
370 let mut new_tree_builder = LayoutTreeBuilder::new();
371 let mut recon_result = ReconciliationResult::default();
372 let old_tree = cache.tree.as_ref();
373
374 if cache.viewport.map_or(true, |v| v.size != viewport.size) {
376 recon_result.layout_roots.insert(0); }
378
379 let root_dom_id = ctx
380 .styled_dom
381 .root
382 .into_crate_internal()
383 .unwrap_or(NodeId::ZERO);
384 let root_idx = reconcile_recursive(
385 ctx.styled_dom,
386 root_dom_id,
387 old_tree.map(|t| t.root),
388 None,
389 old_tree,
390 &mut new_tree_builder,
391 &mut recon_result,
392 &mut ctx.debug_messages,
393 )?;
394
395 let final_layout_roots = recon_result
397 .layout_roots
398 .iter()
399 .filter(|&&idx| {
400 let mut current = new_tree_builder.get(idx).and_then(|n| n.parent);
401 while let Some(p_idx) = current {
402 if recon_result.layout_roots.contains(&p_idx) {
403 return false;
404 }
405 current = new_tree_builder.get(p_idx).and_then(|n| n.parent);
406 }
407 true
408 })
409 .copied()
410 .collect();
411 recon_result.layout_roots = final_layout_roots;
412
413 let new_tree = new_tree_builder.build(root_idx);
414 Ok((new_tree, recon_result))
415}
416
417pub fn reconcile_recursive(
419 styled_dom: &StyledDom,
420 new_dom_id: NodeId,
421 old_tree_idx: Option<usize>,
422 new_parent_idx: Option<usize>,
423 old_tree: Option<&LayoutTree>,
424 new_tree_builder: &mut LayoutTreeBuilder,
425 recon: &mut ReconciliationResult,
426 debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
427) -> Result<usize> {
428 let node_data = &styled_dom.node_data.as_container()[new_dom_id];
429
430 let old_node = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.get(idx)));
431 let new_node_data_hash = hash_styled_node_data(styled_dom, new_dom_id);
432
433 let is_dirty = old_node.map_or(true, |n| new_node_data_hash != n.node_data_hash);
436
437 let new_node_idx = if is_dirty {
438 new_tree_builder.create_node_from_dom(
439 styled_dom,
440 new_dom_id,
441 new_parent_idx,
442 debug_messages,
443 )?
444 } else {
445 new_tree_builder.clone_node_from_old(old_node.unwrap(), new_parent_idx)
446 };
447
448 {
452 let node_data = &styled_dom.node_data.as_container()[new_dom_id];
453 let node_state = &styled_dom.styled_nodes.as_container()[new_dom_id].styled_node_state;
454 let cache = &styled_dom.css_property_cache.ptr;
455
456 let display = cache
457 .get_display(node_data, &new_dom_id, node_state)
458 .and_then(|v| v.get_property().copied());
459
460 if matches!(display, Some(LayoutDisplay::ListItem)) {
461 new_tree_builder.create_marker_pseudo_element(styled_dom, new_dom_id, new_node_idx);
463 }
464 }
465
466 let new_children_dom_ids: Vec<_> = collect_children_dom_ids(styled_dom, new_dom_id);
468 let old_children_indices: Vec<_> = old_node.map(|n| n.children.clone()).unwrap_or_default();
469
470 let mut children_are_different = new_children_dom_ids.len() != old_children_indices.len();
471 let mut new_child_hashes = Vec::new();
472
473 let has_block_child = new_children_dom_ids
483 .iter()
484 .any(|&id| is_block_level(styled_dom, id));
485
486 if !has_block_child {
487 for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
490 let old_child_idx = old_children_indices.get(i).copied();
491
492 let reconciled_child_idx = reconcile_recursive(
493 styled_dom,
494 new_child_dom_id,
495 old_child_idx,
496 Some(new_node_idx),
497 old_tree,
498 new_tree_builder,
499 recon,
500 debug_messages,
501 )?;
502 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
503 new_child_hashes.push(child_node.subtree_hash.0);
504 }
505
506 if old_tree.and_then(|t| t.get(old_child_idx?).map(|n| n.subtree_hash))
507 != new_tree_builder
508 .get(reconciled_child_idx)
509 .map(|n| n.subtree_hash)
510 {
511 children_are_different = true;
512 }
513 }
514 } else {
515 if let Some(msgs) = debug_messages.as_mut() {
519 msgs.push(LayoutDebugMessage::info(format!(
520 "[reconcile_recursive] Mixed content in node {}: creating anonymous IFC wrappers",
521 new_dom_id.index()
522 )));
523 }
524
525 let mut inline_run: Vec<(usize, NodeId)> = Vec::new(); for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
528 if is_block_level(styled_dom, new_child_dom_id) {
529 if !inline_run.is_empty() {
531 let anon_idx = new_tree_builder.create_anonymous_node(
534 new_node_idx,
535 AnonymousBoxType::InlineWrapper,
536 FormattingContext::Inline, );
538
539 if let Some(msgs) = debug_messages.as_mut() {
540 msgs.push(LayoutDebugMessage::info(format!(
541 "[reconcile_recursive] Created anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
542 anon_idx,
543 inline_run.len(),
544 inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
545 )));
546 }
547
548 for (pos, inline_dom_id) in inline_run.drain(..) {
550 let old_child_idx = old_children_indices.get(pos).copied();
551 let reconciled_child_idx = reconcile_recursive(
552 styled_dom,
553 inline_dom_id,
554 old_child_idx,
555 Some(anon_idx), old_tree,
557 new_tree_builder,
558 recon,
559 debug_messages,
560 )?;
561 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
562 new_child_hashes.push(child_node.subtree_hash.0);
563 }
564 }
565
566 recon.intrinsic_dirty.insert(anon_idx);
568 children_are_different = true;
569 }
570
571 let old_child_idx = old_children_indices.get(i).copied();
573 let reconciled_child_idx = reconcile_recursive(
574 styled_dom,
575 new_child_dom_id,
576 old_child_idx,
577 Some(new_node_idx),
578 old_tree,
579 new_tree_builder,
580 recon,
581 debug_messages,
582 )?;
583 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
584 new_child_hashes.push(child_node.subtree_hash.0);
585 }
586
587 if old_tree.and_then(|t| t.get(old_child_idx?).map(|n| n.subtree_hash))
588 != new_tree_builder
589 .get(reconciled_child_idx)
590 .map(|n| n.subtree_hash)
591 {
592 children_are_different = true;
593 }
594 } else {
595 inline_run.push((i, new_child_dom_id));
597 }
598 }
599
600 if !inline_run.is_empty() {
602 let anon_idx = new_tree_builder.create_anonymous_node(
603 new_node_idx,
604 AnonymousBoxType::InlineWrapper,
605 FormattingContext::Inline, );
607
608 if let Some(msgs) = debug_messages.as_mut() {
609 msgs.push(LayoutDebugMessage::info(format!(
610 "[reconcile_recursive] Created trailing anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
611 anon_idx,
612 inline_run.len(),
613 inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
614 )));
615 }
616
617 for (pos, inline_dom_id) in inline_run.drain(..) {
618 let old_child_idx = old_children_indices.get(pos).copied();
619 let reconciled_child_idx = reconcile_recursive(
620 styled_dom,
621 inline_dom_id,
622 old_child_idx,
623 Some(anon_idx),
624 old_tree,
625 new_tree_builder,
626 recon,
627 debug_messages,
628 )?;
629 if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
630 new_child_hashes.push(child_node.subtree_hash.0);
631 }
632 }
633
634 recon.intrinsic_dirty.insert(anon_idx);
635 children_are_different = true;
636 }
637 }
638
639 let final_subtree_hash = calculate_subtree_hash(new_node_data_hash, &new_child_hashes);
641 if let Some(current_node) = new_tree_builder.get_mut(new_node_idx) {
642 current_node.subtree_hash = final_subtree_hash;
643 }
644
645 if is_dirty || children_are_different {
647 recon.intrinsic_dirty.insert(new_node_idx);
648 recon.layout_roots.insert(new_node_idx);
649 }
650
651 Ok(new_node_idx)
652}
653
654struct PreparedLayoutContext<'a> {
657 constraints: LayoutConstraints<'a>,
658 dom_id: Option<NodeId>,
660 writing_mode: LayoutWritingMode,
661 final_used_size: LogicalSize,
662 box_props: crate::solver3::geometry::BoxProps,
663}
664
665fn prepare_layout_context<'a, T: ParsedFontTrait>(
671 ctx: &LayoutContext<'a, T>,
672 node: &LayoutNode,
673 containing_block_size: LogicalSize,
674) -> Result<PreparedLayoutContext<'a>> {
675 let dom_id = node.dom_node_id; let intrinsic = node.intrinsic_sizes.clone().unwrap_or_default();
682 let final_used_size = calculate_used_size_for_node(
683 ctx.styled_dom,
684 dom_id, containing_block_size,
686 intrinsic,
687 &node.box_props,
688 )?;
689
690 let styled_node_state = dom_id
695 .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
696 .map(|n| n.styled_node_state)
697 .unwrap_or_default();
698
699 let writing_mode = match dom_id {
701 Some(id) => get_writing_mode(ctx.styled_dom, id, &styled_node_state).unwrap_or_default(),
702 None => LayoutWritingMode::default(),
703 };
704 let text_align = match dom_id {
705 Some(id) => get_text_align(ctx.styled_dom, id, &styled_node_state).unwrap_or_default(),
706 None => StyleTextAlign::default(),
707 };
708
709 let css_height: MultiValue<LayoutHeight> = match dom_id {
715 Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
716 None => MultiValue::Auto, };
718
719 let display = match dom_id {
721 Some(id) => get_display_property(ctx.styled_dom, Some(id)),
722 None => MultiValue::Auto, };
724
725 let available_size_for_children = if should_use_content_height(&css_height) {
726 let inner_size = node.box_props.inner_size(final_used_size, writing_mode);
728
729 let available_width = match display {
735 MultiValue::Exact(LayoutDisplay::Inline) | MultiValue::Auto => {
736 containing_block_size.width
738 }
739 _ => {
740 inner_size.width
742 }
743 };
744
745 LogicalSize {
746 width: available_width,
747 height: containing_block_size.height,
749 }
750 } else {
751 node.box_props.inner_size(final_used_size, writing_mode)
753 };
754
755 let scrollbar_reservation = match dom_id {
760 Some(id) => {
761 let styled_node_state = ctx
762 .styled_dom
763 .styled_nodes
764 .as_container()
765 .get(id)
766 .map(|s| s.styled_node_state.clone())
767 .unwrap_or_default();
768 let overflow_y = get_overflow_y(ctx.styled_dom, id, &styled_node_state);
769 use azul_css::props::layout::LayoutOverflow;
770 match overflow_y.unwrap_or_default() {
771 LayoutOverflow::Scroll | LayoutOverflow::Auto => fc::SCROLLBAR_WIDTH_PX,
772 _ => 0.0,
773 }
774 }
775 None => 0.0,
776 };
777
778 let available_size_for_children = if scrollbar_reservation > 0.0 {
780 LogicalSize {
781 width: (available_size_for_children.width - scrollbar_reservation).max(0.0),
782 height: available_size_for_children.height,
783 }
784 } else {
785 available_size_for_children
786 };
787
788 let constraints = LayoutConstraints {
789 available_size: available_size_for_children,
790 bfc_state: None,
791 writing_mode,
792 text_align: style_text_align_to_fc(text_align),
793 containing_block_size,
794 available_width_type: Text3AvailableSpace::Definite(available_size_for_children.width),
795 };
796
797 Ok(PreparedLayoutContext {
798 constraints,
799 dom_id,
800 writing_mode,
801 final_used_size,
802 box_props: node.box_props.clone(),
803 })
804}
805
806fn compute_scrollbar_info<T: ParsedFontTrait>(
812 ctx: &LayoutContext<'_, T>,
813 dom_id: NodeId,
814 styled_node_state: &azul_core::styled_dom::StyledNodeState,
815 content_size: LogicalSize,
816 box_props: &crate::solver3::geometry::BoxProps,
817 final_used_size: LogicalSize,
818 writing_mode: LayoutWritingMode,
819) -> ScrollbarRequirements {
820 if ctx.fragmentation_context.is_some() {
822 return ScrollbarRequirements {
823 needs_horizontal: false,
824 needs_vertical: false,
825 scrollbar_width: 0.0,
826 scrollbar_height: 0.0,
827 };
828 }
829
830 let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, styled_node_state);
831 let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, styled_node_state);
832
833 let container_size = box_props.inner_size(final_used_size, writing_mode);
834
835 fc::check_scrollbar_necessity(
836 content_size,
837 container_size,
838 to_overflow_behavior(overflow_x),
839 to_overflow_behavior(overflow_y),
840 )
841}
842
843fn check_scrollbar_change(
852 tree: &LayoutTree,
853 node_index: usize,
854 scrollbar_info: &ScrollbarRequirements,
855 skip_scrollbar_check: bool,
856) -> bool {
857 if skip_scrollbar_check {
858 return false;
859 }
860
861 let Some(current_node) = tree.get(node_index) else {
862 return false;
863 };
864
865 match ¤t_node.scrollbar_info {
866 None => scrollbar_info.needs_reflow(),
867 Some(old_info) => {
868 let adding_horizontal = !old_info.needs_horizontal && scrollbar_info.needs_horizontal;
870 let adding_vertical = !old_info.needs_vertical && scrollbar_info.needs_vertical;
871 adding_horizontal || adding_vertical
872 }
873 }
874}
875
876fn merge_scrollbar_info(
881 tree: &LayoutTree,
882 node_index: usize,
883 new_info: &ScrollbarRequirements,
884) -> ScrollbarRequirements {
885 let Some(current_node) = tree.get(node_index) else {
886 return new_info.clone();
887 };
888
889 match ¤t_node.scrollbar_info {
890 Some(old) => ScrollbarRequirements {
891 needs_horizontal: old.needs_horizontal || new_info.needs_horizontal,
892 needs_vertical: old.needs_vertical || new_info.needs_vertical,
893 scrollbar_width: if old.needs_vertical || new_info.needs_vertical {
894 16.0
895 } else {
896 0.0
897 },
898 scrollbar_height: if old.needs_horizontal || new_info.needs_horizontal {
899 16.0
900 } else {
901 0.0
902 },
903 },
904 None => new_info.clone(),
905 }
906}
907
908fn calculate_content_box_pos(
913 containing_block_pos: LogicalPosition,
914 box_props: &crate::solver3::geometry::BoxProps,
915) -> LogicalPosition {
916 LogicalPosition::new(
917 containing_block_pos.x + box_props.border.left + box_props.padding.left,
918 containing_block_pos.y + box_props.border.top + box_props.padding.top,
919 )
920}
921
922fn log_content_box_calculation<T: ParsedFontTrait>(
924 ctx: &mut LayoutContext<'_, T>,
925 node_index: usize,
926 current_node: &LayoutNode,
927 containing_block_pos: LogicalPosition,
928 self_content_box_pos: LogicalPosition,
929) {
930 let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
931 return;
932 };
933
934 let dom_name = current_node
935 .dom_node_id
936 .and_then(|id| {
937 ctx.styled_dom
938 .node_data
939 .as_container()
940 .internal
941 .get(id.index())
942 })
943 .map(|n| format!("{:?}", n.node_type))
944 .unwrap_or_else(|| "Unknown".to_string());
945
946 debug_msgs.push(LayoutDebugMessage::new(
947 LayoutDebugMessageType::PositionCalculation,
948 format!(
949 "[CONTENT BOX {}] {} - margin-box pos=({:.2}, {:.2}) + border=({:.2},{:.2}) + \
950 padding=({:.2},{:.2}) = content-box pos=({:.2}, {:.2})",
951 node_index,
952 dom_name,
953 containing_block_pos.x,
954 containing_block_pos.y,
955 current_node.box_props.border.left,
956 current_node.box_props.border.top,
957 current_node.box_props.padding.left,
958 current_node.box_props.padding.top,
959 self_content_box_pos.x,
960 self_content_box_pos.y
961 ),
962 ));
963}
964
965fn log_child_positioning<T: ParsedFontTrait>(
967 ctx: &mut LayoutContext<'_, T>,
968 child_index: usize,
969 child_node: &LayoutNode,
970 self_content_box_pos: LogicalPosition,
971 child_relative_pos: LogicalPosition,
972 child_absolute_pos: LogicalPosition,
973) {
974 let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
975 return;
976 };
977
978 let child_dom_name = child_node
979 .dom_node_id
980 .and_then(|id| {
981 ctx.styled_dom
982 .node_data
983 .as_container()
984 .internal
985 .get(id.index())
986 })
987 .map(|n| format!("{:?}", n.node_type))
988 .unwrap_or_else(|| "Unknown".to_string());
989
990 debug_msgs.push(LayoutDebugMessage::new(
991 LayoutDebugMessageType::PositionCalculation,
992 format!(
993 "[CHILD POS {}] {} - parent content-box=({:.2}, {:.2}) + relative=({:.2}, {:.2}) + \
994 margin=({:.2}, {:.2}) = absolute=({:.2}, {:.2})",
995 child_index,
996 child_dom_name,
997 self_content_box_pos.x,
998 self_content_box_pos.y,
999 child_relative_pos.x,
1000 child_relative_pos.y,
1001 child_node.box_props.margin.left,
1002 child_node.box_props.margin.top,
1003 child_absolute_pos.x,
1004 child_absolute_pos.y
1005 ),
1006 ));
1007}
1008
1009fn process_inflow_child<T: ParsedFontTrait>(
1015 ctx: &mut LayoutContext<'_, T>,
1016 tree: &mut LayoutTree,
1017 text_cache: &mut TextLayoutCache,
1018 child_index: usize,
1019 child_relative_pos: LogicalPosition,
1020 self_content_box_pos: LogicalPosition,
1021 inner_size_after_scrollbars: LogicalSize,
1022 writing_mode: LayoutWritingMode,
1023 is_flex_or_grid: bool,
1024 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1025 reflow_needed_for_scrollbars: &mut bool,
1026 float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1027) -> Result<()> {
1028 let child_node = tree.get_mut(child_index).ok_or(LayoutError::InvalidTree)?;
1030 child_node.relative_position = Some(child_relative_pos);
1031
1032 let child_absolute_pos = LogicalPosition::new(
1034 self_content_box_pos.x + child_relative_pos.x,
1035 self_content_box_pos.y + child_relative_pos.y,
1036 );
1037
1038 {
1040 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1041 log_child_positioning(
1042 ctx,
1043 child_index,
1044 child_node,
1045 self_content_box_pos,
1046 child_relative_pos,
1047 child_absolute_pos,
1048 );
1049 }
1050
1051 calculated_positions.insert(child_index, child_absolute_pos);
1052
1053 if is_flex_or_grid {
1055 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1057 let child_content_box_pos =
1058 calculate_content_box_pos(child_absolute_pos, &child_node.box_props);
1059 let child_inner_size = child_node
1060 .box_props
1061 .inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
1062
1063 position_flex_child_descendants(
1064 ctx,
1065 tree,
1066 text_cache,
1067 child_index,
1068 child_content_box_pos,
1069 child_inner_size,
1070 calculated_positions,
1071 reflow_needed_for_scrollbars,
1072 float_cache,
1073 )?;
1074 } else {
1075 calculate_layout_for_subtree(
1077 ctx,
1078 tree,
1079 text_cache,
1080 child_index,
1081 child_absolute_pos,
1082 inner_size_after_scrollbars,
1083 calculated_positions,
1084 reflow_needed_for_scrollbars,
1085 float_cache,
1086 )?;
1087 }
1088
1089 Ok(())
1090}
1091
1092fn process_out_of_flow_children<T: ParsedFontTrait>(
1098 ctx: &mut LayoutContext<'_, T>,
1099 tree: &mut LayoutTree,
1100 text_cache: &mut TextLayoutCache,
1101 node_index: usize,
1102 self_content_box_pos: LogicalPosition,
1103 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1104) -> Result<()> {
1105 let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
1107 let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1108 current_node
1109 .children
1110 .iter()
1111 .filter_map(|&child_index| {
1112 if calculated_positions.contains_key(&child_index) {
1113 return None;
1114 }
1115 let child = tree.get(child_index)?;
1116 Some((child_index, child.dom_node_id))
1117 })
1118 .collect()
1119 };
1120
1121 for (child_index, child_dom_id_opt) in out_of_flow_children {
1122 let Some(child_dom_id) = child_dom_id_opt else {
1123 continue;
1124 };
1125
1126 let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
1127 if position_type != LayoutPosition::Absolute && position_type != LayoutPosition::Fixed {
1128 continue;
1129 }
1130
1131 calculated_positions.insert(child_index, self_content_box_pos);
1133
1134 set_static_positions_recursive(
1136 ctx,
1137 tree,
1138 text_cache,
1139 child_index,
1140 self_content_box_pos,
1141 calculated_positions,
1142 )?;
1143 }
1144
1145 Ok(())
1146}
1147
1148pub fn calculate_layout_for_subtree<T: ParsedFontTrait>(
1151 ctx: &mut LayoutContext<'_, T>,
1152 tree: &mut LayoutTree,
1153 text_cache: &mut TextLayoutCache,
1154 node_index: usize,
1155 containing_block_pos: LogicalPosition,
1156 containing_block_size: LogicalSize,
1157 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1158 reflow_needed_for_scrollbars: &mut bool,
1159 float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1160) -> Result<()> {
1161 let PreparedLayoutContext {
1163 constraints,
1164 dom_id,
1165 writing_mode,
1166 mut final_used_size,
1167 box_props,
1168 } = {
1169 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1170 prepare_layout_context(ctx, node, containing_block_size)?
1171 };
1172
1173 let layout_result =
1175 layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?;
1176 let content_size = layout_result.output.overflow_size;
1177
1178 let styled_node_state = dom_id
1181 .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
1182 .map(|n| n.styled_node_state)
1183 .unwrap_or_default();
1184
1185 let css_height: MultiValue<LayoutHeight> = match dom_id {
1186 Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
1187 None => MultiValue::Auto, };
1189 if should_use_content_height(&css_height) {
1190 final_used_size = apply_content_based_height(
1191 final_used_size,
1192 content_size,
1193 tree,
1194 node_index,
1195 writing_mode,
1196 );
1197 }
1198
1199 let skip_scrollbar_check = ctx.fragmentation_context.is_some();
1202 let scrollbar_info = match dom_id {
1203 Some(id) => compute_scrollbar_info(
1204 ctx,
1205 id,
1206 &styled_node_state,
1207 content_size,
1208 &box_props,
1209 final_used_size,
1210 writing_mode,
1211 ),
1212 None => ScrollbarRequirements::default(),
1213 };
1214
1215 if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
1216 *reflow_needed_for_scrollbars = true;
1217 }
1218
1219 let merged_scrollbar_info = merge_scrollbar_info(tree, node_index, &scrollbar_info);
1220 let content_box_size = box_props.inner_size(final_used_size, writing_mode);
1221 let inner_size_after_scrollbars = merged_scrollbar_info.shrink_size(content_box_size);
1222
1223 let self_content_box_pos = {
1225 let current_node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
1226
1227 let is_table_cell = matches!(
1229 current_node.formatting_context,
1230 FormattingContext::TableCell
1231 );
1232 if !is_table_cell || current_node.used_size.is_none() {
1233 current_node.used_size = Some(final_used_size);
1234 }
1235 current_node.scrollbar_info = Some(merged_scrollbar_info);
1236 current_node.overflow_content_size = Some(content_size);
1238
1239 let pos = calculate_content_box_pos(containing_block_pos, ¤t_node.box_props);
1240 log_content_box_calculation(ctx, node_index, current_node, containing_block_pos, pos);
1241 pos
1242 };
1243
1244 let is_flex_or_grid = {
1246 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1247 matches!(
1248 node.formatting_context,
1249 FormattingContext::Flex | FormattingContext::Grid
1250 )
1251 };
1252
1253 let positions: Vec<_> = layout_result
1255 .output
1256 .positions
1257 .iter()
1258 .map(|(&idx, &pos)| (idx, pos))
1259 .collect();
1260
1261 for (child_index, child_relative_pos) in positions {
1262 process_inflow_child(
1263 ctx,
1264 tree,
1265 text_cache,
1266 child_index,
1267 child_relative_pos,
1268 self_content_box_pos,
1269 inner_size_after_scrollbars,
1270 writing_mode,
1271 is_flex_or_grid,
1272 calculated_positions,
1273 reflow_needed_for_scrollbars,
1274 float_cache,
1275 )?;
1276 }
1277
1278 process_out_of_flow_children(
1280 ctx,
1281 tree,
1282 text_cache,
1283 node_index,
1284 self_content_box_pos,
1285 calculated_positions,
1286 )?;
1287
1288 Ok(())
1289}
1290
1291fn position_flex_child_descendants<T: ParsedFontTrait>(
1299 ctx: &mut LayoutContext<'_, T>,
1300 tree: &mut LayoutTree,
1301 text_cache: &mut TextLayoutCache,
1302 node_index: usize,
1303 content_box_pos: LogicalPosition,
1304 available_size: LogicalSize,
1305 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1306 reflow_needed_for_scrollbars: &mut bool,
1307 float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1308) -> Result<()> {
1309 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1310 let children: Vec<usize> = node.children.clone();
1311 let fc = node.formatting_context.clone();
1312
1313 if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
1316 for &child_index in &children {
1317 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1318 let child_rel_pos = child_node.relative_position.unwrap_or_default();
1319 let child_abs_pos = LogicalPosition::new(
1320 content_box_pos.x + child_rel_pos.x,
1321 content_box_pos.y + child_rel_pos.y,
1322 );
1323
1324 calculated_positions.insert(child_index, child_abs_pos);
1326
1327 let child_content_box = LogicalPosition::new(
1329 child_abs_pos.x
1330 + child_node.box_props.border.left
1331 + child_node.box_props.padding.left,
1332 child_abs_pos.y
1333 + child_node.box_props.border.top
1334 + child_node.box_props.padding.top,
1335 );
1336 let child_inner_size = child_node.box_props.inner_size(
1337 child_node.used_size.unwrap_or_default(),
1338 LayoutWritingMode::HorizontalTb,
1339 );
1340
1341 position_flex_child_descendants(
1343 ctx,
1344 tree,
1345 text_cache,
1346 child_index,
1347 child_content_box,
1348 child_inner_size,
1349 calculated_positions,
1350 reflow_needed_for_scrollbars,
1351 float_cache,
1352 )?;
1353 }
1354 } else {
1355 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1358 let children: Vec<usize> = node.children.clone();
1359
1360 for &child_index in &children {
1361 let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1362 let child_rel_pos = child_node.relative_position.unwrap_or_default();
1363 let child_abs_pos = LogicalPosition::new(
1364 content_box_pos.x + child_rel_pos.x,
1365 content_box_pos.y + child_rel_pos.y,
1366 );
1367
1368 calculated_positions.insert(child_index, child_abs_pos);
1370
1371 let child_content_box = LogicalPosition::new(
1373 child_abs_pos.x
1374 + child_node.box_props.border.left
1375 + child_node.box_props.padding.left,
1376 child_abs_pos.y
1377 + child_node.box_props.border.top
1378 + child_node.box_props.padding.top,
1379 );
1380 let child_inner_size = child_node.box_props.inner_size(
1381 child_node.used_size.unwrap_or_default(),
1382 LayoutWritingMode::HorizontalTb,
1383 );
1384
1385 position_flex_child_descendants(
1387 ctx,
1388 tree,
1389 text_cache,
1390 child_index,
1391 child_content_box,
1392 child_inner_size,
1393 calculated_positions,
1394 reflow_needed_for_scrollbars,
1395 float_cache,
1396 )?;
1397 }
1398 }
1399
1400 Ok(())
1401}
1402
1403fn set_static_positions_recursive<T: ParsedFontTrait>(
1404 ctx: &mut LayoutContext<'_, T>,
1405 tree: &mut LayoutTree,
1406 _text_cache: &mut TextLayoutCache,
1407 node_index: usize,
1408 parent_content_box_pos: LogicalPosition,
1409 calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1410) -> Result<()> {
1411 let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
1412 let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1413 node.children
1414 .iter()
1415 .filter_map(|&child_index| {
1416 if calculated_positions.contains_key(&child_index) {
1417 None
1418 } else {
1419 let child = tree.get(child_index)?;
1420 Some((child_index, child.dom_node_id))
1421 }
1422 })
1423 .collect()
1424 };
1425
1426 for (child_index, child_dom_id_opt) in out_of_flow_children {
1427 if let Some(child_dom_id) = child_dom_id_opt {
1428 let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
1429 if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
1430 calculated_positions.insert(child_index, parent_content_box_pos);
1431
1432 set_static_positions_recursive(
1434 ctx,
1435 tree,
1436 _text_cache,
1437 child_index,
1438 parent_content_box_pos,
1439 calculated_positions,
1440 )?;
1441 }
1442 }
1443 }
1444
1445 Ok(())
1446}
1447
1448fn should_use_content_height(css_height: &MultiValue<LayoutHeight>) -> bool {
1450 match css_height {
1451 MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
1452 true
1454 }
1455 MultiValue::Exact(height) => match height {
1456 LayoutHeight::Auto => {
1457 true
1459 }
1460 LayoutHeight::Px(px) => {
1461 use azul_css::props::basic::{pixel::PixelValue, SizeMetric};
1464 px == &PixelValue::zero()
1465 || (px.metric != SizeMetric::Px
1466 && px.metric != SizeMetric::Percent
1467 && px.metric != SizeMetric::Em
1468 && px.metric != SizeMetric::Rem)
1469 }
1470 LayoutHeight::MinContent | LayoutHeight::MaxContent => {
1471 true
1473 }
1474 },
1475 }
1476}
1477
1478fn apply_content_based_height(
1490 mut used_size: LogicalSize,
1491 content_size: LogicalSize,
1492 tree: &LayoutTree,
1493 node_index: usize,
1494 writing_mode: LayoutWritingMode,
1495) -> LogicalSize {
1496 let node_props = &tree.get(node_index).unwrap().box_props;
1497 let main_axis_padding_border =
1498 node_props.padding.main_sum(writing_mode) + node_props.border.main_sum(writing_mode);
1499
1500 let old_main_size = used_size.main(writing_mode);
1502 let new_main_size = content_size.main(writing_mode) + main_axis_padding_border;
1503
1504 let final_main_size = old_main_size.max(new_main_size);
1507
1508 used_size = used_size.with_main(writing_mode, final_main_size);
1509
1510 used_size
1511}
1512
1513fn hash_styled_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
1514 let mut hasher = DefaultHasher::new();
1515 if let Some(styled_node) = dom.styled_nodes.as_container().get(node_id) {
1516 styled_node.styled_node_state.hash(&mut hasher);
1517 }
1518 if let Some(node_data) = dom.node_data.as_container().get(node_id) {
1519 node_data.get_node_type().hash(&mut hasher);
1520 }
1521 hasher.finish()
1522}
1523
1524fn calculate_subtree_hash(node_self_hash: u64, child_hashes: &[u64]) -> SubtreeHash {
1525 let mut hasher = DefaultHasher::new();
1526 node_self_hash.hash(&mut hasher);
1527 child_hashes.hash(&mut hasher);
1528 SubtreeHash(hasher.finish())
1529}
1530
1531pub fn compute_counters(
1541 styled_dom: &StyledDom,
1542 tree: &LayoutTree,
1543 counters: &mut BTreeMap<(usize, String), i32>,
1544) {
1545 use std::collections::HashMap;
1546
1547 let mut counter_stacks: HashMap<String, Vec<i32>> = HashMap::new();
1550
1551 let mut scope_stack: Vec<Vec<String>> = Vec::new();
1554
1555 compute_counters_recursive(
1556 styled_dom,
1557 tree,
1558 tree.root,
1559 counters,
1560 &mut counter_stacks,
1561 &mut scope_stack,
1562 );
1563}
1564
1565fn compute_counters_recursive(
1566 styled_dom: &StyledDom,
1567 tree: &LayoutTree,
1568 node_idx: usize,
1569 counters: &mut BTreeMap<(usize, String), i32>,
1570 counter_stacks: &mut std::collections::HashMap<String, Vec<i32>>,
1571 scope_stack: &mut Vec<Vec<String>>,
1572) {
1573 let node = match tree.get(node_idx) {
1574 Some(n) => n,
1575 None => return,
1576 };
1577
1578 if node.pseudo_element.is_some() {
1582 if let Some(parent_idx) = node.parent {
1585 let parent_counters: Vec<_> = counters
1587 .iter()
1588 .filter(|((idx, _), _)| *idx == parent_idx)
1589 .map(|((_, name), &value)| (name.clone(), value))
1590 .collect();
1591
1592 for (counter_name, value) in parent_counters {
1593 counters.insert((node_idx, counter_name), value);
1594 }
1595 }
1596
1597 return;
1600 }
1601
1602 let dom_id = match node.dom_node_id {
1604 Some(id) => id,
1605 None => {
1606 for &child_idx in &node.children {
1608 compute_counters_recursive(
1609 styled_dom,
1610 tree,
1611 child_idx,
1612 counters,
1613 counter_stacks,
1614 scope_stack,
1615 );
1616 }
1617 return;
1618 }
1619 };
1620
1621 let node_data = &styled_dom.node_data.as_container()[dom_id];
1622 let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1623 let cache = &styled_dom.css_property_cache.ptr;
1624
1625 let mut reset_counters_at_this_level = Vec::new();
1627
1628 let display = cache
1631 .get_display(node_data, &dom_id, node_state)
1632 .and_then(|d| d.get_property().copied());
1633 let is_list_item = matches!(display, Some(LayoutDisplay::ListItem));
1634
1635 let counter_reset = cache
1637 .get_counter_reset(node_data, &dom_id, node_state)
1638 .and_then(|v| v.get_property());
1639
1640 if let Some(counter_reset) = counter_reset {
1641 let counter_name_str = counter_reset.counter_name.as_str();
1642 if counter_name_str != "none" {
1643 let counter_name = counter_name_str.to_string();
1644 let reset_value = counter_reset.value;
1645
1646 counter_stacks
1648 .entry(counter_name.clone())
1649 .or_default()
1650 .push(reset_value);
1651 reset_counters_at_this_level.push(counter_name);
1652 }
1653 }
1654
1655 let counter_inc = cache
1657 .get_counter_increment(node_data, &dom_id, node_state)
1658 .and_then(|v| v.get_property());
1659
1660 if let Some(counter_inc) = counter_inc {
1661 let counter_name_str = counter_inc.counter_name.as_str();
1662 if counter_name_str != "none" {
1663 let counter_name = counter_name_str.to_string();
1664 let inc_value = counter_inc.value;
1665
1666 let stack = counter_stacks.entry(counter_name.clone()).or_default();
1668 if stack.is_empty() {
1669 stack.push(inc_value);
1671 } else if let Some(current) = stack.last_mut() {
1672 *current += inc_value;
1673 }
1674 }
1675 }
1676
1677 if is_list_item {
1679 let counter_name = "list-item".to_string();
1680 let stack = counter_stacks.entry(counter_name.clone()).or_default();
1681 if stack.is_empty() {
1682 stack.push(1);
1684 } else {
1685 if let Some(current) = stack.last_mut() {
1686 *current += 1;
1687 }
1688 }
1689 }
1690
1691 for (counter_name, stack) in counter_stacks.iter() {
1693 if let Some(&value) = stack.last() {
1694 counters.insert((node_idx, counter_name.clone()), value);
1695 }
1696 }
1697
1698 scope_stack.push(reset_counters_at_this_level.clone());
1700
1701 for &child_idx in &node.children {
1703 compute_counters_recursive(
1704 styled_dom,
1705 tree,
1706 child_idx,
1707 counters,
1708 counter_stacks,
1709 scope_stack,
1710 );
1711 }
1712
1713 if let Some(reset_counters) = scope_stack.pop() {
1715 for counter_name in reset_counters {
1716 if let Some(stack) = counter_stacks.get_mut(&counter_name) {
1717 stack.pop();
1718 }
1719 }
1720 }
1721}