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 return cached_slots.clone();
107 }
108 let mut slots = content(&scope);
109 for (slot_id, view) in slots.iter_mut() {
110 view.modifier.key = Some(*slot_id);
111 }
112 self.subcompose_cache
113 .insert(node_id, (scope, slots.clone()));
114 slots
115 }
116
117 fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
122 let mut scope = self.subcompose_scope;
123 let mut chain: Vec<NodeId> = Vec::new();
124 let mut current = Some(node_id);
125 while let Some(id) = current {
126 chain.push(id);
127 match self.nodes.get(id) {
128 Some(node) => current = node.parent,
129 None => break,
130 }
131 }
132 chain.reverse();
133 for ancestor_id in chain {
134 if let Some(node) = self.nodes.get(ancestor_id) {
135 scope = intersect_scope_with_modifier(scope, &node.modifier);
136 }
137 }
138 scope
139 }
140
141 pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
145 self.subcompose_cache.remove(&node_id);
146 }
147
148 fn drop_subcompose_cache_for(&mut self, ids: &[NodeId]) {
151 for id in ids {
152 self.subcompose_cache.remove(id);
153 }
154 }
155
156 fn collect_subcompose_cache(&mut self, node_id: &NodeId) {
159 self.subcompose_cache.remove(node_id);
160 let children: Vec<NodeId> = self
161 .nodes
162 .get(*node_id)
163 .map(|n| n.children.iter().copied().collect())
164 .unwrap_or_default();
165 for child in children {
166 self.collect_subcompose_cache(&child);
167 }
168 }
169
170 pub fn generation(&self) -> u64 {
172 self.generation
173 }
174
175 pub fn root(&self) -> Option<NodeId> {
177 self.root
178 }
179
180 pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
182 self.nodes.get(id)
183 }
184
185 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
187 self.nodes.get_mut(id)
188 }
189
190 pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
192 self.view_id_map
193 .get(&view_id)
194 .and_then(|id| self.nodes.get(*id))
195 }
196
197 pub fn len(&self) -> usize {
199 self.nodes.len()
200 }
201
202 pub fn is_empty(&self) -> bool {
204 self.nodes.is_empty()
205 }
206
207 pub fn is_dirty(&self, id: NodeId) -> bool {
209 self.dirty.contains(&id)
210 }
211
212 pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
214 &self.dirty
215 }
216
217 pub fn clear_dirty(&mut self) {
219 self.dirty.clear();
220 }
221
222 pub fn mark_dirty(&mut self, id: NodeId) {
224 self.dirty.insert(id);
225
226 let mut current = id;
228 while let Some(node) = self.nodes.get(current) {
229 if let Some(parent) = node.parent {
230 self.dirty.insert(parent);
231 current = parent;
232 } else {
233 break;
234 }
235 }
236 }
237
238 pub fn update(&mut self, new_root: &View) -> NodeId {
241 self.removed_ids.clear(); self.generation += 1;
244 self.stats = TreeStats::default();
245
246 let mut ctx = ReconcileContext::new(self.generation);
247
248 let root_id = if let Some(existing_root) = self.root {
249 self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
250 } else {
251 self.create_node(new_root, None, 0, 0, &mut ctx)
252 };
253
254 self.root = Some(root_id);
255
256 self.collect_garbage();
258
259 self.stats.total_nodes = self.nodes.len();
261 self.stats.dirty_nodes = self.dirty.len();
262 self.stats.reconciled_nodes = ctx.reconciled;
263 self.stats.skipped_nodes = ctx.skipped;
264 self.stats.created_nodes = ctx.created;
265 self.stats.removed_nodes = ctx.removed;
266
267 root_id
268 }
269
270 fn reconcile_node(
272 &mut self,
273 node_id: NodeId,
274 view: &View,
275 parent: Option<NodeId>,
276 depth: u32,
277 index_in_parent: u32,
278 ctx: &mut ReconcileContext,
279 ) -> NodeId {
280 let content_hash = hash_view_content(view);
281
282 let old_hash = self
283 .nodes
284 .get(node_id)
285 .expect("reconcile_node: node not found")
286 .content_hash;
287 let content_changed = old_hash != content_hash;
288
289 if content_changed {
290 self.invalidate_subcompose_cache(node_id);
291 }
292
293 let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
294 let subcomposed = self.run_subcompose(node_id, content);
295 let slot_views: Vec<View> = subcomposed.into_iter().map(|(_, v)| v).collect();
296 self.reconcile_children(node_id, &slot_views, depth, ctx)
297 } else {
298 self.reconcile_children(node_id, &view.children, depth, ctx)
299 };
300
301 let new_subtree_hash = hash_subtree(content_hash, &new_children_hashes);
302
303 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
304
305 let subtree_changed;
306 {
307 let node = self
308 .nodes
309 .get_mut(node_id)
310 .expect("reconcile_node: node not found");
311
312 node.parent = parent;
314 node.depth = depth;
315 node.generation = self.generation;
316
317 node.kind = view.kind.clone();
319 node.modifier = view.modifier.clone();
320 node.content_hash = content_hash;
321 node.user_key = view.modifier.key;
322
323 if content_changed {
324 node.invalidate_layout();
325 ctx.reconciled += 1;
326 }
327
328 subtree_changed = node.subtree_hash != new_subtree_hash;
330 if subtree_changed {
331 node.subtree_hash = new_subtree_hash;
332 } else if !content_changed {
333 ctx.skipped += 1;
334 }
335
336 node.view_id = view_id;
338 } if subtree_changed {
341 self.mark_dirty(node_id);
342 }
343 self.view_id_map.insert(view_id, node_id);
344
345 node_id
346 }
347 fn reconcile_children(
350 &mut self,
351 parent_id: NodeId,
352 new_children: &[View],
353 parent_depth: u32,
354 ctx: &mut ReconcileContext,
355 ) -> Vec<u64> {
356 let child_depth = parent_depth + 1;
357
358 let old_children: SmallVec<[NodeId; 4]> = self
360 .nodes
361 .get(parent_id)
362 .map(|n| n.children.clone())
363 .unwrap_or_default();
364
365 let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
367 let mut unkeyed_children: Vec<NodeId> = Vec::new();
368
369 for &child_id in &old_children {
370 if let Some(node) = self.nodes.get(child_id) {
371 if let Some(key) = node.user_key {
372 keyed_children.insert(key, child_id);
373 } else {
374 unkeyed_children.push(child_id);
375 }
376 }
377 }
378
379 let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
380 let mut new_subtree_hashes: Vec<u64> = Vec::with_capacity(new_children.len());
381 let mut unkeyed_index = 0;
382 let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
383
384 for (i, new_child) in new_children.iter().enumerate() {
385 let idx = i as u32;
386 let child_id = if let Some(key) = new_child.modifier.key {
387 if let Some(&existing_id) = keyed_children.get(&key) {
389 used_nodes.insert(existing_id);
390 self.reconcile_node(
391 existing_id,
392 new_child,
393 Some(parent_id),
394 child_depth,
395 idx,
396 ctx,
397 )
398 } else {
399 self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
400 }
401 } else {
402 if unkeyed_index < unkeyed_children.len() {
404 let existing_id = unkeyed_children[unkeyed_index];
405 unkeyed_index += 1;
406 used_nodes.insert(existing_id);
407 self.reconcile_node(
408 existing_id,
409 new_child,
410 Some(parent_id),
411 child_depth,
412 idx,
413 ctx,
414 )
415 } else {
416 self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
417 }
418 };
419
420 new_child_ids.push(child_id);
421
422 if let Some(node) = self.nodes.get(child_id) {
423 new_subtree_hashes.push(node.subtree_hash);
424 }
425 }
426
427 for &old_child in &old_children {
429 if !used_nodes.contains(&old_child) {
430 self.mark_for_removal(old_child, ctx);
431 }
432 }
433
434 if let Some(parent) = self.nodes.get_mut(parent_id) {
436 parent.children = new_child_ids;
437 }
438
439 new_subtree_hashes
440 }
441
442 fn create_node(
444 &mut self,
445 view: &View,
446 parent: Option<NodeId>,
447 depth: u32,
448 index_in_parent: u32,
449 ctx: &mut ReconcileContext,
450 ) -> NodeId {
451 let content_hash = hash_view_content(view);
452
453 let node_id = self.nodes.insert_with_key(|id| {
455 TreeNode::new(
456 id,
457 0,
458 view.kind.clone(),
459 view.modifier.clone(),
460 self.generation,
461 )
462 });
463 ctx.created += 1;
464
465 {
466 let node = self
467 .nodes
468 .get_mut(node_id)
469 .expect("create_node: node just inserted");
470 node.parent = parent;
471 node.depth = depth;
472 node.content_hash = content_hash;
473 node.user_key = view.modifier.key;
474 }
475
476 let child_depth = depth + 1;
478 let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
479 let mut child_hashes: Vec<u64> = Vec::with_capacity(view.children.len());
480 let children_to_create: Vec<View> =
481 if let ViewKind::SubcomposeLayout { content } = &view.kind {
482 self.run_subcompose(node_id, content)
483 .into_iter()
484 .map(|(_, v)| v)
485 .collect()
486 } else {
487 view.children.clone()
488 };
489 for (i, child_view) in children_to_create.iter().enumerate() {
490 let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
491 child_ids.push(child_id);
492 child_hashes.push(
493 self.nodes
494 .get(child_id)
495 .expect("create_node: child just created")
496 .subtree_hash,
497 );
498 }
499
500 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
502 let subtree_hash = hash_subtree(content_hash, &child_hashes);
503
504 let node = self
505 .nodes
506 .get_mut(node_id)
507 .expect("create_node: node just inserted");
508 node.children = child_ids;
509 node.subtree_hash = subtree_hash;
510 node.view_id = view_id;
511
512 self.view_id_map.insert(view_id, node_id);
513 self.dirty.insert(node_id);
514
515 node_id
516 }
517 fn compute_view_id(
519 &self,
520 view: &View,
521 _node_id: NodeId,
522 parent: Option<NodeId>,
523 index_in_parent: u32,
524 ) -> ViewId {
525 if view.id != 0 {
527 return view.id;
528 }
529
530 let parent_id = parent
532 .and_then(|p| self.nodes.get(p))
533 .map(|n| n.view_id)
534 .unwrap_or(0);
535
536 let salt = view.modifier.key.unwrap_or(index_in_parent as u64);
537
538 let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
540 id = id.wrapping_mul(0x9E3779B97F4A7C15);
541 id ^= id >> 30;
542
543 if id == 0 {
544 id = 1;
545 }
546
547 id
548 }
549
550 fn mark_for_removal(&mut self, node_id: NodeId, ctx: &mut ReconcileContext) {
552 let (view_id, children) = {
555 let node = self.nodes.get(node_id);
556 match node {
557 Some(n) => (n.view_id, n.children.clone()),
558 None => return,
559 }
560 };
561 self.view_id_map.remove(&view_id);
562 self.subcompose_cache.remove(&node_id);
563 for child_id in children.iter() {
564 self.collect_subcompose_cache(child_id);
565 }
566 for child_id in children {
567 self.mark_for_removal(child_id, ctx);
568 }
569 ctx.removed += 1;
570
571 if let Some(node) = self.nodes.get_mut(node_id) {
573 node.generation = 0; }
575 }
576
577 fn collect_garbage(&mut self) {
579 let current_gen = self.generation;
580
581 let to_remove: Vec<NodeId> = self
583 .nodes
584 .iter()
585 .filter(|(_, node)| node.generation != current_gen)
586 .map(|(id, _)| id)
587 .collect();
588
589 for id in to_remove {
591 if let Some(node) = self.nodes.remove(id) {
592 self.view_id_map.remove(&node.view_id);
593 self.dirty.remove(&id);
594
595 self.removed_ids.push(id);
597 }
598 }
599 }
600
601 pub fn set_layout(
603 &mut self,
604 id: NodeId,
605 rect: Rect,
606 screen_rect: Rect,
607 constraints: LayoutConstraints,
608 ) {
609 if let Some(node) = self.nodes.get_mut(id) {
610 node.layout_cache = Some(LayoutCache {
611 rect,
612 screen_rect,
613 constraints,
614 generation: self.generation,
615 });
616 }
617 }
618
619 pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
621 self.nodes.values()
622 }
623
624 pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
626 self.nodes.iter()
627 }
628
629 pub fn walk<F>(&self, mut f: F)
632 where
633 F: FnMut(&TreeNode, u32) -> bool,
634 {
635 if let Some(root_id) = self.root {
636 self.walk_node(root_id, 0, &mut f);
637 }
638 }
639
640 fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
641 where
642 F: FnMut(&TreeNode, u32) -> bool,
643 {
644 if let Some(node) = self.nodes.get(id) {
645 if !f(node, depth) {
646 return;
647 }
648
649 for &child_id in &node.children {
650 self.walk_node(child_id, depth + 1, f);
651 }
652 }
653 }
654
655 pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
657 self.nodes.get(id).map(|n| n.children.as_slice())
658 }
659}
660
661fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
666 let mut s = scope;
667 if let Some(w) = modifier.width {
668 s.min_width = s.min_width.max(w);
669 s.max_width = s.max_width.min(w);
670 }
671 if let Some(h) = modifier.height {
672 s.min_height = s.min_height.max(h);
673 s.max_height = s.max_height.min(h);
674 }
675 if let Some(mw) = modifier.min_width {
676 s.min_width = s.min_width.max(mw);
677 }
678 if let Some(mh) = modifier.min_height {
679 s.min_height = s.min_height.max(mh);
680 }
681 if let Some(mw) = modifier.max_width {
682 s.max_width = s.max_width.min(mw);
683 }
684 if let Some(mh) = modifier.max_height {
685 s.max_height = s.max_height.min(mh);
686 }
687 s
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use repose_core::{Color, Modifier, SubcomposeScope, View, ViewKind};
694 use std::sync::Arc;
695
696 fn text_view(text: &str) -> View {
697 View::new(
698 0,
699 ViewKind::Text {
700 text: text.to_string(),
701 color: Color::WHITE,
702 font_size: 16.0,
703 soft_wrap: true,
704 max_lines: None,
705 overflow: repose_core::TextOverflow::Visible,
706 font_family: None,
707 annotations: None,
708 },
709 )
710 }
711
712 fn box_view() -> View {
713 View::new(0, ViewKind::Box)
714 }
715
716 #[test]
717 fn test_create_tree() {
718 let mut tree = ViewTree::new();
719
720 let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
721
722 tree.update(&root);
723
724 assert_eq!(tree.len(), 3); assert!(tree.root().is_some());
726 }
727
728 #[test]
729 fn test_unchanged_tree_skips() {
730 let mut tree = ViewTree::new();
731
732 let root = box_view().with_children(vec![text_view("Hello")]);
733
734 tree.update(&root);
735 let gen1 = tree.generation();
736
737 tree.update(&root);
739 let gen2 = tree.generation();
740
741 assert_eq!(gen2, gen1 + 1);
742 assert!(tree.stats.skipped_nodes > 0);
743 }
744
745 #[test]
746 fn test_changed_content_reconciles() {
747 let mut tree = ViewTree::new();
748
749 let root1 = box_view().with_children(vec![text_view("Hello")]);
750
751 tree.update(&root1);
752
753 let root2 = box_view().with_children(vec![text_view("Changed")]);
754
755 tree.update(&root2);
756
757 assert!(tree.stats.reconciled_nodes > 0);
758 }
759
760 #[test]
761 fn test_keyed_children_stable() {
762 let mut tree = ViewTree::new();
763
764 let root1 = box_view().with_children(vec![
766 text_view("A").modifier(Modifier::new().key(1)),
767 text_view("B").modifier(Modifier::new().key(2)),
768 text_view("C").modifier(Modifier::new().key(3)),
769 ]);
770
771 tree.update(&root1);
772
773 let b_view_id = tree
775 .root()
776 .and_then(|r| tree.children(r))
777 .and_then(|c| c.get(1).copied())
778 .and_then(|id| tree.get(id))
779 .map(|n| n.view_id);
780
781 let root2 = box_view().with_children(vec![
783 text_view("C").modifier(Modifier::new().key(3)),
784 text_view("A").modifier(Modifier::new().key(1)),
785 text_view("B").modifier(Modifier::new().key(2)),
786 ]);
787
788 tree.update(&root2);
789
790 assert_eq!(tree.len(), 4); }
794
795 fn subcompose_view<F>(f: F) -> View
796 where
797 F: Fn(&SubcomposeScope) -> View + 'static,
798 {
799 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
800 Arc::new(move |scope| vec![(0, f(scope))]);
801 View {
802 id: 0,
803 kind: ViewKind::SubcomposeLayout { content },
804 modifier: Modifier::default(),
805 children: Vec::new(),
806 semantics: None,
807 }
808 }
809
810 #[test]
811 fn test_subcompose_invokes_content() {
812 let mut tree = ViewTree::new();
813 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
814 let counter2 = counter.clone();
815
816 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
817 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
818 text_view("from subcompose")
819 })]);
820
821 tree.update(&root);
822
823 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
824 assert_eq!(tree.len(), 3); }
826
827 #[test]
828 fn test_subcompose_receives_scope() {
829 let mut tree = ViewTree::new();
830 let captured = Arc::new(std::sync::Mutex::new(None));
831 let captured2 = captured.clone();
832
833 let root = box_view().with_children(vec![subcompose_view(move |scope| {
834 *captured2.lock().unwrap() = Some(*scope);
835 text_view("hi")
836 })]);
837
838 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 360.0, 0.0, 640.0));
839 tree.update(&root);
840
841 let observed = captured.lock().unwrap().expect("scope captured");
842 assert_eq!(observed.max_width, 360.0);
843 assert_eq!(observed.max_height, 640.0);
844 assert_eq!(observed.min_width, 0.0);
845 assert_eq!(observed.min_height, 0.0);
846 }
847
848 #[test]
849 fn test_subcompose_re_invokes_on_update() {
850 let mut tree = ViewTree::new();
851 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
852 let counter2 = counter.clone();
853
854 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
855 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
856 text_view("hi")
857 })]);
858
859 tree.update(&root);
860 tree.update(&root);
861 tree.update(&root);
862
863 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
866 }
867
868 #[test]
869 fn test_subcompose_reruns_on_scope_change() {
870 let mut tree = ViewTree::new();
871 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
872 let counter2 = counter.clone();
873
874 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
875 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
876 text_view("hi")
877 })]);
878
879 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
880 tree.update(&root);
881 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
882
883 tree.update(&root);
885 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
886
887 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 200.0, 0.0, 200.0));
889 tree.update(&root);
890 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
891 }
892
893 #[test]
894 fn test_subcompose_reruns_on_content_change() {
895 let mut tree = ViewTree::new();
896 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
897 let c1 = counter.clone();
898
899 let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
900 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
901 text_view("hi")
902 })]);
903
904 tree.update(&root1);
905 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
906
907 tree.update(&root1);
909 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
910
911 let c2 = counter.clone();
913 let root2 = box_view().with_children(vec![
914 subcompose_view(move |_scope| {
915 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
916 text_view("hi")
917 })
918 .modifier(Modifier::new().padding(4.0)),
919 ]);
920
921 tree.update(&root2);
922 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
923 }
924
925 #[test]
926 fn test_subcompose_cache_drops_on_node_removal() {
927 let mut tree = ViewTree::new();
928 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 100.0, 0.0, 100.0));
929
930 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
932 let c1 = counter.clone();
933 let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
934 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
935 text_view("hi")
936 })]);
937
938 tree.update(&root_with_sub);
939 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
940
941 let root_no_sub = box_view().with_children(vec![text_view("plain")]);
944 tree.update(&root_no_sub);
945 assert_eq!(tree.len(), 2);
946
947 let c2 = counter.clone();
950 let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
951 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
952 text_view("hi")
953 })]);
954
955 tree.update(&root_with_sub_again);
956 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
957 }
958
959 fn multi_slot_view<F>(f: F) -> View
960 where
961 F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
962 {
963 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
964 View {
965 id: 0,
966 kind: ViewKind::SubcomposeLayout { content },
967 modifier: Modifier::default(),
968 children: Vec::new(),
969 semantics: None,
970 }
971 }
972
973 #[test]
974 fn test_subcompose_multi_slot_produces_multiple_children() {
975 let mut tree = ViewTree::new();
976 let root = box_view().with_children(vec![multi_slot_view(|_scope| {
977 vec![
978 (0, text_view("a")),
979 (1, text_view("b")),
980 (2, text_view("c")),
981 ]
982 })]);
983
984 tree.update(&root);
985
986 assert_eq!(tree.len(), 5);
988 let sub_id = tree
989 .root()
990 .and_then(|r| tree.children(r))
991 .and_then(|c| c.first().copied())
992 .expect("subcompose node");
993 let sub_children = tree.children(sub_id).expect("subcompose has children");
994 assert_eq!(sub_children.len(), 3);
995 }
996
997 #[test]
998 fn test_subcompose_multi_slot_preserves_identity_across_removal() {
999 let mut tree = ViewTree::new();
1000
1001 let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
1003 vec![
1004 (0, text_view("a")),
1005 (1, text_view("b")),
1006 (2, text_view("c")),
1007 ]
1008 })]);
1009 tree.update(&root3);
1010
1011 let sub_id = tree
1012 .root()
1013 .and_then(|r| tree.children(r))
1014 .and_then(|c| c.first().copied())
1015 .expect("subcompose node");
1016 let before = tree.children(sub_id).expect("children").to_vec();
1017 let a_node = before[0];
1018 let b_node = before[1];
1019 let c_node = before[2];
1020
1021 let root2 = box_view().with_children(vec![
1024 multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
1025 .modifier(Modifier::new().padding(4.0)),
1026 ]);
1027 tree.update(&root2);
1028
1029 let after = tree.children(sub_id).expect("children after");
1030 assert_eq!(after.len(), 2);
1031 assert_eq!(after[0], a_node);
1033 assert_eq!(after[1], c_node);
1034 assert!(tree.get(b_node).is_none());
1036 }
1037
1038 #[test]
1039 fn test_subcompose_ancestor_modifier_narrows_scope() {
1040 let mut tree = ViewTree::new();
1041 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1042
1043 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1044 let cap2 = captured.clone();
1045
1046 let sub = multi_slot_view(move |scope| {
1049 *cap2.lock().unwrap() = *scope;
1050 vec![(0, text_view("hi"))]
1051 });
1052 let root = box_view()
1053 .modifier(Modifier::new().width(200.0))
1054 .with_children(vec![sub]);
1055
1056 tree.update(&root);
1057
1058 let observed = *captured.lock().unwrap();
1059 assert_eq!(observed.max_width, 200.0);
1060 }
1061
1062 #[test]
1063 fn test_subcompose_chained_ancestor_constraints_intersect() {
1064 let mut tree = ViewTree::new();
1065 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1066
1067 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1068 let cap2 = captured.clone();
1069
1070 let sub = multi_slot_view(move |scope| {
1071 *cap2.lock().unwrap() = *scope;
1072 vec![(0, text_view("hi"))]
1073 });
1074 let root = box_view()
1077 .modifier(Modifier::new().width(400.0))
1078 .with_children(vec![
1079 box_view()
1080 .modifier(Modifier::new().max_width(300.0))
1081 .with_children(vec![sub]),
1082 ]);
1083
1084 tree.update(&root);
1085
1086 let observed = *captured.lock().unwrap();
1087 assert_eq!(observed.max_width, 300.0);
1088 }
1089
1090 #[test]
1091 fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
1092 let mut tree = ViewTree::new();
1093 tree.set_subcompose_scope(SubcomposeScope::new(0.0, 1000.0, 0.0, 1000.0));
1094
1095 let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1096 let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1097 let outer2 = outer_captured.clone();
1098 let inner2 = inner_captured.clone();
1099
1100 let inner = Arc::new(multi_slot_view(move |scope| {
1103 *inner2.lock().unwrap() = *scope;
1104 vec![(0, text_view("inner"))]
1105 }));
1106 let inner_clone = inner.clone();
1107 let outer = multi_slot_view(move |scope| {
1108 *outer2.lock().unwrap() = *scope;
1109 vec![(0, (*inner_clone).clone())]
1110 })
1111 .modifier(Modifier::new().width(400.0));
1112 let root = box_view().with_children(vec![outer]);
1113
1114 tree.update(&root);
1115
1116 let outer_obs = *outer_captured.lock().unwrap();
1117 let inner_obs = *inner_captured.lock().unwrap();
1118 assert_eq!(outer_obs.max_width, 400.0);
1119 assert_eq!(inner_obs.max_width, 400.0);
1120 }
1121}