1use crate::{
4 hash::{SubtreeHashBuilder, SubtreeHashes, ViewHashes, hash_view_facets},
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::rc::Rc;
14use std::sync::Arc;
15
16pub struct ViewTree {
18 nodes: SlotMap<NodeId, TreeNode>,
20
21 root: Option<NodeId>,
23
24 dirty: FxHashSet<NodeId>,
26 dirty_propagated: FxHashSet<NodeId>,
27
28 paint_dirty: FxHashSet<NodeId>,
29 paint_dirty_propagated: FxHashSet<NodeId>,
30
31 generation: u64,
33
34 view_id_map: FxHashMap<ViewId, NodeId>,
36
37 view_id_members: FxHashMap<ViewId, SmallVec<[NodeId; 2]>>,
40
41 duplicate_view_ids: FxHashSet<ViewId>,
42
43 pub stats: TreeStats,
45
46 pub removed_ids: Vec<NodeId>,
48
49 pending_removals: Vec<NodeId>,
50 removal_seen: FxHashSet<NodeId>,
51
52 subcompose_scope: SubcomposeScope,
56
57 subcompose_cache: FxHashMap<NodeId, (SubcomposeScope, Arc<Vec<View>>)>,
62
63 scope_invalidation_seen: FxHashSet<NodeId>,
64}
65
66impl Default for ViewTree {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72fn rc_identity_changed<T: ?Sized>(old: &Option<Rc<T>>, new: &Option<Rc<T>>) -> bool {
73 match (old, new) {
74 (Some(old), Some(new)) => !Rc::ptr_eq(old, new),
75 (None, None) => false,
76 _ => true,
77 }
78}
79
80fn arc_identity_changed<T: ?Sized>(old: &Option<Arc<T>>, new: &Option<Arc<T>>) -> bool {
81 match (old, new) {
82 (Some(old), Some(new)) => !Arc::ptr_eq(old, new),
83 (None, None) => false,
84 _ => true,
85 }
86}
87
88#[derive(Clone, Copy, Default)]
89struct IdentityChange {
90 paint: bool,
91 layout: bool,
92 subcompose: bool,
93}
94
95impl IdentityChange {
96 fn merge(&mut self, other: Self) {
97 self.paint |= other.paint;
98 self.layout |= other.layout;
99 self.subcompose |= other.subcompose;
100 }
101}
102
103fn focus_requester_changed(
104 old: &Option<repose_core::FocusRequester>,
105 new: &Option<repose_core::FocusRequester>,
106) -> bool {
107 match (old, new) {
108 (Some(old), Some(new)) => !Rc::ptr_eq(&old.target, &new.target),
109 (None, None) => false,
110 _ => true,
111 }
112}
113
114fn interaction_source_changed(
115 old: &Option<repose_core::InteractionSource>,
116 new: &Option<repose_core::InteractionSource>,
117) -> bool {
118 match (old, new) {
119 (Some(old), Some(new)) => old.stable_id() != new.stable_id(),
120 (None, None) => false,
121 _ => true,
122 }
123}
124
125fn text_selection_changed(
126 old: &Option<repose_core::text::TextSelection>,
127 new: &Option<repose_core::text::TextSelection>,
128) -> bool {
129 match (old, new) {
130 (Some(old), Some(new)) => old.stable_id() != new.stable_id(),
131 (None, None) => false,
132 _ => true,
133 }
134}
135
136fn scroll_axis_identity_changed(
137 old: &repose_core::scroll::ScrollAxisBinding,
138 new: &repose_core::scroll::ScrollAxisBinding,
139) -> bool {
140 rc_identity_changed(&old.on_scroll, &new.on_scroll)
141 || rc_identity_changed(&old.set_viewport_main, &new.set_viewport_main)
142 || rc_identity_changed(&old.set_content_main, &new.set_content_main)
143 || rc_identity_changed(&old.get_offset_main, &new.get_offset_main)
144 || rc_identity_changed(&old.set_offset_main, &new.set_offset_main)
145 || rc_identity_changed(&old.tick, &new.tick)
146 || rc_identity_changed(&old.set_nested_scroll_parent, &new.set_nested_scroll_parent)
147}
148
149fn scroll_identity_changed(
150 old: &Option<repose_core::ScrollBinding>,
151 new: &Option<repose_core::ScrollBinding>,
152) -> bool {
153 match (old, new) {
154 (
155 Some(repose_core::ScrollBinding::Vertical(old)),
156 Some(repose_core::ScrollBinding::Vertical(new)),
157 ) => scroll_axis_identity_changed(old, new),
158 (
159 Some(repose_core::ScrollBinding::Horizontal(old)),
160 Some(repose_core::ScrollBinding::Horizontal(new)),
161 ) => scroll_axis_identity_changed(old, new),
162 (
163 Some(repose_core::ScrollBinding::Both(old)),
164 Some(repose_core::ScrollBinding::Both(new)),
165 ) => {
166 rc_identity_changed(&old.on_scroll, &new.on_scroll)
167 || rc_identity_changed(&old.set_viewport_width, &new.set_viewport_width)
168 || rc_identity_changed(&old.set_viewport_height, &new.set_viewport_height)
169 || rc_identity_changed(&old.set_content_width, &new.set_content_width)
170 || rc_identity_changed(&old.set_content_height, &new.set_content_height)
171 || rc_identity_changed(&old.get_offset_xy, &new.get_offset_xy)
172 || rc_identity_changed(&old.set_offset_xy, &new.set_offset_xy)
173 || rc_identity_changed(&old.tick, &new.tick)
174 || rc_identity_changed(&old.set_nested_scroll_parent, &new.set_nested_scroll_parent)
175 }
176 (None, None) => false,
177 _ => true,
178 }
179}
180
181fn cursor_identity_changed(
182 old: &Option<repose_core::CursorIcon>,
183 new: &Option<repose_core::CursorIcon>,
184) -> bool {
185 match (old, new) {
186 (
187 Some(repose_core::CursorIcon::Custom(old)),
188 Some(repose_core::CursorIcon::Custom(new)),
189 ) => !std::sync::Arc::ptr_eq(old, new) || old != new,
190 (None, None) => false,
191 _ => true,
192 }
193}
194
195fn text_input_identity_change(
196 old: Option<&repose_core::TextInputConfig>,
197 new: Option<&repose_core::TextInputConfig>,
198) -> IdentityChange {
199 let (Some(old), Some(new)) = (old, new) else {
200 return IdentityChange {
201 paint: old.is_some() != new.is_some(),
202 layout: old.is_some() != new.is_some(),
203 subcompose: false,
204 };
205 };
206 let mut change = IdentityChange::default();
207 if rc_identity_changed(&old.on_change, &new.on_change)
208 || rc_identity_changed(&old.on_submit, &new.on_submit)
209 || rc_identity_changed(&old.on_text_layout, &new.on_text_layout)
210 || rc_identity_changed(&old.focus_tracker, &new.focus_tracker)
211 || interaction_source_changed(&old.interaction_source, &new.interaction_source)
212 {
213 change.paint = true;
214 }
215 if rc_identity_changed(&old.visual_transformation, &new.visual_transformation) {
216 change.paint = true;
217 change.layout = true;
218 }
219 match (&old.keyboard_actions, &new.keyboard_actions) {
220 (Some(old), Some(new)) => {
221 if rc_identity_changed(&old.on_done, &new.on_done)
222 || rc_identity_changed(&old.on_go, &new.on_go)
223 || rc_identity_changed(&old.on_next, &new.on_next)
224 || rc_identity_changed(&old.on_previous, &new.on_previous)
225 || rc_identity_changed(&old.on_search, &new.on_search)
226 || rc_identity_changed(&old.on_send, &new.on_send)
227 {
228 change.paint = true;
229 }
230 }
231 (None, None) => {}
232 _ => change.paint = true,
233 }
234 change
235}
236
237fn modifier_identity_change(old: &Modifier, new: &Modifier) -> IdentityChange {
238 let mut change = IdentityChange::default();
239 macro_rules! paint_changed {
240 ($($field:ident),+ $(,)?) => {
241 if false $(|| rc_identity_changed(&old.$field, &new.$field))+ {
242 change.paint = true;
243 }
244 };
245 }
246 macro_rules! layout_changed {
247 ($($field:ident),+ $(,)?) => {
248 if false $(|| rc_identity_changed(&old.$field, &new.$field))+ {
249 change.layout = true;
250 }
251 };
252 }
253 paint_changed!(
254 on_scroll,
255 on_pointer_down,
256 on_pointer_move,
257 on_pointer_up,
258 on_pointer_cancel,
259 on_pointer_enter,
260 on_pointer_leave,
261 on_click,
262 on_double_click,
263 on_long_click,
264 on_globally_positioned,
265 on_size_changed,
266 on_key_event,
267 on_preview_key_event,
268 on_ime,
269 on_drag_start,
270 on_drag_end,
271 on_drag_enter,
272 on_drag_over,
273 on_drag_leave,
274 on_drop,
275 on_action,
276 painter,
277 indication,
278 drag_preview,
279 on_focus_changed,
280 );
281 layout_changed!(layout);
282 if arc_identity_changed(&old.paint_callback, &new.paint_callback) {
283 change.paint = true;
284 }
285 if focus_requester_changed(&old.focus_requester, &new.focus_requester)
286 || interaction_source_changed(&old.interaction_source, &new.interaction_source)
287 || text_selection_changed(&old.text_selection, &new.text_selection)
288 {
289 change.paint = true;
290 }
291 change.merge(text_input_identity_change(
292 old.text_input.as_ref(),
293 new.text_input.as_ref(),
294 ));
295 match (&old.nested_scroll_connection, &new.nested_scroll_connection) {
296 (Some(old), Some(new)) => {
297 if rc_identity_changed(&old.on_pre_scroll, &new.on_pre_scroll)
298 || rc_identity_changed(&old.on_post_scroll, &new.on_post_scroll)
299 || rc_identity_changed(&old.on_pre_fling, &new.on_pre_fling)
300 || rc_identity_changed(&old.on_post_fling, &new.on_post_fling)
301 {
302 change.paint = true;
303 }
304 }
305 (None, None) => {}
306 _ => change.paint = true,
307 }
308 if scroll_identity_changed(&old.scroll, &new.scroll)
309 || cursor_identity_changed(&old.cursor, &new.cursor)
310 || old.scrollbar_style != new.scrollbar_style
311 {
312 change.paint = true;
313 }
314 change
315}
316
317fn view_identity_change(node: &TreeNode, view: &View) -> IdentityChange {
318 let mut change = modifier_identity_change(&node.modifier, &view.modifier);
319 if let (
320 ViewKind::SubcomposeLayout { content: old },
321 ViewKind::SubcomposeLayout { content: new },
322 ) = (&node.kind, &view.kind)
323 && !Arc::ptr_eq(old, new)
324 {
325 change.paint = true;
326 change.layout = true;
327 change.subcompose = true;
328 }
329 change
330}
331
332fn semantics_equal(
333 old: Option<&repose_core::Semantics>,
334 new: Option<&repose_core::Semantics>,
335) -> bool {
336 match (old, new) {
337 (Some(old), Some(new)) => {
338 old.role == new.role
339 && old.label == new.label
340 && old.focused == new.focused
341 && old.enabled == new.enabled
342 && old.selectable_group == new.selectable_group
343 && old.checked == new.checked
344 && old.selected == new.selected
345 && old.value == new.value
346 }
347 (None, None) => true,
348 _ => false,
349 }
350}
351
352fn view_kind_equal(old: &ViewKind, new: &ViewKind) -> bool {
353 match (old, new) {
354 (
355 ViewKind::SubcomposeLayout { content: old },
356 ViewKind::SubcomposeLayout { content: new },
357 ) => Arc::ptr_eq(old, new),
358 (
359 ViewKind::Text {
360 text: old_text,
361 color: old_color,
362 font_size: old_font_size,
363 soft_wrap: old_soft_wrap,
364 max_lines: old_max_lines,
365 overflow: old_overflow,
366 font_family: old_font_family,
367 annotations: old_annotations,
368 text_align: old_text_align,
369 font_weight: old_font_weight,
370 font_style: old_font_style,
371 text_decoration: old_text_decoration,
372 letter_spacing: old_letter_spacing,
373 line_height: old_line_height,
374 url: old_url,
375 font_variation_settings: old_font_variation_settings,
376 draw_style: old_draw_style,
377 },
378 ViewKind::Text {
379 text: new_text,
380 color: new_color,
381 font_size: new_font_size,
382 soft_wrap: new_soft_wrap,
383 max_lines: new_max_lines,
384 overflow: new_overflow,
385 font_family: new_font_family,
386 annotations: new_annotations,
387 text_align: new_text_align,
388 font_weight: new_font_weight,
389 font_style: new_font_style,
390 text_decoration: new_text_decoration,
391 letter_spacing: new_letter_spacing,
392 line_height: new_line_height,
393 url: new_url,
394 font_variation_settings: new_font_variation_settings,
395 draw_style: new_draw_style,
396 },
397 ) => {
398 old_text == new_text
399 && old_color == new_color
400 && old_font_size == new_font_size
401 && old_soft_wrap == new_soft_wrap
402 && old_max_lines == new_max_lines
403 && old_overflow == new_overflow
404 && old_font_family == new_font_family
405 && old_annotations == new_annotations
406 && old_text_align == new_text_align
407 && old_font_weight == new_font_weight
408 && old_font_style == new_font_style
409 && old_text_decoration == new_text_decoration
410 && old_letter_spacing == new_letter_spacing
411 && old_line_height == new_line_height
412 && old_url == new_url
413 && old_font_variation_settings == new_font_variation_settings
414 && old_draw_style == new_draw_style
415 }
416 (
417 ViewKind::Image {
418 handle: old_handle,
419 tint: old_tint,
420 fit: old_fit,
421 filter: old_filter,
422 source_rect: old_source_rect,
423 alignment: old_alignment,
424 },
425 ViewKind::Image {
426 handle: new_handle,
427 tint: new_tint,
428 fit: new_fit,
429 filter: new_filter,
430 source_rect: new_source_rect,
431 alignment: new_alignment,
432 },
433 ) => {
434 old_handle == new_handle
435 && old_tint == new_tint
436 && old_fit == new_fit
437 && old_filter == new_filter
438 && old_source_rect == new_source_rect
439 && old_alignment == new_alignment
440 }
441 _ => std::mem::discriminant(old) == std::mem::discriminant(new),
442 }
443}
444
445fn next_dedup_key(
446 original: u64,
447 index: usize,
448 parent_id: NodeId,
449 new_seen_keys: &mut FxHashSet<u64>,
450 keyed_children: &FxHashMap<u64, NodeId>,
451) -> u64 {
452 let mut candidate = (original.wrapping_mul(0x9E3779B97F4A7C15)
453 ^ (index as u64).wrapping_add(0xBF58476D1CE4E5B9))
454 .wrapping_add(parent_id.data().as_ffi());
455 loop {
456 if candidate != original
457 && !new_seen_keys.contains(&candidate)
458 && !keyed_children.contains_key(&candidate)
459 {
460 new_seen_keys.insert(candidate);
461 return candidate;
462 }
463 candidate = candidate.wrapping_add(1);
464 if candidate == 0 {
465 candidate = 1;
466 }
467 }
468}
469
470impl ViewTree {
471 pub fn new() -> Self {
473 Self {
474 nodes: SlotMap::with_key(),
475 root: None,
476 dirty: FxHashSet::default(),
477 dirty_propagated: FxHashSet::default(),
478 paint_dirty: FxHashSet::default(),
479 paint_dirty_propagated: FxHashSet::default(),
480 generation: 0,
481 view_id_map: FxHashMap::default(),
482 view_id_members: FxHashMap::default(),
483 duplicate_view_ids: FxHashSet::default(),
484 stats: TreeStats::default(),
485 removed_ids: Vec::new(),
486 pending_removals: Vec::new(),
487 removal_seen: FxHashSet::default(),
488 subcompose_scope: SubcomposeScope::UNBOUNDED,
489 subcompose_cache: FxHashMap::default(),
490 scope_invalidation_seen: FxHashSet::default(),
491 }
492 }
493
494 pub fn set_subcompose_scope(&mut self, scope: SubcomposeScope) {
500 self.subcompose_scope = SubcomposeScope::new(
501 scope.min_width,
502 scope.max_width,
503 scope.min_height,
504 scope.max_height,
505 );
506 }
507
508 pub fn subcompose_scope(&self) -> SubcomposeScope {
510 self.subcompose_scope
511 }
512
513 fn run_subcompose(
529 &mut self,
530 node_id: NodeId,
531 content: &Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
532 ) -> Arc<Vec<View>> {
533 let raw_scope = self.compute_scope_for_node(node_id);
534 let scope = SubcomposeScope::new(
535 raw_scope.min_width,
536 raw_scope.max_width,
537 raw_scope.min_height,
538 raw_scope.max_height,
539 );
540 if let Some((cached_scope, cached_slots)) = self.subcompose_cache.get(&node_id)
541 && *cached_scope == scope
542 {
543 return Arc::clone(cached_slots);
544 }
545 let slots = content(&scope);
546 let scope_key = format!("subcompose_{:?}", node_id);
547 let mut views = Vec::with_capacity(slots.len());
548 for (slot_id, mut view) in slots {
549 view.modifier.key = Some(slot_id);
550 view.scope_key = Some(scope_key.clone());
551 view.modifier.repaint_boundary = true;
552 views.push(view);
553 }
554 let views = Arc::new(views);
555 let old_cache = self
556 .subcompose_cache
557 .insert(node_id, (scope, Arc::clone(&views)));
558 drop(old_cache);
559 views
560 }
561
562 fn compute_scope_for_node(&self, node_id: NodeId) -> SubcomposeScope {
573 let mut scope = self.subcompose_scope;
574 let mut chain: SmallVec<[NodeId; 16]> = SmallVec::new();
575 let mut current = Some(node_id);
576 while let Some(id) = current {
577 chain.push(id);
578 match self.nodes.get(id) {
579 Some(node) => current = node.parent,
580 None => break,
581 }
582 }
583 chain.reverse();
584 for &ancestor_id in &chain {
585 if let Some(node) = self.nodes.get(ancestor_id) {
586 scope = intersect_scope_with_modifier(scope, &node.modifier);
587 if ancestor_id == node_id {
588 continue;
589 }
590 if let Some(cache) = &node.layout_cache {
591 let w = cache.rect.w;
592 if w >= 0.0 && w.is_finite() {
593 scope.max_width = scope.max_width.min(repose_core::Dp(w));
594 }
595 let h = cache.rect.h;
596 if h >= 0.0 && h.is_finite() {
597 scope.max_height = scope.max_height.min(repose_core::Dp(h));
598 }
599 }
600 }
601 }
602 scope
603 }
604
605 pub fn invalidate_subcompose_cache(&mut self, node_id: NodeId) {
609 let removed = self.subcompose_cache.remove(&node_id);
610 drop(removed);
611 }
612
613 pub fn generation(&self) -> u64 {
615 self.generation
616 }
617
618 pub fn root(&self) -> Option<NodeId> {
620 self.root
621 }
622
623 pub fn get(&self, id: NodeId) -> Option<&TreeNode> {
625 self.nodes.get(id)
626 }
627
628 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut TreeNode> {
630 self.nodes.get_mut(id)
631 }
632
633 pub fn get_by_view_id(&self, view_id: ViewId) -> Option<&TreeNode> {
635 self.view_id_map
636 .get(&view_id)
637 .and_then(|id| self.nodes.get(*id))
638 }
639
640 pub fn len(&self) -> usize {
642 self.nodes.len()
643 }
644
645 pub fn is_empty(&self) -> bool {
647 self.nodes.is_empty()
648 }
649
650 pub fn is_dirty(&self, id: NodeId) -> bool {
652 self.dirty.contains(&id)
653 }
654
655 pub fn dirty_nodes(&self) -> &FxHashSet<NodeId> {
657 &self.dirty
658 }
659
660 pub fn clear_dirty(&mut self) {
662 self.dirty.clear();
663 self.dirty_propagated.clear();
664 }
665
666 pub fn paint_dirty_nodes(&self) -> &FxHashSet<NodeId> {
667 &self.paint_dirty
668 }
669
670 pub fn clear_paint_dirty(&mut self) {
671 self.paint_dirty.clear();
672 self.paint_dirty_propagated.clear();
673 }
674
675 fn mark_paint_dirty(&mut self, node_id: NodeId) {
676 let mut current = Some(node_id);
677 while let Some(id) = current {
678 if !self.paint_dirty_propagated.insert(id) {
679 break;
680 }
681 self.paint_dirty.insert(id);
682 current = self.nodes.get(id).and_then(|node| node.parent);
683 }
684 }
685
686 fn mark_scope_descendants_dirty(&mut self, id: NodeId) {
687 if !self.scope_invalidation_seen.insert(id) {
688 return;
689 }
690 let mut stack: SmallVec<[NodeId; 16]> = SmallVec::new();
691 stack.push(id);
692 while let Some(current) = stack.pop() {
693 let Some(node) = self.nodes.get(current) else {
694 continue;
695 };
696 for &child in &node.children {
697 if self
698 .nodes
699 .get(child)
700 .is_some_and(|child| child.scope_key.is_some())
701 {
702 self.dirty.insert(child);
703 }
704 stack.push(child);
705 }
706 }
707 }
708
709 pub fn mark_dirty(&mut self, id: NodeId) {
711 let mut current = Some(id);
712 while let Some(current_id) = current {
713 if !self.dirty_propagated.insert(current_id) {
714 break;
715 }
716 self.dirty.insert(current_id);
717 current = self.nodes.get(current_id).and_then(|node| node.parent);
718 }
719
720 let needs_scope_invalidate = self.nodes.get(id).is_some_and(|node| {
721 node.modifier.transform.is_some()
722 || node.modifier.alpha.is_some()
723 || node.modifier.graphics_layer.is_some()
724 });
725 if needs_scope_invalidate {
726 self.mark_scope_descendants_dirty(id);
727 }
728 }
729
730 pub fn update(&mut self, new_root: &View) -> NodeId {
733 self.removed_ids.clear();
734 self.pending_removals.clear();
735 self.removal_seen.clear();
736 self.paint_dirty.clear();
737 self.paint_dirty_propagated.clear();
738 self.dirty_propagated.clear();
739 self.scope_invalidation_seen.clear();
740
741 self.generation += 1;
742 self.stats = TreeStats::default();
743
744 let mut ctx = ReconcileContext::new(self.generation);
745
746 let root_id = if let Some(existing_root) = self.root {
747 self.reconcile_node(existing_root, new_root, None, 0, 0, &mut ctx)
748 } else {
749 self.create_node(new_root, None, 0, 0, &mut ctx)
750 };
751
752 self.root = Some(root_id);
753 self.collect_explicit_removals(&mut ctx);
754
755 self.stats.total_nodes = self.nodes.len();
756 self.stats.dirty_nodes = self.dirty.len();
757 self.stats.reconciled_nodes = ctx.reconciled;
758 self.stats.skipped_nodes = ctx.skipped;
759 self.stats.created_nodes = ctx.created;
760 self.stats.removed_nodes = ctx.removed;
761
762 root_id
763 }
764
765 fn reconcile_node(
767 &mut self,
768 node_id: NodeId,
769 view: &View,
770 parent: Option<NodeId>,
771 depth: u32,
772 index_in_parent: u32,
773 ctx: &mut ReconcileContext,
774 ) -> NodeId {
775 let hashes = hash_view_facets(view);
776 let Some(node) = self.nodes.get(node_id) else {
777 log::error!(
778 "reconcile_node: node {:?} not found - creating fresh",
779 node_id
780 );
781 return self.create_node(view, parent, depth, index_in_parent, ctx);
782 };
783
784 let old_hashes = ViewHashes {
785 layout: node.layout_hash,
786 measurement: node.measurement_hash,
787 paint: node.paint_hash,
788 semantics: node.semantics_hash,
789 };
790 let identity = view_identity_change(node, view);
791 let old_subtree = SubtreeHashes {
792 layout: node.layout_subtree_hash,
793 measurement: node.measurement_subtree_hash,
794 paint: node.paint_subtree_hash,
795 semantics: node.semantics_subtree_hash,
796 };
797 let old_subtree_hash = node.subtree_hash;
798 let old_scope_key = node.scope_key.clone();
799 let old_view_id = node.view_id;
800 let old_is_subcompose = matches!(&node.kind, ViewKind::SubcomposeLayout { .. });
801 let new_is_subcompose = matches!(&view.kind, ViewKind::SubcomposeLayout { .. });
802 let hashes_equal = old_hashes == hashes;
803 let content_hash = hashes.combined();
804 let content_changed = !hashes_equal;
805 let view_semantics_changed =
806 !hashes_equal && !semantics_equal(node.semantics.as_ref(), view.semantics.as_ref());
807 let modifier_semantics_changed = !hashes_equal
808 && !semantics_equal(
809 node.modifier.semantics.as_ref(),
810 view.modifier.semantics.as_ref(),
811 );
812 let scope_effect_changed = node.modifier.transform != view.modifier.transform
813 || node.modifier.alpha != view.modifier.alpha
814 || node.modifier.graphics_layer != view.modifier.graphics_layer;
815 let kind_changed = !view_kind_equal(&node.kind, &view.kind);
816 let modifier_changed = old_hashes.layout != hashes.layout
817 || old_hashes.measurement != hashes.measurement
818 || old_hashes.paint != hashes.paint
819 || modifier_semantics_changed
820 || identity.paint
821 || identity.layout;
822 let scope_changed = old_scope_key.as_deref() != view.scope_key.as_deref();
823 let node_layout_changed = old_hashes.layout != hashes.layout
824 || old_hashes.measurement != hashes.measurement
825 || identity.layout
826 || identity.subcompose
827 || old_is_subcompose != new_is_subcompose;
828 let subcompose_changed = identity.subcompose
829 || (new_is_subcompose
830 && (old_hashes.layout != hashes.layout
831 || old_hashes.measurement != hashes.measurement
832 || identity.layout))
833 || old_is_subcompose != new_is_subcompose;
834 if subcompose_changed {
835 self.invalidate_subcompose_cache(node_id);
836 }
837
838 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
839 self.assign_view_id(
840 node_id,
841 old_view_id,
842 view_id,
843 view.id == 0,
844 "reconcile_node",
845 );
846
847 {
848 let Some(node) = self.nodes.get_mut(node_id) else {
849 log::error!("reconcile_node: node {:?} vanished mid-reconcile", node_id);
850 return node_id;
851 };
852 node.parent = parent;
853 node.depth = depth;
854 node.generation = self.generation;
855 if kind_changed {
856 node.kind = view.kind.clone();
857 }
858 if modifier_changed {
859 node.modifier = view.modifier.clone();
860 }
861 if view_semantics_changed {
862 node.semantics = view.semantics.clone();
863 }
864 if scope_changed {
865 node.scope_key = view.scope_key.clone();
866 }
867 node.user_key = view.modifier.key;
868 if node_layout_changed {
869 node.layout_cache = None;
870 }
871 }
872
873 let new_children_hashes = if let ViewKind::SubcomposeLayout { content } = &view.kind {
874 let subcomposed = self.run_subcompose(node_id, content);
875 self.reconcile_children(node_id, subcomposed.as_slice(), depth, ctx)
876 } else {
877 self.reconcile_children(node_id, &view.children, depth, ctx)
878 };
879 let new_subtree = new_children_hashes.finish(hashes);
880 let new_subtree_hash = new_subtree.combined();
881 let layout_subtree_changed = old_subtree.layout != new_subtree.layout
882 || old_subtree.measurement != new_subtree.measurement
883 || identity.layout
884 || identity.subcompose;
885 let paint_subtree_changed =
886 old_subtree.paint != new_subtree.paint || identity.paint || identity.subcompose;
887 let semantics_subtree_changed = old_subtree.semantics != new_subtree.semantics;
888 let subtree_changed = old_subtree_hash != new_subtree_hash || identity.subcompose;
889
890 {
891 let Some(node) = self.nodes.get_mut(node_id) else {
892 log::error!("reconcile_node: node {:?} vanished mid-reconcile", node_id);
893 return node_id;
894 };
895 if layout_subtree_changed {
896 node.layout_cache = None;
897 }
898 node.content_hash = content_hash;
899 node.layout_hash = hashes.layout;
900 node.measurement_hash = hashes.measurement;
901 node.paint_hash = hashes.paint;
902 node.semantics_hash = hashes.semantics;
903 node.subtree_hash = new_subtree_hash;
904 node.layout_subtree_hash = new_subtree.layout;
905 node.measurement_subtree_hash = new_subtree.measurement;
906 node.paint_subtree_hash = new_subtree.paint;
907 node.semantics_subtree_hash = new_subtree.semantics;
908
909 if content_changed || identity.paint || identity.layout || identity.subcompose {
910 ctx.reconciled += 1;
911 } else if !subtree_changed {
912 ctx.skipped += 1;
913 }
914 }
915
916 if paint_subtree_changed || semantics_subtree_changed {
917 self.mark_paint_dirty(node_id);
918 }
919 if layout_subtree_changed {
920 self.mark_dirty(node_id);
921 }
922 if scope_effect_changed {
923 self.mark_scope_descendants_dirty(node_id);
924 }
925
926 node_id
927 }
928 fn node_subtree_hashes(&self, node_id: NodeId) -> SubtreeHashes {
929 self.nodes
930 .get(node_id)
931 .map_or_default(|node| SubtreeHashes {
932 layout: node.layout_subtree_hash,
933 measurement: node.measurement_subtree_hash,
934 paint: node.paint_subtree_hash,
935 semantics: node.semantics_subtree_hash,
936 })
937 }
938
939 fn reconcile_children(
942 &mut self,
943 parent_id: NodeId,
944 new_children: &[View],
945 parent_depth: u32,
946 ctx: &mut ReconcileContext,
947 ) -> SubtreeHashBuilder {
948 let child_depth = parent_depth + 1;
949 let old_children: SmallVec<[NodeId; 4]> = self
950 .nodes
951 .get_mut(parent_id)
952 .map(|node| std::mem::take(&mut node.children))
953 .unwrap_or_default();
954
955 let mut keyed_children: FxHashMap<u64, NodeId> = FxHashMap::default();
956 let mut unkeyed_children: SmallVec<[NodeId; 4]> = SmallVec::new();
957 for &child_id in &old_children {
958 if let Some(node) = self.nodes.get(child_id) {
959 match node.user_key {
960 Some(key) => {
961 keyed_children.entry(key).or_insert(child_id);
962 }
963 None => unkeyed_children.push(child_id),
964 }
965 }
966 }
967
968 let mut new_child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
969 let mut child_hashes = SubtreeHashBuilder::new(new_children.len());
970 let mut unkeyed_index = 0;
971 let mut used_nodes: FxHashSet<NodeId> = FxHashSet::default();
972 let mut new_seen_keys: FxHashSet<u64> = FxHashSet::default();
973
974 for (i, new_child) in new_children.iter().enumerate() {
975 let mut owned_child: Option<View>;
976 let child_ref = if let Some(key) = new_child.modifier.key {
977 if new_seen_keys.insert(key) {
978 new_child
979 } else {
980 log::error!(
981 "reconcile_children: duplicate modifier.key={} in children of node {:?} - deduplicating (suffixing). Ensure get_key returns unique keys.",
982 key,
983 parent_id
984 );
985 let salt =
986 next_dedup_key(key, i, parent_id, &mut new_seen_keys, &keyed_children);
987 owned_child = Some(new_child.clone());
988 let child = owned_child.as_mut().expect("owned child inserted");
989 child.modifier.key = Some(salt);
990 child
991 }
992 } else {
993 new_child
994 };
995
996 let is_subcompose = matches!(&child_ref.kind, ViewKind::SubcomposeLayout { .. });
997 let child_id = if let Some(key) = child_ref.modifier.key {
998 if let Some(&existing_id) = keyed_children.get(&key) {
999 if used_nodes.insert(existing_id) {
1000 self.reconcile_node(
1001 existing_id,
1002 child_ref,
1003 Some(parent_id),
1004 child_depth,
1005 i as u32,
1006 ctx,
1007 )
1008 } else {
1009 let salt =
1010 next_dedup_key(key, i, parent_id, &mut new_seen_keys, &keyed_children);
1011 let mut fresh = child_ref.clone();
1012 fresh.modifier.key = Some(salt);
1013 self.create_node(&fresh, Some(parent_id), child_depth, i as u32, ctx)
1014 }
1015 } else if is_subcompose && unkeyed_index < unkeyed_children.len() {
1016 let existing_id = unkeyed_children[unkeyed_index];
1017 unkeyed_index += 1;
1018 if used_nodes.insert(existing_id) {
1019 self.reconcile_node(
1020 existing_id,
1021 child_ref,
1022 Some(parent_id),
1023 child_depth,
1024 i as u32,
1025 ctx,
1026 )
1027 } else {
1028 self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1029 }
1030 } else {
1031 self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1032 }
1033 } else if let Some(&existing_id) = unkeyed_children.get(unkeyed_index) {
1034 unkeyed_index += 1;
1035 if used_nodes.insert(existing_id) {
1036 self.reconcile_node(
1037 existing_id,
1038 child_ref,
1039 Some(parent_id),
1040 child_depth,
1041 i as u32,
1042 ctx,
1043 )
1044 } else {
1045 self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1046 }
1047 } else {
1048 self.create_node(child_ref, Some(parent_id), child_depth, i as u32, ctx)
1049 };
1050
1051 new_child_ids.push(child_id);
1052 child_hashes.push(self.node_subtree_hashes(child_id));
1053 }
1054
1055 for &old_child in &old_children {
1056 if !used_nodes.contains(&old_child) {
1057 self.mark_for_removal(old_child);
1058 }
1059 }
1060
1061 if let Some(parent) = self.nodes.get_mut(parent_id) {
1062 parent.children = new_child_ids;
1063 }
1064
1065 child_hashes
1066 }
1067
1068 fn create_node(
1070 &mut self,
1071 view: &View,
1072 parent: Option<NodeId>,
1073 depth: u32,
1074 index_in_parent: u32,
1075 ctx: &mut ReconcileContext,
1076 ) -> NodeId {
1077 let hashes = hash_view_facets(view);
1078 let content_hash = hashes.combined();
1079
1080 let node_id = self.nodes.insert_with_key(|id| {
1081 TreeNode::new(
1082 id,
1083 0,
1084 view.kind.clone(),
1085 view.modifier.clone(),
1086 self.generation,
1087 )
1088 });
1089 ctx.created += 1;
1090
1091 {
1092 let node = self
1093 .nodes
1094 .get_mut(node_id)
1095 .expect("create_node: node just inserted");
1096 node.parent = parent;
1097 node.depth = depth;
1098 node.semantics = view.semantics.clone();
1099 node.content_hash = content_hash;
1100 node.layout_hash = hashes.layout;
1101 node.measurement_hash = hashes.measurement;
1102 node.paint_hash = hashes.paint;
1103 node.semantics_hash = hashes.semantics;
1104 node.user_key = view.modifier.key;
1105 node.scope_key = view.scope_key.clone();
1106 }
1107
1108 let view_id = self.compute_view_id(view, node_id, parent, index_in_parent);
1109 self.assign_view_id(node_id, 0, view_id, view.id == 0, "create_node");
1110
1111 let child_depth = depth + 1;
1112 let mut child_ids: SmallVec<[NodeId; 4]> = SmallVec::new();
1113 let subcomposed = if let ViewKind::SubcomposeLayout { content } = &view.kind {
1114 Some(self.run_subcompose(node_id, content))
1115 } else {
1116 None
1117 };
1118 let children_to_create = subcomposed
1119 .as_ref()
1120 .map_or(view.children.as_slice(), |views| views.as_slice());
1121 let mut child_hashes = SubtreeHashBuilder::new(children_to_create.len());
1122 for (i, child_view) in children_to_create.iter().enumerate() {
1123 let child_id = self.create_node(child_view, Some(node_id), child_depth, i as u32, ctx);
1124 child_ids.push(child_id);
1125 child_hashes.push(self.node_subtree_hashes(child_id));
1126 }
1127
1128 let subtree = child_hashes.finish(hashes);
1129 let subtree_hash = subtree.combined();
1130
1131 let node = self
1132 .nodes
1133 .get_mut(node_id)
1134 .expect("create_node: node just inserted");
1135 node.children = child_ids;
1136 node.subtree_hash = subtree_hash;
1137 node.layout_subtree_hash = subtree.layout;
1138 node.measurement_subtree_hash = subtree.measurement;
1139 node.paint_subtree_hash = subtree.paint;
1140 node.semantics_subtree_hash = subtree.semantics;
1141 self.dirty.insert(node_id);
1142
1143 node_id
1144 }
1145 fn compute_view_id(
1147 &self,
1148 view: &View,
1149 _node_id: NodeId,
1150 parent: Option<NodeId>,
1151 index_in_parent: u32,
1152 ) -> ViewId {
1153 if view.id != 0 {
1154 return view.id;
1155 }
1156
1157 let parent_id = parent
1158 .and_then(|p| self.nodes.get(p))
1159 .map(|n| n.view_id)
1160 .unwrap_or(0);
1161
1162 let salt = match view.modifier.key {
1163 Some(key) => key.wrapping_mul(2).wrapping_add(1),
1164 None => (index_in_parent as u64).wrapping_mul(2),
1165 };
1166
1167 let mut id = parent_id.wrapping_mul(31).wrapping_add(salt);
1168 id = id.wrapping_mul(0x9E3779B97F4A7C15);
1169 id ^= id >> 30;
1170
1171 if id == 0 {
1172 id = 1;
1173 }
1174
1175 id
1176 }
1177
1178 fn sync_view_id_owner(&mut self, view_id: ViewId) {
1182 let Some(members) = self.view_id_members.get(&view_id) else {
1183 self.view_id_map.remove(&view_id);
1184 self.duplicate_view_ids.remove(&view_id);
1185 return;
1186 };
1187 match members.split_first() {
1188 Some((owner, rest)) => {
1189 self.view_id_map.insert(view_id, *owner);
1190 if rest.is_empty() {
1191 self.duplicate_view_ids.remove(&view_id);
1192 } else {
1193 self.duplicate_view_ids.insert(view_id);
1194 }
1195 }
1196 None => {
1197 self.view_id_members.remove(&view_id);
1198 self.view_id_map.remove(&view_id);
1199 self.duplicate_view_ids.remove(&view_id);
1200 }
1201 }
1202 }
1203
1204 fn claim_view_id(&mut self, node_id: NodeId, view_id: ViewId) {
1205 let members = self.view_id_members.entry(view_id).or_default();
1206 if !members.contains(&node_id) {
1207 members.push(node_id);
1208 }
1209 self.sync_view_id_owner(view_id);
1210 }
1211
1212 fn release_view_id(&mut self, node_id: NodeId, view_id: ViewId) {
1213 let empty = match self.view_id_members.get_mut(&view_id) {
1214 Some(members) => {
1215 if let Some(index) = members.iter().position(|member| *member == node_id) {
1216 members.remove(index);
1217 }
1218 members.is_empty()
1219 }
1220 None => return,
1221 };
1222 if empty {
1223 self.view_id_members.remove(&view_id);
1224 }
1225 self.sync_view_id_owner(view_id);
1226 }
1227
1228 fn assign_view_id(
1229 &mut self,
1230 node_id: NodeId,
1231 old_view_id: ViewId,
1232 requested_view_id: ViewId,
1233 generated: bool,
1234 phase: &str,
1235 ) {
1236 let view_id = if generated {
1237 self.available_generated_view_id(node_id, requested_view_id)
1238 } else {
1239 requested_view_id
1240 };
1241
1242 if old_view_id == view_id && self.view_id_map.get(&view_id).copied() == Some(node_id) {
1243 return;
1244 }
1245
1246 if old_view_id != 0 && old_view_id != view_id {
1247 self.release_view_id(node_id, old_view_id);
1248 }
1249
1250 if let Some(existing) = self
1251 .view_id_members
1252 .get(&view_id)
1253 .and_then(|m| m.first())
1254 .copied()
1255 && existing != node_id
1256 {
1257 log::error!(
1258 "{}: duplicate View.id {}; keeping the first node {:?}",
1259 phase,
1260 view_id,
1261 existing
1262 );
1263 }
1264
1265 self.claim_view_id(node_id, view_id);
1266
1267 if let Some(node) = self.nodes.get_mut(node_id) {
1268 node.view_id = view_id;
1269 }
1270 }
1271
1272 fn available_generated_view_id(&self, node_id: NodeId, requested: ViewId) -> ViewId {
1273 if self
1274 .view_id_map
1275 .get(&requested)
1276 .is_none_or(|existing| *existing == node_id)
1277 {
1278 return requested;
1279 }
1280
1281 let mut candidate = requested;
1282 loop {
1283 candidate = candidate.wrapping_add(1);
1284 if candidate == 0 {
1285 candidate = 1;
1286 }
1287 if self
1288 .view_id_map
1289 .get(&candidate)
1290 .is_none_or(|existing| *existing == node_id)
1291 {
1292 return candidate;
1293 }
1294 }
1295 }
1296
1297 fn mark_for_removal(&mut self, node_id: NodeId) {
1299 if !self.removal_seen.insert(node_id) {
1300 return;
1301 }
1302 let Some((view_id, children)) = self.nodes.get_mut(node_id).map(|node| {
1303 node.generation = 0;
1304 let children = std::mem::take(&mut node.children);
1305 (node.view_id, children)
1306 }) else {
1307 return;
1308 };
1309 self.pending_removals.push(node_id);
1310 self.release_view_id(node_id, view_id);
1311 self.dirty.remove(&node_id);
1312 self.dirty_propagated.remove(&node_id);
1313 self.paint_dirty.remove(&node_id);
1314 self.paint_dirty_propagated.remove(&node_id);
1315 self.scope_invalidation_seen.remove(&node_id);
1316 let removed_cache = self.subcompose_cache.remove(&node_id);
1317 drop(removed_cache);
1318 for child_id in children {
1319 self.mark_for_removal(child_id);
1320 }
1321 }
1322
1323 fn collect_explicit_removals(&mut self, ctx: &mut ReconcileContext) {
1324 let pending = std::mem::take(&mut self.pending_removals);
1325 for node_id in pending {
1326 if self.nodes.remove(node_id).is_none() {
1327 continue;
1328 }
1329 ctx.removed += 1;
1330 self.removed_ids.push(node_id);
1331 }
1332 self.removal_seen.clear();
1333 }
1334
1335 pub fn set_layout(
1337 &mut self,
1338 id: NodeId,
1339 rect: Rect,
1340 screen_rect: Rect,
1341 constraints: LayoutConstraints,
1342 ) {
1343 if let Some(node) = self.nodes.get_mut(id) {
1344 node.layout_cache = Some(LayoutCache {
1345 rect,
1346 screen_rect,
1347 constraints,
1348 generation: self.generation,
1349 });
1350 }
1351 }
1352
1353 pub fn iter(&self) -> impl Iterator<Item = &TreeNode> {
1355 self.nodes.values()
1356 }
1357
1358 pub fn iter_with_ids(&self) -> impl Iterator<Item = (NodeId, &TreeNode)> {
1360 self.nodes.iter()
1361 }
1362
1363 pub fn walk<F>(&self, mut f: F)
1366 where
1367 F: FnMut(&TreeNode, u32) -> bool,
1368 {
1369 if let Some(root_id) = self.root {
1370 self.walk_node(root_id, 0, &mut f);
1371 }
1372 }
1373
1374 fn walk_node<F>(&self, id: NodeId, depth: u32, f: &mut F)
1375 where
1376 F: FnMut(&TreeNode, u32) -> bool,
1377 {
1378 if let Some(node) = self.nodes.get(id) {
1379 if !f(node, depth) {
1380 return;
1381 }
1382
1383 for &child_id in &node.children {
1384 self.walk_node(child_id, depth + 1, f);
1385 }
1386 }
1387 }
1388
1389 pub fn children(&self, id: NodeId) -> Option<&[NodeId]> {
1391 self.nodes.get(id).map(|n| n.children.as_slice())
1392 }
1393}
1394
1395fn intersect_scope_with_modifier(scope: SubcomposeScope, modifier: &Modifier) -> SubcomposeScope {
1403 let mut s = scope;
1404 if let Some(sz) = modifier.size {
1406 s.min_width = s.min_width.max(sz.width);
1407 s.max_width = s.max_width.min(sz.width);
1408 s.min_height = s.min_height.max(sz.height);
1409 s.max_height = s.max_height.min(sz.height);
1410 }
1411 if let Some(w) = modifier.width {
1412 s.min_width = s.min_width.max(w);
1413 s.max_width = s.max_width.min(w);
1414 }
1415 if let Some(h) = modifier.height {
1416 s.min_height = s.min_height.max(h);
1417 s.max_height = s.max_height.min(h);
1418 }
1419 if let Some(mw) = modifier.min_width {
1420 s.min_width = s.min_width.max(mw);
1421 }
1422 if let Some(mh) = modifier.min_height {
1423 s.min_height = s.min_height.max(mh);
1424 }
1425 if let Some(mw) = modifier.max_width {
1426 s.max_width = s.max_width.min(mw);
1427 }
1428 if let Some(mh) = modifier.max_height {
1429 s.max_height = s.max_height.min(mh);
1430 }
1431 if let Some(p) = modifier.padding {
1434 let total = p * 2.0;
1435 s.min_width = (s.min_width - total).max(repose_core::Dp::ZERO);
1436 s.max_width = (s.max_width - total).max(repose_core::Dp::ZERO);
1437 s.min_height = (s.min_height - total).max(repose_core::Dp::ZERO);
1438 s.max_height = (s.max_height - total).max(repose_core::Dp::ZERO);
1439 }
1440 if let Some(pv) = modifier.padding_values {
1441 let h_total = pv.left + pv.right;
1442 let v_total = pv.top + pv.bottom;
1443 s.min_width = (s.min_width - h_total).max(repose_core::Dp::ZERO);
1444 s.max_width = (s.max_width - h_total).max(repose_core::Dp::ZERO);
1445 s.min_height = (s.min_height - v_total).max(repose_core::Dp::ZERO);
1446 s.max_height = (s.max_height - v_total).max(repose_core::Dp::ZERO);
1447 }
1448 if let Some(required) = modifier.required_size {
1449 s.min_width = required.width.max(repose_core::Dp::ZERO);
1450 s.max_width = s.min_width;
1451 s.min_height = required.height.max(repose_core::Dp::ZERO);
1452 s.max_height = s.min_height;
1453 }
1454 if let Some(value) = modifier.required_min_width {
1455 s.min_width = value.max(repose_core::Dp::ZERO);
1456 if s.min_width > s.max_width {
1457 s.max_width = s.min_width;
1458 }
1459 }
1460 if let Some(value) = modifier.required_max_width {
1461 s.max_width = value.max(repose_core::Dp::ZERO);
1462 if s.min_width > s.max_width {
1463 s.min_width = s.max_width;
1464 }
1465 }
1466 if let Some(value) = modifier.required_min_height {
1467 s.min_height = value.max(repose_core::Dp::ZERO);
1468 if s.min_height > s.max_height {
1469 s.max_height = s.min_height;
1470 }
1471 }
1472 if let Some(value) = modifier.required_max_height {
1473 s.max_height = value.max(repose_core::Dp::ZERO);
1474 if s.min_height > s.max_height {
1475 s.min_height = s.max_height;
1476 }
1477 }
1478 if s.min_width > s.max_width {
1479 let coerced = s.max_width.max(repose_core::Dp::ZERO);
1480 s.min_width = coerced;
1481 s.max_width = coerced;
1482 }
1483 if s.min_height > s.max_height {
1484 let coerced = s.max_height.max(repose_core::Dp::ZERO);
1485 s.min_height = coerced;
1486 s.max_height = coerced;
1487 }
1488 SubcomposeScope::new(s.min_width, s.max_width, s.min_height, s.max_height)
1489}
1490
1491#[cfg(test)]
1492mod tests {
1493 use super::*;
1494 use repose_core::{
1495 Color, Dp, DrawStyle, FontStyle, FontWeight, Modifier, Sp, SubcomposeScope, TextAlign,
1496 TextDecoration, UnitExt, View, ViewKind,
1497 };
1498 use std::rc::Rc;
1499 use std::sync::Arc;
1500
1501 fn text_view(text: &str) -> View {
1502 View::new(
1503 0,
1504 ViewKind::Text {
1505 text: text.to_string(),
1506 color: Color::WHITE,
1507 font_size: 16.0.sp(),
1508 soft_wrap: true,
1509 max_lines: None,
1510 overflow: repose_core::TextOverflow::Visible,
1511 font_family: None,
1512 annotations: None,
1513 text_align: TextAlign::Unspecified,
1514 font_weight: FontWeight::NORMAL,
1515 font_style: FontStyle::Normal,
1516 text_decoration: TextDecoration::default(),
1517 letter_spacing: Sp::ZERO,
1518 line_height: Sp::ZERO,
1519 url: None,
1520 font_variation_settings: None,
1521 draw_style: DrawStyle::Fill,
1522 },
1523 )
1524 }
1525
1526 fn box_view() -> View {
1527 View::new(0, ViewKind::Box)
1528 }
1529
1530 #[test]
1531 fn test_create_tree() {
1532 let mut tree = ViewTree::new();
1533
1534 let root = box_view().with_children(vec![text_view("Hello"), text_view("World")]);
1535
1536 tree.update(&root);
1537
1538 assert_eq!(tree.len(), 3); assert!(tree.root().is_some());
1540 }
1541
1542 #[test]
1543 fn test_unchanged_tree_skips() {
1544 let mut tree = ViewTree::new();
1545
1546 let root = box_view().with_children(vec![text_view("Hello")]);
1547
1548 tree.update(&root);
1549 let gen1 = tree.generation();
1550
1551 tree.update(&root);
1552 let gen2 = tree.generation();
1553
1554 assert_eq!(gen2, gen1 + 1);
1555 assert!(tree.stats.skipped_nodes > 0);
1556 }
1557
1558 #[test]
1559 fn test_changed_content_reconciles() {
1560 let mut tree = ViewTree::new();
1561
1562 let root1 = box_view().with_children(vec![text_view("Hello")]);
1563
1564 tree.update(&root1);
1565
1566 let root2 = box_view().with_children(vec![text_view("Changed")]);
1567
1568 tree.update(&root2);
1569
1570 assert!(tree.stats.reconciled_nodes > 0);
1571 }
1572
1573 #[test]
1574 fn generated_view_ids_are_unique_and_stable() {
1575 let mut tree = ViewTree::new();
1576 let branch = |text: &str| box_view().with_children(vec![text_view(text)]);
1577 let root = box_view().with_children(vec![
1578 text_view("keyed").modifier(Modifier::new().key(1)),
1579 text_view("unkeyed"),
1580 branch("left"),
1581 branch("right"),
1582 ]);
1583
1584 tree.update(&root);
1585 let first: Vec<(NodeId, u64)> = tree
1586 .iter_with_ids()
1587 .map(|(id, node)| (id, node.view_id))
1588 .collect();
1589 let unique: std::collections::HashSet<u64> = first.iter().map(|(_, id)| *id).collect();
1590 assert_eq!(unique.len(), first.len());
1591 for (node_id, view_id) in &first {
1592 assert_eq!(
1593 tree.get_by_view_id(*view_id).map(|node| node.id),
1594 Some(*node_id)
1595 );
1596 }
1597
1598 tree.update(&root);
1599 let second: Vec<(NodeId, u64)> = tree
1600 .iter_with_ids()
1601 .map(|(id, node)| (id, node.view_id))
1602 .collect();
1603 assert_eq!(first, second);
1604 }
1605
1606 #[test]
1607 fn duplicate_explicit_ids_keep_first_lookup_stable() {
1608 let mut tree = ViewTree::new();
1609 let mut first = text_view("first").modifier(Modifier::new().key(1));
1610 let mut second = text_view("second").modifier(Modifier::new().key(2));
1611 first.id = 42;
1612 second.id = 42;
1613 let root = box_view().with_children(vec![first.clone(), second.clone()]);
1614 let root_id = tree.update(&root);
1615 let first_id = tree.children(root_id).unwrap()[0];
1616 assert_eq!(tree.get_by_view_id(42).map(|node| node.id), Some(first_id));
1617 let root = box_view().with_children(vec![second]);
1618 tree.update(&root);
1619 let second_id = tree.children(tree.root().unwrap()).unwrap()[0];
1620 assert_eq!(tree.get_by_view_id(42).map(|node| node.id), Some(second_id));
1621 }
1622
1623 #[test]
1624 fn replaced_text_selection_is_picked_up() {
1625 let mut tree = ViewTree::new();
1626 let first = repose_core::text::TextSelection::default();
1627 let second = repose_core::text::TextSelection::default();
1628 assert_ne!(first.stable_id(), second.stable_id());
1629
1630 let root = box_view().with_children(vec![
1631 text_view("hi").modifier(Modifier::new().text_selection(first.clone())),
1632 ]);
1633 let node = tree.update(&root);
1634 let node = tree.children(node).unwrap()[0];
1635 tree.set_layout(
1636 node,
1637 Rect::default(),
1638 Rect::default(),
1639 crate::LayoutConstraints::default(),
1640 );
1641 tree.update(&root);
1642 assert!(tree.get(node).unwrap().has_valid_layout());
1643
1644 let replaced = box_view().with_children(vec![
1645 text_view("hi").modifier(Modifier::new().text_selection(second.clone())),
1646 ]);
1647 tree.update(&replaced);
1648
1649 let stored = &tree.get(node).unwrap().modifier.text_selection;
1650 assert_eq!(
1651 stored.as_ref().map(|selection| selection.stable_id()),
1652 Some(second.stable_id())
1653 );
1654 assert!(tree.paint_dirty_nodes().contains(&node));
1655 assert!(tree.get(node).unwrap().has_valid_layout());
1656 }
1657
1658 #[test]
1659 fn explicit_id_change_releases_the_old_id() {
1660 let mut tree = ViewTree::new();
1661 let mut view = text_view("first").modifier(Modifier::new().key(1));
1662 view.id = 7;
1663 let root = box_view().with_children(vec![view.clone()]);
1664 let child = tree.update(&root);
1665 let child = tree.children(child).unwrap()[0];
1666 assert_eq!(tree.get_by_view_id(7).map(|node| node.id), Some(child));
1667
1668 let mut renamed = view;
1669 renamed.id = 9;
1670 tree.update(&box_view().with_children(vec![renamed]));
1671 let child = tree.children(tree.root().unwrap()).unwrap()[0];
1672
1673 assert_eq!(tree.get_by_view_id(7).map(|node| node.id), None);
1674 assert_eq!(tree.get_by_view_id(9).map(|node| node.id), Some(child));
1675 }
1676
1677 #[test]
1678 fn test_keyed_children_stable() {
1679 let mut tree = ViewTree::new();
1680
1681 let root1 = box_view().with_children(vec![
1683 text_view("A").modifier(Modifier::new().key(1)),
1684 text_view("B").modifier(Modifier::new().key(2)),
1685 text_view("C").modifier(Modifier::new().key(3)),
1686 ]);
1687
1688 tree.update(&root1);
1689
1690 let _b_view_id = tree
1691 .root()
1692 .and_then(|r| tree.children(r))
1693 .and_then(|c| c.get(1).copied())
1694 .and_then(|id| tree.get(id))
1695 .map(|n| n.view_id);
1696
1697 let root2 = box_view().with_children(vec![
1699 text_view("C").modifier(Modifier::new().key(3)),
1700 text_view("A").modifier(Modifier::new().key(1)),
1701 text_view("B").modifier(Modifier::new().key(2)),
1702 ]);
1703
1704 tree.update(&root2);
1705
1706 assert_eq!(tree.len(), 4); }
1710
1711 fn subcompose_view<F>(f: F) -> View
1712 where
1713 F: Fn(&SubcomposeScope) -> View + 'static,
1714 {
1715 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> =
1716 Arc::new(move |scope| vec![(0, f(scope))]);
1717 View {
1718 id: 0,
1719 kind: ViewKind::SubcomposeLayout { content },
1720 modifier: Modifier::default(),
1721 children: Vec::new(),
1722 scope_key: None,
1723 semantics: None,
1724 }
1725 }
1726
1727 #[test]
1728 fn callback_identity_updates_handlers_without_layout_reset() {
1729 let mut tree = ViewTree::new();
1730 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1731 let content_calls = calls.clone();
1732 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(move |_| {
1733 content_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1734 vec![(0, text_view("callback"))]
1735 });
1736 let make = |callback: Rc<dyn Fn()>| {
1737 let callback = callback.clone();
1738 View {
1739 id: 0,
1740 kind: ViewKind::SubcomposeLayout {
1741 content: content.clone(),
1742 },
1743 modifier: Modifier::new().on_click(move || callback()),
1744 children: Vec::new(),
1745 scope_key: None,
1746 semantics: None,
1747 }
1748 };
1749 let root = make(Rc::new(|| {}));
1750 let root_id = tree.update(&root);
1751 tree.set_layout(
1752 root_id,
1753 Rect {
1754 x: 0.0,
1755 y: 0.0,
1756 w: 100.0,
1757 h: 100.0,
1758 },
1759 Rect {
1760 x: 0.0,
1761 y: 0.0,
1762 w: 100.0,
1763 h: 100.0,
1764 },
1765 crate::LayoutConstraints::default(),
1766 );
1767 let root = make(Rc::new(|| {}));
1768 tree.update(&root);
1769 let calls_before = calls.load(std::sync::atomic::Ordering::SeqCst);
1770 tree.update(&root);
1771 assert!(tree.get(root_id).unwrap().has_valid_layout());
1772 assert_eq!(
1773 calls.load(std::sync::atomic::Ordering::SeqCst),
1774 calls_before
1775 );
1776 let calls_before = calls.load(std::sync::atomic::Ordering::SeqCst);
1777 let root = make(Rc::new(|| {}));
1778 tree.update(&root);
1779 assert!(tree.get(root_id).unwrap().has_valid_layout());
1780 assert_eq!(
1781 calls.load(std::sync::atomic::Ordering::SeqCst),
1782 calls_before
1783 );
1784 assert!(tree.paint_dirty_nodes().contains(&root_id));
1785 }
1786
1787 #[test]
1788 fn test_subcompose_invokes_content() {
1789 let mut tree = ViewTree::new();
1790 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1791 let counter2 = counter.clone();
1792
1793 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
1794 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1795 text_view("from subcompose")
1796 })]);
1797
1798 tree.update(&root);
1799
1800 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1801 assert_eq!(tree.len(), 3); }
1803
1804 #[test]
1805 fn test_subcompose_receives_scope() {
1806 let mut tree = ViewTree::new();
1807 let captured = Arc::new(std::sync::Mutex::new(None));
1808 let captured2 = captured.clone();
1809
1810 let root = box_view().with_children(vec![subcompose_view(move |scope| {
1811 *captured2.lock().unwrap() = Some(*scope);
1812 text_view("hi")
1813 })]);
1814
1815 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(360.0), Dp(0.0), Dp(640.0)));
1816 tree.update(&root);
1817
1818 let observed = captured.lock().unwrap().expect("scope captured");
1819 assert_eq!(observed.max_width, Dp(360.0));
1820 assert_eq!(observed.max_height, Dp(640.0));
1821 assert_eq!(observed.min_width, Dp(0.0));
1822 assert_eq!(observed.min_height, Dp(0.0));
1823 }
1824
1825 #[test]
1826 fn zero_subcompose_bounds_are_real() {
1827 let mut tree = ViewTree::new();
1828 let captured = Arc::new(std::sync::Mutex::new(None));
1829 let captured2 = captured.clone();
1830 let root = box_view().with_children(vec![subcompose_view(move |scope| {
1831 *captured2.lock().unwrap() = Some(*scope);
1832 text_view("zero")
1833 })]);
1834 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(0.0), Dp(0.0), Dp(0.0)));
1835 tree.update(&root);
1836 let scope = captured.lock().unwrap().unwrap();
1837 assert_eq!(scope.min_width, Dp(0.0));
1838 assert_eq!(scope.max_width, Dp(0.0));
1839 assert_eq!(scope.min_height, Dp(0.0));
1840 assert_eq!(scope.max_height, Dp(0.0));
1841 }
1842
1843 #[test]
1844 fn test_subcompose_re_invokes_on_update() {
1845 let mut tree = ViewTree::new();
1846 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1847 let counter2 = counter.clone();
1848
1849 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
1850 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1851 text_view("hi")
1852 })]);
1853
1854 tree.update(&root);
1855 tree.update(&root);
1856 tree.update(&root);
1857
1858 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1861 }
1862
1863 #[test]
1864 fn test_subcompose_reruns_on_scope_change() {
1865 let mut tree = ViewTree::new();
1866 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1867 let counter2 = counter.clone();
1868
1869 let root = box_view().with_children(vec![subcompose_view(move |_scope| {
1870 counter2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1871 text_view("hi")
1872 })]);
1873
1874 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
1875 tree.update(&root);
1876 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1877
1878 tree.update(&root);
1880 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1881
1882 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(200.0), Dp(0.0), Dp(200.0)));
1884 tree.update(&root);
1885 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1886 }
1887
1888 #[test]
1889 fn test_subcompose_settles_on_final_scope() {
1890 let mut tree = ViewTree::new();
1891 let root = box_view().with_children(vec![subcompose_view(|scope| {
1892 text_view("content").modifier(Modifier::new().width(scope.max_width))
1893 })]);
1894
1895 for width in [100.0f32, 200.0, 300.0, 400.0, 500.0, 500.0, 500.0, 500.0] {
1896 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(width), Dp(0.0), Dp(width)));
1897 tree.update(&root);
1898 }
1899
1900 let sub_id = tree.children(tree.root().unwrap()).unwrap()[0];
1901 let child_id = tree.children(sub_id).unwrap()[0];
1902 assert_eq!(
1903 tree.get(child_id).unwrap().modifier.width,
1904 Some(Dp(500.0)),
1905 "closure must end up holding the scope that settled"
1906 );
1907 }
1908
1909 #[test]
1910 fn test_subcompose_scope_ignores_own_measured_size() {
1911 let mut tree = ViewTree::new();
1912 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
1913 let captured2 = captured.clone();
1914 let root = box_view().with_children(vec![subcompose_view(move |scope| {
1915 *captured2.lock().unwrap() = *scope;
1916 text_view("hi")
1917 })]);
1918
1919 tree.set_subcompose_scope(SubcomposeScope::new(
1920 Dp(0.0),
1921 Dp(1000.0),
1922 Dp(0.0),
1923 Dp(1000.0),
1924 ));
1925 let root_id = tree.update(&root);
1926 let sub_id = tree.children(root_id).unwrap()[0];
1927
1928 let narrow = Rect {
1929 x: 0.0,
1930 y: 0.0,
1931 w: 120.0,
1932 h: 40.0,
1933 };
1934 tree.set_layout(sub_id, narrow, narrow, crate::LayoutConstraints::default());
1935 tree.update(&root);
1936
1937 assert_eq!(captured.lock().unwrap().max_width, Dp(1000.0));
1938 }
1939
1940 #[test]
1941 fn test_subcompose_reruns_on_content_change() {
1942 let mut tree = ViewTree::new();
1943 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1944 let c1 = counter.clone();
1945
1946 let root1 = box_view().with_children(vec![subcompose_view(move |_scope| {
1947 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1948 text_view("hi")
1949 })]);
1950
1951 tree.update(&root1);
1952 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1953
1954 tree.update(&root1);
1956 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1957
1958 let c2 = counter.clone();
1960 let root2 = box_view().with_children(vec![
1961 subcompose_view(move |_scope| {
1962 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1963 text_view("hi")
1964 })
1965 .modifier(Modifier::new().padding(4.0.dp())),
1966 ]);
1967
1968 tree.update(&root2);
1969 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1970 }
1971
1972 #[test]
1973 fn test_subcompose_cache_drops_on_node_removal() {
1974 let mut tree = ViewTree::new();
1975 tree.set_subcompose_scope(SubcomposeScope::new(Dp(0.0), Dp(100.0), Dp(0.0), Dp(100.0)));
1976
1977 let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1979 let c1 = counter.clone();
1980 let root_with_sub = box_view().with_children(vec![subcompose_view(move |_scope| {
1981 c1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1982 text_view("hi")
1983 })]);
1984
1985 tree.update(&root_with_sub);
1986 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1987
1988 let root_no_sub = box_view().with_children(vec![text_view("plain")]);
1991 tree.update(&root_no_sub);
1992 assert_eq!(tree.len(), 2);
1993
1994 let c2 = counter.clone();
1997 let root_with_sub_again = box_view().with_children(vec![subcompose_view(move |_scope| {
1998 c2.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1999 text_view("hi")
2000 })]);
2001
2002 tree.update(&root_with_sub_again);
2003 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
2004 }
2005
2006 fn multi_slot_view<F>(f: F) -> View
2007 where
2008 F: Fn(&SubcomposeScope) -> Vec<(u64, View)> + 'static,
2009 {
2010 let content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>> = Arc::new(f);
2011 View {
2012 id: 0,
2013 kind: ViewKind::SubcomposeLayout { content },
2014 modifier: Modifier::default(),
2015 children: Vec::new(),
2016 scope_key: None,
2017 semantics: None,
2018 }
2019 }
2020
2021 #[test]
2022 fn test_subcompose_multi_slot_produces_multiple_children() {
2023 let mut tree = ViewTree::new();
2024 let root = box_view().with_children(vec![multi_slot_view(|_scope| {
2025 vec![
2026 (0, text_view("a")),
2027 (1, text_view("b")),
2028 (2, text_view("c")),
2029 ]
2030 })]);
2031
2032 tree.update(&root);
2033
2034 assert_eq!(tree.len(), 5);
2036 let sub_id = tree
2037 .root()
2038 .and_then(|r| tree.children(r))
2039 .and_then(|c| c.first().copied())
2040 .expect("subcompose node");
2041 let sub_children = tree.children(sub_id).expect("subcompose has children");
2042 assert_eq!(sub_children.len(), 3);
2043 }
2044
2045 #[test]
2046 fn test_subcompose_multi_slot_preserves_identity_across_removal() {
2047 let mut tree = ViewTree::new();
2048
2049 let root3 = box_view().with_children(vec![multi_slot_view(|_scope| {
2051 vec![
2052 (0, text_view("a")),
2053 (1, text_view("b")),
2054 (2, text_view("c")),
2055 ]
2056 })]);
2057 tree.update(&root3);
2058
2059 let sub_id = tree
2060 .root()
2061 .and_then(|r| tree.children(r))
2062 .and_then(|c| c.first().copied())
2063 .expect("subcompose node");
2064 let before = tree.children(sub_id).expect("children").to_vec();
2065 let a_node = before[0];
2066 let b_node = before[1];
2067 let c_node = before[2];
2068
2069 let root2 = box_view().with_children(vec![
2072 multi_slot_view(|_scope| vec![(0, text_view("a")), (2, text_view("c"))])
2073 .modifier(Modifier::new().padding(4.0.dp())),
2074 ]);
2075 tree.update(&root2);
2076
2077 let after = tree.children(sub_id).expect("children after");
2078 assert_eq!(after.len(), 2);
2079 assert_eq!(after[0], a_node);
2081 assert_eq!(after[1], c_node);
2082 assert!(tree.get(b_node).is_none());
2084 }
2085
2086 #[test]
2087 fn test_subcompose_ancestor_modifier_narrows_scope() {
2088 let mut tree = ViewTree::new();
2089 tree.set_subcompose_scope(SubcomposeScope::new(
2090 Dp(0.0),
2091 Dp(1000.0),
2092 Dp(0.0),
2093 Dp(1000.0),
2094 ));
2095
2096 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2097 let cap2 = captured.clone();
2098
2099 let sub = multi_slot_view(move |scope| {
2102 *cap2.lock().unwrap() = *scope;
2103 vec![(0, text_view("hi"))]
2104 });
2105 let root = box_view()
2106 .modifier(Modifier::new().width(200.0.dp()))
2107 .with_children(vec![sub]);
2108
2109 tree.update(&root);
2110
2111 let observed = *captured.lock().unwrap();
2112 assert_eq!(observed.max_width, Dp(200.0));
2113 }
2114
2115 #[test]
2116 fn test_subcompose_chained_ancestor_constraints_intersect() {
2117 let mut tree = ViewTree::new();
2118 tree.set_subcompose_scope(SubcomposeScope::new(
2119 Dp(0.0),
2120 Dp(1000.0),
2121 Dp(0.0),
2122 Dp(1000.0),
2123 ));
2124
2125 let captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2126 let cap2 = captured.clone();
2127
2128 let sub = multi_slot_view(move |scope| {
2129 *cap2.lock().unwrap() = *scope;
2130 vec![(0, text_view("hi"))]
2131 });
2132 let root = box_view()
2135 .modifier(Modifier::new().width(400.0.dp()))
2136 .with_children(vec![
2137 box_view()
2138 .modifier(Modifier::new().max_width(300.0.dp()))
2139 .with_children(vec![sub]),
2140 ]);
2141
2142 tree.update(&root);
2143
2144 let observed = *captured.lock().unwrap();
2145 assert_eq!(observed.max_width, Dp(300.0));
2146 }
2147
2148 #[test]
2149 #[allow(clippy::arc_with_non_send_sync)]
2151 fn test_subcompose_nested_layouts_inherit_narrowed_scope() {
2152 let mut tree = ViewTree::new();
2153 tree.set_subcompose_scope(SubcomposeScope::new(
2154 Dp(0.0),
2155 Dp(1000.0),
2156 Dp(0.0),
2157 Dp(1000.0),
2158 ));
2159
2160 let outer_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2161 let inner_captured = Arc::new(std::sync::Mutex::new(SubcomposeScope::UNBOUNDED));
2162 let outer2 = outer_captured.clone();
2163 let inner2 = inner_captured.clone();
2164
2165 let inner = Arc::new(multi_slot_view(move |scope| {
2168 *inner2.lock().unwrap() = *scope;
2169 vec![(0, text_view("inner"))]
2170 }));
2171 let inner_clone = inner.clone();
2172 let outer = multi_slot_view(move |scope| {
2173 *outer2.lock().unwrap() = *scope;
2174 vec![(0, (*inner_clone).clone())]
2175 })
2176 .modifier(Modifier::new().width(400.0.dp()));
2177 let root = box_view().with_children(vec![outer]);
2178
2179 tree.update(&root);
2180
2181 let outer_obs = *outer_captured.lock().unwrap();
2182 let inner_obs = *inner_captured.lock().unwrap();
2183 assert_eq!(outer_obs.max_width, Dp(400.0));
2184 assert_eq!(inner_obs.max_width, Dp(400.0));
2185 }
2186}