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