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::Key;
11use slotmap::SlotMap;
12use smallvec::SmallVec;
13use std::sync::Arc;
14
15pub struct ViewTree {
17 nodes: SlotMap<NodeId, TreeNode>,
19
20 root: Option<NodeId>,
22
23 dirty: FxHashSet<NodeId>,
25
26 generation: u64,
28
29 view_id_map: FxHashMap<ViewId, NodeId>,
31
32 pub stats: TreeStats,
34
35 pub removed_ids: Vec<NodeId>,
37
38 subcompose_scope: SubcomposeScope,
42
43 subcompose_cache: FxHashMap<NodeId, (SubcomposeScope, Vec<(u64, View)>)>,
48}
49
50impl Default for ViewTree {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl ViewTree {
57 pub fn new() -> Self {
59 Self {
60 nodes: SlotMap::with_key(),
61 root: None,
62 dirty: FxHashSet::default(),
63 generation: 0,
64 view_id_map: FxHashMap::default(),
65 stats: TreeStats::default(),
66 removed_ids: Vec::new(),
67 subcompose_scope: SubcomposeScope::UNBOUNDED,
68 subcompose_cache: FxHashMap::default(),
69 }
70 }
71
72 pub fn set_subcompose_scope(&mut self, scope: SubcomposeScope) {
78 self.subcompose_scope = scope;
79 }
80
81 pub fn subcompose_scope(&self) -> SubcomposeScope {
83 self.subcompose_scope
84 }
85
86 fn run_subcompose(
96 &mut self,
97 node_id: NodeId,
98 content: &Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
99 ) -> Vec<(u64, View)> {
100 let scope = self.compute_scope_for_node(node_id);
101 if let Some((cached_scope, cached_slots)) = self.subcompose_cache.get(&node_id)
102 && *cached_scope == scope
103 {
104 return cached_slots.clone();
105 }
106 let mut slots = content(&scope);
107 let scope_key = format!("subcompose_{:?}", node_id);
109 for (slot_id, view) in slots.iter_mut() {
110 view.modifier.key = Some(*slot_id);
111 view.scope_key = Some(scope_key.clone());
112 view.modifier.repaint_boundary = true;
113 }
114 self.subcompose_cache
115 .insert(node_id, (scope, slots.clone()));
116 slots
117 }
118
119 fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
127 let mut scope = self.subcompose_scope;
128 let mut chain: Vec<NodeId> = Vec::new();
129 let mut current = Some(node_id);
130 while let Some(id) = current {
131 chain.push(id);
132 match self.nodes.get(id) {
133 Some(node) => current = node.parent,
134 None => break,
135 }
136 }
137 chain.reverse();
138 for ancestor_id in chain {
139 if let Some(node) = self.nodes.get(ancestor_id) {
140 scope = intersect_scope_with_modifier(scope, &node.modifier);
141 if let Some(cache) = &node.layout_cache {
144 let w = cache.rect.w;
146 if w > 0.0 && w.is_finite() {
147 scope.max_width = scope.max_width.min(repose_core::Px(w).to_dp());
148 }
149 let h = cache.rect.h;
150 if h > 0.0 && h.is_finite() {
151 scope.max_height = scope.max_height.min(repose_core::Px(h).to_dp());
152 }
153 }
154 }
155 }
156 scope
157 }
158
159 pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
163 self.subcompose_cache.remove(&node_id);
164 }
165
166 fn collect_subcompose_cache(&mut self, node_id: &NodeId) {
169 self.subcompose_cache.remove(node_id);
170 let children: Vec<NodeId> = self
171 .nodes
172 .get(*node_id)
173 .map(|n| n.children.iter().copied().collect())
174 .unwrap_or_default();
175 for child in children {
176 self.collect_subcompose_cache(&child);
177 }
178 }
179
180 pub fn generation(&self) -> u64 {
182 self.generation
183 }
184
185 pub fn root(&self) -> Option<NodeId> {
187 self.root
188 }
189
190 pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
192 self.nodes.get(id)
193 }
194
195 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
197 self.nodes.get_mut(id)
198 }
199
200 pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
202 self.view_id_map
203 .get(&view_id)
204 .and_then(|id| self.nodes.get(*id))
205 }
206
207 pub fn len(&self) -> usize {
209 self.nodes.len()
210 }
211
212 pub fn is_empty(&self) -> bool {
214 self.nodes.is_empty()
215 }
216
217 pub fn is_dirty(&self, id: NodeId) -> bool {
219 self.dirty.contains(&id)
220 }
221
222 pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
224 &self.dirty
225 }
226
227 pub fn clear_dirty(&mut self) {
229 self.dirty.clear();
230 }
231
232 pub fn mark_dirty(&mut self, id: NodeId) {
234 self.dirty.insert(id);
235
236 let mut current = id;
237 while let Some(node) = self.nodes.get(current) {
238 if let Some(parent) = node.parent {
239 self.dirty.insert(parent);
240 current = parent;
241 } else {
242 break;
243 }
244 }
245
246 let needs_scope_invalidate = self
247 .nodes
248 .get(id)
249 .map(|n| {
250 n.modifier.transform.is_some()
251 || n.modifier.alpha.is_some()
252 || n.modifier.graphics_layer.is_some()
253 })
254 .unwrap_or(false);
255 if needs_scope_invalidate {
256 let mut stack = vec![id];
258 while let Some(cur) = stack.pop() {
259 if let Some(node) = self.nodes.get(cur) {
260 for &child in &node.children {
261 if let Some(child_node) = self.nodes.get(child) {
262 if child_node.scope_key.is_some() {
263 self.dirty.insert(child);
264 }
267 stack.push(child);
268 }
269 }
270 }
271 }
272 }
273 }
274
275 pub fn update(&mut self, new_root: &View) -> NodeId {
278 self.removed_ids.clear(); self.generation += 1;
281 self.stats = TreeStats::default();
282
283 let mut ctx = ReconcileContext::new(self.generation);
284
285 let root_id = if let Some(existing_root) = self.root {
286 self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
287 } else {
288 self.create_node(new_root, None, 0, 0, &mut ctx)
289 };
290
291 self.root = Some(root_id);
292
293 self.collect_garbage();
294
295 self.stats.total_nodes = self.nodes.len();
296 self.stats.dirty_nodes = self.dirty.len();
297 self.stats.reconciled_nodes = ctx.reconciled;
298 self.stats.skipped_nodes = ctx.skipped;
299 self.stats.created_nodes = ctx.created;
300 self.stats.removed_nodes = ctx.removed;
301
302 root_id
303 }
304
305 fn reconcile_node(
307 &mut self,
308 node_id: NodeId,
309 view: &View,
310 parent: Option<NodeId>,
311 depth: u32,
312 index_in_parent: u32,
313 ctx: &mut ReconcileContext,
314 ) -> NodeId {
315 let content_hash = hash_view_content(view);
316
317 let old_hash = match self.nodes.get(node_id) {
318 Some(n) => n.content_hash,
319 None => {
320 log::error!(
321 "reconcile_node: node {:?} not found (GC race) - creating fresh",
322 node_id
323 );
324 return self.create_node(view, parent, depth, index_in_parent, ctx);
326 }
327 };
328 let content_changed = old_hash != content_hash;
329
330 if content_changed {
331 self.invalidate_subcompose_cache(node_id);
332 }
333
334 let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
335 let subcomposed = self.run_subcompose(node_id, content);
336 let slot_views: Vec<View> = subcomposed.into_iter().map(|(_, v)| v).collect();
337 self.reconcile_children(node_id, &slot_views, depth, ctx)
338 } else {
339 self.reconcile_children(node_id, &view.children, depth, ctx)
340 };
341
342 let new_subtree_hash = hash_subtree(content_hash, &new_children_hashes);
343
344 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
345
346 let subtree_changed;
347 {
348 let Some(node) = self.nodes.get_mut(node_id) else {
349 log::error!("reconcile_node: node {:?} vanished mid-reconcile", node_id);
350 return node_id;
351 };
352
353 node.parent = parent;
354 node.depth = depth;
355 node.generation = self.generation;
356
357 node.kind = view.kind.clone();
359 node.modifier = view.modifier.clone();
360 node.content_hash = content_hash;
361 node.user_key = view.modifier.key;
362 node.scope_key = view.scope_key.clone();
363
364 if content_changed {
365 node.invalidate_layout();
366 ctx.reconciled += 1;
367 }
368
369 subtree_changed = node.subtree_hash != new_subtree_hash;
371 if subtree_changed {
372 node.subtree_hash = new_subtree_hash;
373 } else if !content_changed {
374 ctx.skipped += 1;
375 }
376
377 node.view_id = view_id;
378 } if subtree_changed {
381 self.mark_dirty(node_id);
382 }
383 self.view_id_map.insert(view_id, node_id);
384
385 node_id
386 }
387 fn reconcile_children(
390 &mut self,
391 parent_id: NodeId,
392 new_children: &[View],
393 parent_depth: u32,
394 ctx: &mut ReconcileContext,
395 ) -> Vec<u64> {
396 let child_depth = parent_depth + 1;
397
398 let old_children: SmallVec<[NodeId; 4]> = self
399 .nodes
400 .get(parent_id)
401 .map(|n| n.children.clone())
402 .unwrap_or_default();
403
404 let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
405 let mut unkeyed_children: Vec<NodeId> = Vec::new();
406
407 for &child_id in &old_children {
408 if let Some(node) = self.nodes.get(child_id) {
409 if matches!(node.kind, ViewKind::SubcomposeLayout { .. }) {
410 unkeyed_children.push(child_id);
411 } else if let Some(key) = node.user_key {
412 keyed_children.insert(key, child_id);
413 } else {
414 unkeyed_children.push(child_id);
415 }
416 }
417 }
418
419 let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
420 let mut new_subtree_hashes: Vec<u64> = Vec::with_capacity(new_children.len());
421 let mut unkeyed_index = 0;
422 let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
423 let mut new_seen_keys: FxHashSet<u64> = FxHashSet::default();
424
425 for (i, new_child) in new_children.iter().enumerate() {
426 let is_subcompose = matches!(new_child.kind, ViewKind::SubcomposeLayout { .. });
427 if is_subcompose {
428 if let Some(key) = new_child.modifier.key {
429 new_seen_keys.insert(key);
430 }
431 let idx = i as u32;
432 let child_id = if unkeyed_index < unkeyed_children.len() {
433 let existing_id = unkeyed_children[unkeyed_index];
434 unkeyed_index += 1;
435 used_nodes.insert(existing_id);
436 self.reconcile_node(
437 existing_id,
438 new_child,
439 Some(parent_id),
440 child_depth,
441 idx,
442 ctx,
443 )
444 } else {
445 self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
446 };
447 new_child_ids.push(child_id);
448 if let Some(node) = self.nodes.get(child_id) {
449 new_subtree_hashes.push(node.subtree_hash);
450 }
451 continue;
452 }
453 if let Some(key) = new_child.modifier.key
454 && !new_seen_keys.insert(key)
455 {
456 log::error!(
457 "reconcile_children: duplicate modifier.key={} in children of node {:?} - deduplicating (suffixing). Ensure get_key returns unique keys.",
458 key,
459 parent_id
460 );
461 let mut deduped = new_child.clone();
462 let salt = (key.wrapping_mul(0x9E3779B97F4A7C15)
463 ^ (i as u64).wrapping_add(0xBF58476D1CE4E5B9))
464 .wrapping_add(parent_id.data().as_ffi());
465 deduped.modifier.key = Some(salt);
466 let deduped_ref = deduped;
467 let idx = i as u32;
468 let child_id = if let Some(k) = deduped_ref.modifier.key {
469 if let Some(&existing_id) = keyed_children.get(&k) {
470 used_nodes.insert(existing_id);
471 self.reconcile_node(
472 existing_id,
473 &deduped_ref,
474 Some(parent_id),
475 child_depth,
476 idx,
477 ctx,
478 )
479 } else {
480 self.create_node(&deduped_ref, Some(parent_id), child_depth, idx, ctx)
481 }
482 } else {
483 self.create_node(&deduped_ref, Some(parent_id), child_depth, idx, ctx)
484 };
485 new_child_ids.push(child_id);
486 if let Some(node) = self.nodes.get(child_id) {
487 new_subtree_hashes.push(node.subtree_hash);
488 }
489 continue;
490 }
491 let idx = i as u32;
492 let child_id = if let Some(key) = new_child.modifier.key {
493 if let Some(&existing_id) = keyed_children.get(&key) {
494 used_nodes.insert(existing_id);
495 self.reconcile_node(
496 existing_id,
497 new_child,
498 Some(parent_id),
499 child_depth,
500 idx,
501 ctx,
502 )
503 } else {
504 self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
505 }
506 } else {
507 if unkeyed_index < unkeyed_children.len() {
508 let existing_id = unkeyed_children[unkeyed_index];
509 unkeyed_index += 1;
510 used_nodes.insert(existing_id);
511 self.reconcile_node(
512 existing_id,
513 new_child,
514 Some(parent_id),
515 child_depth,
516 idx,
517 ctx,
518 )
519 } else {
520 self.create_node(new_child, Some(parent_id), child_depth, idx, ctx)
521 }
522 };
523
524 new_child_ids.push(child_id);
525
526 if let Some(node) = self.nodes.get(child_id) {
527 new_subtree_hashes.push(node.subtree_hash);
528 }
529 }
530
531 for &old_child in &old_children {
532 if !used_nodes.contains(&old_child) {
533 self.mark_for_removal(old_child, ctx);
534 }
535 }
536
537 if let Some(parent) = self.nodes.get_mut(parent_id) {
538 parent.children = new_child_ids;
539 }
540
541 new_subtree_hashes
542 }
543
544 fn create_node(
546 &mut self,
547 view: &View,
548 parent: Option<NodeId>,
549 depth: u32,
550 index_in_parent: u32,
551 ctx: &mut ReconcileContext,
552 ) -> NodeId {
553 let content_hash = hash_view_content(view);
554
555 let node_id = self.nodes.insert_with_key(|id| {
556 TreeNode::new(
557 id,
558 0,
559 view.kind.clone(),
560 view.modifier.clone(),
561 self.generation,
562 )
563 });
564 ctx.created += 1;
565
566 {
567 let node = self
568 .nodes
569 .get_mut(node_id)
570 .expect("create_node: node just inserted");
571 node.parent = parent;
572 node.depth = depth;
573 node.content_hash = content_hash;
574 node.user_key = view.modifier.key;
575 node.scope_key = view.scope_key.clone();
576 }
577
578 let child_depth = depth + 1;
579 let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
580 let mut child_hashes: Vec<u64> = Vec::with_capacity(view.children.len());
581 let children_to_create: Vec<View> =
582 if let ViewKind::SubcomposeLayout { content } = &view.kind {
583 self.run_subcompose(node_id, content)
584 .into_iter()
585 .map(|(_, v)| v)
586 .collect()
587 } else {
588 view.children.clone()
589 };
590 for (i, child_view) in children_to_create.iter().enumerate() {
591 let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
592 child_ids.push(child_id);
593 child_hashes.push(
594 self.nodes
595 .get(child_id)
596 .expect("create_node: child just created")
597 .subtree_hash,
598 );
599 }
600
601 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
602 let subtree_hash = hash_subtree(content_hash, &child_hashes);
603
604 let node = self
605 .nodes
606 .get_mut(node_id)
607 .expect("create_node: node just inserted");
608 node.children = child_ids;
609 node.subtree_hash = subtree_hash;
610 node.view_id = view_id;
611
612 self.view_id_map.insert(view_id, node_id);
613 self.dirty.insert(node_id);
614
615 node_id
616 }
617 fn compute_view_id(
619 &self,
620 view: &View,
621 _node_id: NodeId,
622 parent: Option<NodeId>,
623 index_in_parent: u32,
624 ) -> ViewId {
625 if view.id != 0 {
626 return view.id;
627 }
628
629 let parent_id = parent
630 .and_then(|p| self.nodes.get(p))
631 .map(|n| n.view_id)
632 .unwrap_or(0);
633
634 let salt = view.modifier.key.unwrap_or(index_in_parent as u64);
635
636 let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
637 id = id.wrapping_mul(0x9E3779B97F4A7C15);
638 id ^= id >> 30;
639
640 if id == 0 {
641 id = 1;
642 }
643
644 id
645 }
646
647 fn mark_for_removal(&mut self, node_id: NodeId, ctx: &mut ReconcileContext) {
649 let (view_id, children) = {
651 let node = self.nodes.get(node_id);
652 match node {
653 Some(n) => (n.view_id, n.children.clone()),
654 None => return,
655 }
656 };
657 self.view_id_map.remove(&view_id);
658 self.subcompose_cache.remove(&node_id);
659 for child_id in children.iter() {
660 self.collect_subcompose_cache(child_id);
661 }
662 for child_id in children {
663 self.mark_for_removal(child_id, ctx);
664 }
665 ctx.removed += 1;
666
667 if let Some(node) = self.nodes.get_mut(node_id) {
668 node.generation = 0; }
670 }
671
672 fn collect_garbage(&mut self) {
674 let current_gen = self.generation;
675
676 let to_remove: Vec<NodeId> = self
677 .nodes
678 .iter()
679 .filter(|(_, node)| node.generation != current_gen)
680 .map(|(id, _)| id)
681 .collect();
682
683 for id in to_remove {
684 if let Some(node) = self.nodes.remove(id) {
685 self.view_id_map.remove(&node.view_id);
686 self.dirty.remove(&id);
687
688 self.removed_ids.push(id);
690 }
691 }
692 }
693
694 pub fn set_layout(
696 &mut self,
697 id: NodeId,
698 rect: Rect,
699 screen_rect: Rect,
700 constraints: LayoutConstraints,
701 ) {
702 if let Some(node) = self.nodes.get_mut(id) {
703 node.layout_cache = Some(LayoutCache {
704 rect,
705 screen_rect,
706 constraints,
707 generation: self.generation,
708 });
709 }
710 }
711
712 pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
714 self.nodes.values()
715 }
716
717 pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
719 self.nodes.iter()
720 }
721
722 pub fn walk<F>(&self, mut f: F)
725 where
726 F: FnMut(&TreeNode, u32) -> bool,
727 {
728 if let Some(root_id) = self.root {
729 self.walk_node(root_id, 0, &mut f);
730 }
731 }
732
733 fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
734 where
735 F: FnMut(&TreeNode, u32) -> bool,
736 {
737 if let Some(node) = self.nodes.get(id) {
738 if !f(node, depth) {
739 return;
740 }
741
742 for &child_id in &node.children {
743 self.walk_node(child_id, depth + 1, f);
744 }
745 }
746 }
747
748 pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
750 self.nodes.get(id).map(|n| n.children.as_slice())
751 }
752}
753
754fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
762 let mut s = scope;
763 if let Some(sz) = modifier.size {
765 s.min_width = s.min_width.max(sz.width);
766 s.max_width = s.max_width.min(sz.width);
767 s.min_height = s.min_height.max(sz.height);
768 s.max_height = s.max_height.min(sz.height);
769 }
770 if let Some(w) = modifier.width {
771 s.min_width = s.min_width.max(w);
772 s.max_width = s.max_width.min(w);
773 }
774 if let Some(h) = modifier.height {
775 s.min_height = s.min_height.max(h);
776 s.max_height = s.max_height.min(h);
777 }
778 if let Some(mw) = modifier.min_width {
779 s.min_width = s.min_width.max(mw);
780 }
781 if let Some(mh) = modifier.min_height {
782 s.min_height = s.min_height.max(mh);
783 }
784 if let Some(mw) = modifier.max_width {
785 s.max_width = s.max_width.min(mw);
786 }
787 if let Some(mh) = modifier.max_height {
788 s.max_height = s.max_height.min(mh);
789 }
790 if let Some(p) = modifier.padding {
793 let total = p * 2.0;
794 s.min_width = (s.min_width - total).max(repose_core::Dp::ZERO);
795 s.max_width = (s.max_width - total).max(repose_core::Dp::ZERO);
796 s.min_height = (s.min_height - total).max(repose_core::Dp::ZERO);
797 s.max_height = (s.max_height - total).max(repose_core::Dp::ZERO);
798 }
799 if let Some(pv) = modifier.padding_values {
800 let h_total = pv.left + pv.right;
801 let v_total = pv.top + pv.bottom;
802 s.min_width = (s.min_width - h_total).max(repose_core::Dp::ZERO);
803 s.max_width = (s.max_width - h_total).max(repose_core::Dp::ZERO);
804 s.min_height = (s.min_height - v_total).max(repose_core::Dp::ZERO);
805 s.max_height = (s.max_height - v_total).max(repose_core::Dp::ZERO);
806 }
807 s
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813 use repose_core::{
814 Color, Dp, FontStyle, FontWeight, Modifier, Sp, SubcomposeScope, TextAlign, TextDecoration,
815 UnitExt, View, ViewKind,
816 };
817 use std::sync::Arc;
818
819 fn text_view(text: &str) -> View {
820 View::new(
821 0,
822 ViewKind::Text {
823 text: text.to_string(),
824 color: Color::WHITE,
825 font_size: 16.0.sp(),
826 soft_wrap: true,
827 max_lines: None,
828 overflow: repose_core::TextOverflow::Visible,
829 font_family: None,
830 annotations: None,
831 text_align: TextAlign::Unspecified,
832 font_weight: FontWeight::NORMAL,
833 font_style: FontStyle::Normal,
834 text_decoration: TextDecoration::default(),
835 letter_spacing: Sp::ZERO,
836 line_height: Sp::ZERO,
837 url: None,
838 font_variation_settings: None,
839 },
840 )
841 }
842
843 fn box_view() -> View {
844 View::new(0, ViewKind::Box)
845 }
846
847 #[test]
848 fn test_create_tree() {
849 let mut tree = ViewTree::new();
850
851 let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
852
853 tree.update(&root);
854
855 assert_eq!(tree.len(), 3); assert!(tree.root().is_some());
857 }
858
859 #[test]
860 fn test_unchanged_tree_skips() {
861 let mut tree = ViewTree::new();
862
863 let root = box_view().with_children(vec![text_view("Hello")]);
864
865 tree.update(&root);
866 let gen1 = tree.generation();
867
868 tree.update(&root);
869 let gen2 = tree.generation();
870
871 assert_eq!(gen2, gen1 + 1);
872 assert!(tree.stats.skipped_nodes > 0);
873 }
874
875 #[test]
876 fn test_changed_content_reconciles() {
877 let mut tree = ViewTree::new();
878
879 let root1 = box_view().with_children(vec![text_view("Hello")]);
880
881 tree.update(&root1);
882
883 let root2 = box_view().with_children(vec![text_view("Changed")]);
884
885 tree.update(&root2);
886
887 assert!(tree.stats.reconciled_nodes > 0);
888 }
889
890 #[test]
891 fn test_keyed_children_stable() {
892 let mut tree = ViewTree::new();
893
894 let root1 = box_view().with_children(vec![
896 text_view("A").modifier(Modifier::new().key(1)),
897 text_view("B").modifier(Modifier::new().key(2)),
898 text_view("C").modifier(Modifier::new().key(3)),
899 ]);
900
901 tree.update(&root1);
902
903 let _b_view_id = tree
904 .root()
905 .and_then(|r| tree.children(r))
906 .and_then(|c| c.get(1).copied())
907 .and_then(|id| tree.get(id))
908 .map(|n| n.view_id);
909
910 let root2 = box_view().with_children(vec![
912 text_view("C").modifier(Modifier::new().key(3)),
913 text_view("A").modifier(Modifier::new().key(1)),
914 text_view("B").modifier(Modifier::new().key(2)),
915 ]);
916
917 tree.update(&root2);
918
919 assert_eq!(tree.len(), 4); }
923
924 fn subcompose_view<F>(f: F) -> View
925 where
926 F: Fn(&SubcomposeScope) -> View + 'static,
927 {
928 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
929 Arc::new(move |scope| vec![(0, f(scope))]);
930 View {
931 id: 0,
932 kind: ViewKind::SubcomposeLayout { content },
933 modifier: Modifier::default(),
934 children: Vec::new(),
935 scope_key: None,
936 semantics: None,
937 }
938 }
939
940 #[test]
941 fn test_subcompose_invokes_content() {
942 let mut tree = ViewTree::new();
943 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
944 let counter2 = counter.clone();
945
946 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
947 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
948 text_view("from subcompose")
949 })]);
950
951 tree.update(&root);
952
953 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
954 assert_eq!(tree.len(), 3); }
956
957 #[test]
958 fn test_subcompose_receives_scope() {
959 let mut tree = ViewTree::new();
960 let captured = Arc::new(std::sync::Mutex::new(None));
961 let captured2 = captured.clone();
962
963 let root = box_view().with_children(vec![subcompose_view(move |scope| {
964 *captured2.lock().unwrap() = Some(*scope);
965 text_view("hi")
966 })]);
967
968 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(360.0), Dp(0.0), Dp(640.0)));
969 tree.update(&root);
970
971 let observed = captured.lock().unwrap().expect("scope captured");
972 assert_eq!(observed.max_width, Dp(360.0));
973 assert_eq!(observed.max_height, Dp(640.0));
974 assert_eq!(observed.min_width, Dp(0.0));
975 assert_eq!(observed.min_height, Dp(0.0));
976 }
977
978 #[test]
979 fn test_subcompose_re_invokes_on_update() {
980 let mut tree = ViewTree::new();
981 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
982 let counter2 = counter.clone();
983
984 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
985 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
986 text_view("hi")
987 })]);
988
989 tree.update(&root);
990 tree.update(&root);
991 tree.update(&root);
992
993 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
996 }
997
998 #[test]
999 fn test_subcompose_reruns_on_scope_change() {
1000 let mut tree = ViewTree::new();
1001 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1002 let counter2 = counter.clone();
1003
1004 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
1005 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1006 text_view("hi")
1007 })]);
1008
1009 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
1010 tree.update(&root);
1011 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1012
1013 tree.update(&root);
1015 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1016
1017 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(200.0), Dp(0.0), Dp(200.0)));
1019 tree.update(&root);
1020 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1021 }
1022
1023 #[test]
1024 fn test_subcompose_reruns_on_content_change() {
1025 let mut tree = ViewTree::new();
1026 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1027 let c1 = counter.clone();
1028
1029 let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
1030 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1031 text_view("hi")
1032 })]);
1033
1034 tree.update(&root1);
1035 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1036
1037 tree.update(&root1);
1039 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1040
1041 let c2 = counter.clone();
1043 let root2 = box_view().with_children(vec![
1044 subcompose_view(move |_scope| {
1045 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1046 text_view("hi")
1047 })
1048 .modifier(Modifier::new().padding(4.0.dp())),
1049 ]);
1050
1051 tree.update(&root2);
1052 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1053 }
1054
1055 #[test]
1056 fn test_subcompose_cache_drops_on_node_removal() {
1057 let mut tree = ViewTree::new();
1058 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
1059
1060 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1062 let c1 = counter.clone();
1063 let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
1064 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1065 text_view("hi")
1066 })]);
1067
1068 tree.update(&root_with_sub);
1069 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1070
1071 let root_no_sub = box_view().with_children(vec![text_view("plain")]);
1074 tree.update(&root_no_sub);
1075 assert_eq!(tree.len(), 2);
1076
1077 let c2 = counter.clone();
1080 let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
1081 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1082 text_view("hi")
1083 })]);
1084
1085 tree.update(&root_with_sub_again);
1086 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1087 }
1088
1089 fn multi_slot_view<F>(f: F) -> View
1090 where
1091 F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
1092 {
1093 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
1094 View {
1095 id: 0,
1096 kind: ViewKind::SubcomposeLayout { content },
1097 modifier: Modifier::default(),
1098 children: Vec::new(),
1099 scope_key: None,
1100 semantics: None,
1101 }
1102 }
1103
1104 #[test]
1105 fn test_subcompose_multi_slot_produces_multiple_children() {
1106 let mut tree = ViewTree::new();
1107 let root = box_view().with_children(vec![multi_slot_view(|_scope| {
1108 vec![
1109 (0, text_view("a")),
1110 (1, text_view("b")),
1111 (2, text_view("c")),
1112 ]
1113 })]);
1114
1115 tree.update(&root);
1116
1117 assert_eq!(tree.len(), 5);
1119 let sub_id = tree
1120 .root()
1121 .and_then(|r| tree.children(r))
1122 .and_then(|c| c.first().copied())
1123 .expect("subcompose node");
1124 let sub_children = tree.children(sub_id).expect("subcompose has children");
1125 assert_eq!(sub_children.len(), 3);
1126 }
1127
1128 #[test]
1129 fn test_subcompose_multi_slot_preserves_identity_across_removal() {
1130 let mut tree = ViewTree::new();
1131
1132 let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
1134 vec![
1135 (0, text_view("a")),
1136 (1, text_view("b")),
1137 (2, text_view("c")),
1138 ]
1139 })]);
1140 tree.update(&root3);
1141
1142 let sub_id = tree
1143 .root()
1144 .and_then(|r| tree.children(r))
1145 .and_then(|c| c.first().copied())
1146 .expect("subcompose node");
1147 let before = tree.children(sub_id).expect("children").to_vec();
1148 let a_node = before[0];
1149 let b_node = before[1];
1150 let c_node = before[2];
1151
1152 let root2 = box_view().with_children(vec![
1155 multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
1156 .modifier(Modifier::new().padding(4.0.dp())),
1157 ]);
1158 tree.update(&root2);
1159
1160 let after = tree.children(sub_id).expect("children after");
1161 assert_eq!(after.len(), 2);
1162 assert_eq!(after[0], a_node);
1164 assert_eq!(after[1], c_node);
1165 assert!(tree.get(b_node).is_none());
1167 }
1168
1169 #[test]
1170 fn test_subcompose_ancestor_modifier_narrows_scope() {
1171 let mut tree = ViewTree::new();
1172 tree.set_subcompose_scope(SubcomposeScope::new(
1173 Dp(0.0),
1174 Dp(1000.0),
1175 Dp(0.0),
1176 Dp(1000.0),
1177 ));
1178
1179 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1180 let cap2 = captured.clone();
1181
1182 let sub = multi_slot_view(move |scope| {
1185 *cap2.lock().unwrap() = *scope;
1186 vec![(0, text_view("hi"))]
1187 });
1188 let root = box_view()
1189 .modifier(Modifier::new().width(200.0.dp()))
1190 .with_children(vec![sub]);
1191
1192 tree.update(&root);
1193
1194 let observed = *captured.lock().unwrap();
1195 assert_eq!(observed.max_width, Dp(200.0));
1196 }
1197
1198 #[test]
1199 fn test_subcompose_chained_ancestor_constraints_intersect() {
1200 let mut tree = ViewTree::new();
1201 tree.set_subcompose_scope(SubcomposeScope::new(
1202 Dp(0.0),
1203 Dp(1000.0),
1204 Dp(0.0),
1205 Dp(1000.0),
1206 ));
1207
1208 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1209 let cap2 = captured.clone();
1210
1211 let sub = multi_slot_view(move |scope| {
1212 *cap2.lock().unwrap() = *scope;
1213 vec![(0, text_view("hi"))]
1214 });
1215 let root = box_view()
1218 .modifier(Modifier::new().width(400.0.dp()))
1219 .with_children(vec![
1220 box_view()
1221 .modifier(Modifier::new().max_width(300.0.dp()))
1222 .with_children(vec![sub]),
1223 ]);
1224
1225 tree.update(&root);
1226
1227 let observed = *captured.lock().unwrap();
1228 assert_eq!(observed.max_width, Dp(300.0));
1229 }
1230
1231 #[test]
1232 #[allow(clippy::arc_with_non_send_sync)]
1234 fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
1235 let mut tree = ViewTree::new();
1236 tree.set_subcompose_scope(SubcomposeScope::new(
1237 Dp(0.0),
1238 Dp(1000.0),
1239 Dp(0.0),
1240 Dp(1000.0),
1241 ));
1242
1243 let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1244 let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1245 let outer2 = outer_captured.clone();
1246 let inner2 = inner_captured.clone();
1247
1248 let inner = Arc::new(multi_slot_view(move |scope| {
1251 *inner2.lock().unwrap() = *scope;
1252 vec![(0, text_view("inner"))]
1253 }));
1254 let inner_clone = inner.clone();
1255 let outer = multi_slot_view(move |scope| {
1256 *outer2.lock().unwrap() = *scope;
1257 vec![(0, (*inner_clone).clone())]
1258 })
1259 .modifier(Modifier::new().width(400.0.dp()));
1260 let root = box_view().with_children(vec![outer]);
1261
1262 tree.update(&root);
1263
1264 let outer_obs = *outer_captured.lock().unwrap();
1265 let inner_obs = *inner_captured.lock().unwrap();
1266 assert_eq!(outer_obs.max_width, Dp(400.0));
1267 assert_eq!(inner_obs.max_width, Dp(400.0));
1268 }
1269}