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