1use crate::{
4 hash::{hash_subtree, hash_view_content},
5 node::{LayoutCache, LayoutConstraints, NodeId, TreeNode, TreeStats},
6 reconcile::ReconcileContext,
7};
8use repose_core::{Modifier, Rect, SubcomposeScope, View, ViewId, ViewKind};
9use rustc_hash::{FxHashMap, FxHashSet};
10use slotmap::SlotMap;
11use smallvec::SmallVec;
12use std::sync::Arc;
13
14pub struct ViewTree {
16 nodes: SlotMap<NodeId, TreeNode>,
18
19 root: Option<NodeId>,
21
22 dirty: FxHashSet<NodeId>,
24
25 paint_dirty: FxHashSet<NodeId>,
27
28 generation: u64,
30
31 view_id_map: FxHashMap<ViewId, NodeId>,
33
34 pub stats: TreeStats,
36
37 pub removed_ids: Vec<NodeId>,
39
40 subcompose_scope: SubcomposeScope,
44
45 subcompose_cache: FxHashMap<NodeId, (SubcomposeScope, Vec<(u64, View)>)>,
50}
51
52impl Default for ViewTree {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl ViewTree {
59 pub fn new() -> Self {
61 Self {
62 nodes: SlotMap::with_key(),
63 root: None,
64 dirty: FxHashSet::default(),
65 paint_dirty: FxHashSet::default(),
66 generation: 0,
67 view_id_map: FxHashMap::default(),
68 stats: TreeStats::default(),
69 removed_ids: Vec::new(),
70 subcompose_scope: SubcomposeScope::UNBOUNDED,
71 subcompose_cache: FxHashMap::default(),
72 }
73 }
74
75 pub fn set_subcompose_scope(&mut self, scope: SubcomposeScope) {
81 self.subcompose_scope = scope;
82 }
83
84 pub fn subcompose_scope(&self) -> SubcomposeScope {
86 self.subcompose_scope
87 }
88
89 fn run_subcompose(
99 &mut self,
100 node_id: NodeId,
101 content: &Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
102 ) -> Vec<(u64, View)> {
103 let scope = self.compute_scope_for_node(node_id);
104 if let Some((cached_scope, cached_slots)) = self.subcompose_cache.get(&node_id)
105 && *cached_scope == scope
106 {
107 return cached_slots.clone();
108 }
109 let mut slots = content(&scope);
110 let scope_key = format!("subcompose_{:?}", node_id);
112 for (slot_id, view) in slots.iter_mut() {
113 view.modifier.key = Some(*slot_id);
114 view.scope_key = Some(scope_key.clone());
115 view.modifier.repaint_boundary = true;
116 }
117 self.subcompose_cache
118 .insert(node_id, (scope, slots.clone()));
119 slots
120 }
121
122 fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
130 let mut scope = self.subcompose_scope;
131 let mut chain: Vec<NodeId> = Vec::new();
132 let mut current = Some(node_id);
133 while let Some(id) = current {
134 chain.push(id);
135 match self.nodes.get(id) {
136 Some(node) => current = node.parent,
137 None => break,
138 }
139 }
140 chain.reverse();
141 for ancestor_id in chain {
142 if let Some(node) = self.nodes.get(ancestor_id) {
143 scope = intersect_scope_with_modifier(scope, &node.modifier);
144 if let Some(cache) = &node.layout_cache {
147 let w = cache.rect.w;
148 if w > 0.0 && w.is_finite() {
149 scope.max_width = scope.max_width.min(w);
150 }
151 }
152 }
153 }
154 scope
155 }
156
157 pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
161 self.subcompose_cache.remove(&node_id);
162 }
163
164 fn drop_subcompose_cache_for(&mut self, ids: &[NodeId]) {
167 for id in ids {
168 self.subcompose_cache.remove(id);
169 }
170 }
171
172 fn collect_subcompose_cache(&mut self, node_id: &NodeId) {
175 self.subcompose_cache.remove(node_id);
176 let children: Vec<NodeId> = self
177 .nodes
178 .get(*node_id)
179 .map(|n| n.children.iter().copied().collect())
180 .unwrap_or_default();
181 for child in children {
182 self.collect_subcompose_cache(&child);
183 }
184 }
185
186 pub fn generation(&self) -> u64 {
188 self.generation
189 }
190
191 pub fn root(&self) -> Option<NodeId> {
193 self.root
194 }
195
196 pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
198 self.nodes.get(id)
199 }
200
201 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
203 self.nodes.get_mut(id)
204 }
205
206 pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
208 self.view_id_map
209 .get(&view_id)
210 .and_then(|id| self.nodes.get(*id))
211 }
212
213 pub fn len(&self) -> usize {
215 self.nodes.len()
216 }
217
218 pub fn is_empty(&self) -> bool {
220 self.nodes.is_empty()
221 }
222
223 pub fn is_dirty(&self, id: NodeId) -> bool {
225 self.dirty.contains(&id)
226 }
227
228 pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
230 &self.dirty
231 }
232
233 pub fn clear_dirty(&mut self) {
235 self.dirty.clear();
236 }
237
238 pub fn mark_dirty(&mut self, id: NodeId) {
240 self.dirty.insert(id);
241
242 let mut current = id;
244 while let Some(node) = self.nodes.get(current) {
245 if let Some(parent) = node.parent {
246 self.dirty.insert(parent);
247 current = parent;
248 } else {
249 break;
250 }
251 }
252 }
253
254 pub fn update(&mut self, new_root: &View) -> NodeId {
257 self.removed_ids.clear(); self.generation += 1;
260 self.stats = TreeStats::default();
261
262 let mut ctx = ReconcileContext::new(self.generation);
263
264 let root_id = if let Some(existing_root) = self.root {
265 self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
266 } else {
267 self.create_node(new_root, None, 0, 0, &mut ctx)
268 };
269
270 self.root = Some(root_id);
271
272 self.collect_garbage();
274
275 self.stats.total_nodes = self.nodes.len();
277 self.stats.dirty_nodes = self.dirty.len();
278 self.stats.reconciled_nodes = ctx.reconciled;
279 self.stats.skipped_nodes = ctx.skipped;
280 self.stats.created_nodes = ctx.created;
281 self.stats.removed_nodes = ctx.removed;
282
283 root_id
284 }
285
286 fn reconcile_node(
288 &mut self,
289 node_id: NodeId,
290 view: &View,
291 parent: Option<NodeId>,
292 depth: u32,
293 index_in_parent: u32,
294 ctx: &mut ReconcileContext,
295 ) -> NodeId {
296 let content_hash = hash_view_content(view);
297
298 let old_hash = self
299 .nodes
300 .get(node_id)
301 .expect("reconcile_node: node not found")
302 .content_hash;
303 let content_changed = old_hash != content_hash;
304
305 if content_changed {
306 self.invalidate_subcompose_cache(node_id);
307 }
308
309 let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
310 let subcomposed = self.run_subcompose(node_id, content);
311 let slot_views: Vec<View> = subcomposed.into_iter().map(|(_, v)| v).collect();
312 self.reconcile_children(node_id, &slot_views, depth, ctx)
313 } else {
314 self.reconcile_children(node_id, &view.children, depth, ctx)
315 };
316
317 let new_subtree_hash = hash_subtree(content_hash, &new_children_hashes);
318
319 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
320
321 let subtree_changed;
322 {
323 let node = self
324 .nodes
325 .get_mut(node_id)
326 .expect("reconcile_node: node not found");
327
328 node.parent = parent;
330 node.depth = depth;
331 node.generation = self.generation;
332
333 node.kind = view.kind.clone();
335 node.modifier = view.modifier.clone();
336 node.content_hash = content_hash;
337 node.user_key = view.modifier.key;
338 node.scope_key = view.scope_key.clone();
339
340 if content_changed {
341 node.invalidate_layout();
342 ctx.reconciled += 1;
343 }
344
345 subtree_changed = node.subtree_hash != new_subtree_hash;
347 if subtree_changed {
348 node.subtree_hash = new_subtree_hash;
349 } else if !content_changed {
350 ctx.skipped += 1;
351 }
352
353 node.view_id = view_id;
355 } if subtree_changed {
358 self.mark_dirty(node_id);
359 }
360 self.view_id_map.insert(view_id, node_id);
361
362 node_id
363 }
364 fn reconcile_children(
367 &mut self,
368 parent_id: NodeId,
369 new_children: &[View],
370 parent_depth: u32,
371 ctx: &mut ReconcileContext,
372 ) -> Vec<u64> {
373 let child_depth = parent_depth + 1;
374
375 let old_children: SmallVec<[NodeId; 4]> = self
377 .nodes
378 .get(parent_id)
379 .map(|n| n.children.clone())
380 .unwrap_or_default();
381
382 let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
384 let mut unkeyed_children: Vec<NodeId> = Vec::new();
385
386 for &child_id in &old_children {
387 if let Some(node) = self.nodes.get(child_id) {
388 if let Some(key) = node.user_key {
389 keyed_children.insert(key, child_id);
390 } else {
391 unkeyed_children.push(child_id);
392 }
393 }
394 }
395
396 let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
397 let mut new_subtree_hashes: Vec<u64> = Vec::with_capacity(new_children.len());
398 let mut unkeyed_index = 0;
399 let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
400 let mut new_seen_keys: FxHashSet<u64> = FxHashSet::default();
401
402 for (i, new_child) in new_children.iter().enumerate() {
403 if let Some(key) = new_child.modifier.key {
404 if !new_seen_keys.insert(key) {
405 panic!(
406 "reconcile_children: duplicate modifier.key={} in children of node {:?}.\n\
407 Two sibling views share the same key. Each view passed to a layout \
408 must have a unique modifier.key. For lazy layouts (LazyColumn, LazyRow, \
409 etc.), ensure `get_key` returns a unique key for each item by hashing \
410 the full item identity.",
411 key, parent_id,
412 );
413 }
414 }
415 let idx = i as u32;
416 let child_id = if let Some(key) = new_child.modifier.key {
417 if let Some(&existing_id) = keyed_children.get(&key) {
419 used_nodes.insert(existing_id);
420 self.reconcile_node(
421 existing_id,
422 new_child,
423 Some(parent_id),
424 child_depth,
425 idx,
426 ctx,
427 )
428 } else {
429 self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
430 }
431 } else {
432 if unkeyed_index < unkeyed_children.len() {
434 let existing_id = unkeyed_children[unkeyed_index];
435 unkeyed_index += 1;
436 used_nodes.insert(existing_id);
437 self.reconcile_node(
438 existing_id,
439 new_child,
440 Some(parent_id),
441 child_depth,
442 idx,
443 ctx,
444 )
445 } else {
446 self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
447 }
448 };
449
450 new_child_ids.push(child_id);
451
452 if let Some(node) = self.nodes.get(child_id) {
453 new_subtree_hashes.push(node.subtree_hash);
454 }
455 }
456
457 for &old_child in &old_children {
459 if !used_nodes.contains(&old_child) {
460 self.mark_for_removal(old_child, ctx);
461 }
462 }
463
464 if let Some(parent) = self.nodes.get_mut(parent_id) {
466 parent.children = new_child_ids;
467 }
468
469 new_subtree_hashes
470 }
471
472 fn create_node(
474 &mut self,
475 view: &View,
476 parent: Option<NodeId>,
477 depth: u32,
478 index_in_parent: u32,
479 ctx: &mut ReconcileContext,
480 ) -> NodeId {
481 let content_hash = hash_view_content(view);
482
483 let node_id = self.nodes.insert_with_key(|id| {
485 TreeNode::new(
486 id,
487 0,
488 view.kind.clone(),
489 view.modifier.clone(),
490 self.generation,
491 )
492 });
493 ctx.created += 1;
494
495 {
496 let node = self
497 .nodes
498 .get_mut(node_id)
499 .expect("create_node: node just inserted");
500 node.parent = parent;
501 node.depth = depth;
502 node.content_hash = content_hash;
503 node.user_key = view.modifier.key;
504 node.scope_key = view.scope_key.clone();
505 }
506
507 let child_depth = depth + 1;
509 let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
510 let mut child_hashes: Vec<u64> = Vec::with_capacity(view.children.len());
511 let children_to_create: Vec<View> =
512 if let ViewKind::SubcomposeLayout { content } = &view.kind {
513 self.run_subcompose(node_id, content)
514 .into_iter()
515 .map(|(_, v)| v)
516 .collect()
517 } else {
518 view.children.clone()
519 };
520 for (i, child_view) in children_to_create.iter().enumerate() {
521 let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
522 child_ids.push(child_id);
523 child_hashes.push(
524 self.nodes
525 .get(child_id)
526 .expect("create_node: child just created")
527 .subtree_hash,
528 );
529 }
530
531 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
533 let subtree_hash = hash_subtree(content_hash, &child_hashes);
534
535 let node = self
536 .nodes
537 .get_mut(node_id)
538 .expect("create_node: node just inserted");
539 node.children = child_ids;
540 node.subtree_hash = subtree_hash;
541 node.view_id = view_id;
542
543 self.view_id_map.insert(view_id, node_id);
544 self.dirty.insert(node_id);
545
546 node_id
547 }
548 fn compute_view_id(
550 &self,
551 view: &View,
552 _node_id: NodeId,
553 parent: Option<NodeId>,
554 index_in_parent: u32,
555 ) -> ViewId {
556 if view.id != 0 {
558 return view.id;
559 }
560
561 let parent_id = parent
563 .and_then(|p| self.nodes.get(p))
564 .map(|n| n.view_id)
565 .unwrap_or(0);
566
567 let salt = view.modifier.key.unwrap_or(index_in_parent as u64);
568
569 let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
571 id = id.wrapping_mul(0x9E3779B97F4A7C15);
572 id ^= id >> 30;
573
574 if id == 0 {
575 id = 1;
576 }
577
578 id
579 }
580
581 fn mark_for_removal(&mut self, node_id: NodeId, ctx: &mut ReconcileContext) {
583 let (view_id, children) = {
586 let node = self.nodes.get(node_id);
587 match node {
588 Some(n) => (n.view_id, n.children.clone()),
589 None => return,
590 }
591 };
592 self.view_id_map.remove(&view_id);
593 self.subcompose_cache.remove(&node_id);
594 for child_id in children.iter() {
595 self.collect_subcompose_cache(child_id);
596 }
597 for child_id in children {
598 self.mark_for_removal(child_id, ctx);
599 }
600 ctx.removed += 1;
601
602 if let Some(node) = self.nodes.get_mut(node_id) {
604 node.generation = 0; }
606 }
607
608 fn collect_garbage(&mut self) {
610 let current_gen = self.generation;
611
612 let to_remove: Vec<NodeId> = self
614 .nodes
615 .iter()
616 .filter(|(_, node)| node.generation != current_gen)
617 .map(|(id, _)| id)
618 .collect();
619
620 for id in to_remove {
622 if let Some(node) = self.nodes.remove(id) {
623 self.view_id_map.remove(&node.view_id);
624 self.dirty.remove(&id);
625
626 self.removed_ids.push(id);
628 }
629 }
630 }
631
632 pub fn set_layout(
634 &mut self,
635 id: NodeId,
636 rect: Rect,
637 screen_rect: Rect,
638 constraints: LayoutConstraints,
639 ) {
640 if let Some(node) = self.nodes.get_mut(id) {
641 node.layout_cache = Some(LayoutCache {
642 rect,
643 screen_rect,
644 constraints,
645 generation: self.generation,
646 });
647 }
648 }
649
650 pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
652 self.nodes.values()
653 }
654
655 pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
657 self.nodes.iter()
658 }
659
660 pub fn walk<F>(&self, mut f: F)
663 where
664 F: FnMut(&TreeNode, u32) -> bool,
665 {
666 if let Some(root_id) = self.root {
667 self.walk_node(root_id, 0, &mut f);
668 }
669 }
670
671 fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
672 where
673 F: FnMut(&TreeNode, u32) -> bool,
674 {
675 if let Some(node) = self.nodes.get(id) {
676 if !f(node, depth) {
677 return;
678 }
679
680 for &child_id in &node.children {
681 self.walk_node(child_id, depth + 1, f);
682 }
683 }
684 }
685
686 pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
688 self.nodes.get(id).map(|n| n.children.as_slice())
689 }
690}
691
692fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
700 let mut s = scope;
701 if let Some(sz) = modifier.size {
703 s.min_width = s.min_width.max(sz.width);
704 s.max_width = s.max_width.min(sz.width);
705 s.min_height = s.min_height.max(sz.height);
706 s.max_height = s.max_height.min(sz.height);
707 }
708 if let Some(w) = modifier.width {
709 s.min_width = s.min_width.max(w);
710 s.max_width = s.max_width.min(w);
711 }
712 if let Some(h) = modifier.height {
713 s.min_height = s.min_height.max(h);
714 s.max_height = s.max_height.min(h);
715 }
716 if let Some(mw) = modifier.min_width {
717 s.min_width = s.min_width.max(mw);
718 }
719 if let Some(mh) = modifier.min_height {
720 s.min_height = s.min_height.max(mh);
721 }
722 if let Some(mw) = modifier.max_width {
723 s.max_width = s.max_width.min(mw);
724 }
725 if let Some(mh) = modifier.max_height {
726 s.max_height = s.max_height.min(mh);
727 }
728 if let Some(p) = modifier.padding {
731 let total = p * 2.0;
732 s.min_width = (s.min_width - total).max(0.0);
733 s.max_width = (s.max_width - total).max(0.0);
734 s.min_height = (s.min_height - total).max(0.0);
735 s.max_height = (s.max_height - total).max(0.0);
736 }
737 if let Some(pv) = modifier.padding_values {
738 let h_total = pv.left + pv.right;
739 let v_total = pv.top + pv.bottom;
740 s.min_width = (s.min_width - h_total).max(0.0);
741 s.max_width = (s.max_width - h_total).max(0.0);
742 s.min_height = (s.min_height - v_total).max(0.0);
743 s.max_height = (s.max_height - v_total).max(0.0);
744 }
745 s
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751 use repose_core::{
752 Color, FontStyle, FontWeight, Modifier, SubcomposeScope, TextAlign, TextDecoration, View,
753 ViewKind,
754 };
755 use std::sync::Arc;
756
757 fn text_view(text: &str) -> View {
758 View::new(
759 0,
760 ViewKind::Text {
761 text: text.to_string(),
762 color: Color::WHITE,
763 font_size: 16.0,
764 soft_wrap: true,
765 max_lines: None,
766 overflow: repose_core::TextOverflow::Visible,
767 font_family: None,
768 annotations: None,
769 text_align: TextAlign::Unspecified,
770 font_weight: FontWeight::NORMAL,
771 font_style: FontStyle::Normal,
772 text_decoration: TextDecoration::default(),
773 letter_spacing: 0.0,
774 line_height: 0.0,
775 },
776 )
777 }
778
779 fn box_view() -> View {
780 View::new(0, ViewKind::Box)
781 }
782
783 #[test]
784 fn test_create_tree() {
785 let mut tree = ViewTree::new();
786
787 let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
788
789 tree.update(&root);
790
791 assert_eq!(tree.len(), 3); assert!(tree.root().is_some());
793 }
794
795 #[test]
796 fn test_unchanged_tree_skips() {
797 let mut tree = ViewTree::new();
798
799 let root = box_view().with_children(vec![text_view("Hello")]);
800
801 tree.update(&root);
802 let gen1 = tree.generation();
803
804 tree.update(&root);
806 let gen2 = tree.generation();
807
808 assert_eq!(gen2, gen1 + 1);
809 assert!(tree.stats.skipped_nodes > 0);
810 }
811
812 #[test]
813 fn test_changed_content_reconciles() {
814 let mut tree = ViewTree::new();
815
816 let root1 = box_view().with_children(vec![text_view("Hello")]);
817
818 tree.update(&root1);
819
820 let root2 = box_view().with_children(vec![text_view("Changed")]);
821
822 tree.update(&root2);
823
824 assert!(tree.stats.reconciled_nodes > 0);
825 }
826
827 #[test]
828 fn test_keyed_children_stable() {
829 let mut tree = ViewTree::new();
830
831 let root1 = box_view().with_children(vec![
833 text_view("A").modifier(Modifier::new().key(1)),
834 text_view("B").modifier(Modifier::new().key(2)),
835 text_view("C").modifier(Modifier::new().key(3)),
836 ]);
837
838 tree.update(&root1);
839
840 let b_view_id = tree
842 .root()
843 .and_then(|r| tree.children(r))
844 .and_then(|c| c.get(1).copied())
845 .and_then(|id| tree.get(id))
846 .map(|n| n.view_id);
847
848 let root2 = box_view().with_children(vec![
850 text_view("C").modifier(Modifier::new().key(3)),
851 text_view("A").modifier(Modifier::new().key(1)),
852 text_view("B").modifier(Modifier::new().key(2)),
853 ]);
854
855 tree.update(&root2);
856
857 assert_eq!(tree.len(), 4); }
861
862 fn subcompose_view<F>(f: F) -> View
863 where
864 F: Fn(&SubcomposeScope) -> View + 'static,
865 {
866 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
867 Arc::new(move |scope| vec![(0, f(scope))]);
868 View {
869 id: 0,
870 kind: ViewKind::SubcomposeLayout { content },
871 modifier: Modifier::default(),
872 children: Vec::new(),
873 scope_key: None,
874 semantics: None,
875 }
876 }
877
878 #[test]
879 fn test_subcompose_invokes_content() {
880 let mut tree = ViewTree::new();
881 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
882 let counter2 = counter.clone();
883
884 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
885 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
886 text_view("from subcompose")
887 })]);
888
889 tree.update(&root);
890
891 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
892 assert_eq!(tree.len(), 3); }
894
895 #[test]
896 fn test_subcompose_receives_scope() {
897 let mut tree = ViewTree::new();
898 let captured = Arc::new(std::sync::Mutex::new(None));
899 let captured2 = captured.clone();
900
901 let root = box_view().with_children(vec![subcompose_view(move |scope| {
902 *captured2.lock().unwrap() = Some(*scope);
903 text_view("hi")
904 })]);
905
906 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 360.0, 0.0, 640.0));
907 tree.update(&root);
908
909 let observed = captured.lock().unwrap().expect("scope captured");
910 assert_eq!(observed.max_width, 360.0);
911 assert_eq!(observed.max_height, 640.0);
912 assert_eq!(observed.min_width, 0.0);
913 assert_eq!(observed.min_height, 0.0);
914 }
915
916 #[test]
917 fn test_subcompose_re_invokes_on_update() {
918 let mut tree = ViewTree::new();
919 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
920 let counter2 = counter.clone();
921
922 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
923 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
924 text_view("hi")
925 })]);
926
927 tree.update(&root);
928 tree.update(&root);
929 tree.update(&root);
930
931 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
934 }
935
936 #[test]
937 fn test_subcompose_reruns_on_scope_change() {
938 let mut tree = ViewTree::new();
939 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
940 let counter2 = counter.clone();
941
942 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
943 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
944 text_view("hi")
945 })]);
946
947 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
948 tree.update(&root);
949 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
950
951 tree.update(&root);
953 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
954
955 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 200.0, 0.0, 200.0));
957 tree.update(&root);
958 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
959 }
960
961 #[test]
962 fn test_subcompose_reruns_on_content_change() {
963 let mut tree = ViewTree::new();
964 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
965 let c1 = counter.clone();
966
967 let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
968 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
969 text_view("hi")
970 })]);
971
972 tree.update(&root1);
973 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
974
975 tree.update(&root1);
977 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
978
979 let c2 = counter.clone();
981 let root2 = box_view().with_children(vec![
982 subcompose_view(move |_scope| {
983 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
984 text_view("hi")
985 })
986 .modifier(Modifier::new().padding(4.0)),
987 ]);
988
989 tree.update(&root2);
990 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
991 }
992
993 #[test]
994 fn test_subcompose_cache_drops_on_node_removal() {
995 let mut tree = ViewTree::new();
996 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
997
998 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1000 let c1 = counter.clone();
1001 let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
1002 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1003 text_view("hi")
1004 })]);
1005
1006 tree.update(&root_with_sub);
1007 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1008
1009 let root_no_sub = box_view().with_children(vec![text_view("plain")]);
1012 tree.update(&root_no_sub);
1013 assert_eq!(tree.len(), 2);
1014
1015 let c2 = counter.clone();
1018 let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
1019 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1020 text_view("hi")
1021 })]);
1022
1023 tree.update(&root_with_sub_again);
1024 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1025 }
1026
1027 fn multi_slot_view<F>(f: F) -> View
1028 where
1029 F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
1030 {
1031 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
1032 View {
1033 id: 0,
1034 kind: ViewKind::SubcomposeLayout { content },
1035 modifier: Modifier::default(),
1036 children: Vec::new(),
1037 scope_key: None,
1038 semantics: None,
1039 }
1040 }
1041
1042 #[test]
1043 fn test_subcompose_multi_slot_produces_multiple_children() {
1044 let mut tree = ViewTree::new();
1045 let root = box_view().with_children(vec![multi_slot_view(|_scope| {
1046 vec![
1047 (0, text_view("a")),
1048 (1, text_view("b")),
1049 (2, text_view("c")),
1050 ]
1051 })]);
1052
1053 tree.update(&root);
1054
1055 assert_eq!(tree.len(), 5);
1057 let sub_id = tree
1058 .root()
1059 .and_then(|r| tree.children(r))
1060 .and_then(|c| c.first().copied())
1061 .expect("subcompose node");
1062 let sub_children = tree.children(sub_id).expect("subcompose has children");
1063 assert_eq!(sub_children.len(), 3);
1064 }
1065
1066 #[test]
1067 fn test_subcompose_multi_slot_preserves_identity_across_removal() {
1068 let mut tree = ViewTree::new();
1069
1070 let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
1072 vec![
1073 (0, text_view("a")),
1074 (1, text_view("b")),
1075 (2, text_view("c")),
1076 ]
1077 })]);
1078 tree.update(&root3);
1079
1080 let sub_id = tree
1081 .root()
1082 .and_then(|r| tree.children(r))
1083 .and_then(|c| c.first().copied())
1084 .expect("subcompose node");
1085 let before = tree.children(sub_id).expect("children").to_vec();
1086 let a_node = before[0];
1087 let b_node = before[1];
1088 let c_node = before[2];
1089
1090 let root2 = box_view().with_children(vec![
1093 multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
1094 .modifier(Modifier::new().padding(4.0)),
1095 ]);
1096 tree.update(&root2);
1097
1098 let after = tree.children(sub_id).expect("children after");
1099 assert_eq!(after.len(), 2);
1100 assert_eq!(after[0], a_node);
1102 assert_eq!(after[1], c_node);
1103 assert!(tree.get(b_node).is_none());
1105 }
1106
1107 #[test]
1108 fn test_subcompose_ancestor_modifier_narrows_scope() {
1109 let mut tree = ViewTree::new();
1110 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1111
1112 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1113 let cap2 = captured.clone();
1114
1115 let sub = multi_slot_view(move |scope| {
1118 *cap2.lock().unwrap() = *scope;
1119 vec![(0, text_view("hi"))]
1120 });
1121 let root = box_view()
1122 .modifier(Modifier::new().width(200.0))
1123 .with_children(vec![sub]);
1124
1125 tree.update(&root);
1126
1127 let observed = *captured.lock().unwrap();
1128 assert_eq!(observed.max_width, 200.0);
1129 }
1130
1131 #[test]
1132 fn test_subcompose_chained_ancestor_constraints_intersect() {
1133 let mut tree = ViewTree::new();
1134 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1135
1136 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1137 let cap2 = captured.clone();
1138
1139 let sub = multi_slot_view(move |scope| {
1140 *cap2.lock().unwrap() = *scope;
1141 vec![(0, text_view("hi"))]
1142 });
1143 let root = box_view()
1146 .modifier(Modifier::new().width(400.0))
1147 .with_children(vec![
1148 box_view()
1149 .modifier(Modifier::new().max_width(300.0))
1150 .with_children(vec![sub]),
1151 ]);
1152
1153 tree.update(&root);
1154
1155 let observed = *captured.lock().unwrap();
1156 assert_eq!(observed.max_width, 300.0);
1157 }
1158
1159 #[test]
1160 fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
1161 let mut tree = ViewTree::new();
1162 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1163
1164 let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1165 let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1166 let outer2 = outer_captured.clone();
1167 let inner2 = inner_captured.clone();
1168
1169 let inner = Arc::new(multi_slot_view(move |scope| {
1172 *inner2.lock().unwrap() = *scope;
1173 vec![(0, text_view("inner"))]
1174 }));
1175 let inner_clone = inner.clone();
1176 let outer = multi_slot_view(move |scope| {
1177 *outer2.lock().unwrap() = *scope;
1178 vec![(0, (*inner_clone).clone())]
1179 })
1180 .modifier(Modifier::new().width(400.0));
1181 let root = box_view().with_children(vec![outer]);
1182
1183 tree.update(&root);
1184
1185 let outer_obs = *outer_captured.lock().unwrap();
1186 let inner_obs = *inner_captured.lock().unwrap();
1187 assert_eq!(outer_obs.max_width, 400.0);
1188 assert_eq!(inner_obs.max_width, 400.0);
1189 }
1190}