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