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