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