1use super::frame::layout_style_for_instance;
2use super::frame::{
3 DismissibleLayerProps, ElementFrame, ElementInstance, ElementRecord, WindowFrame,
4};
5use super::host_widget::ElementHostWidget;
6use super::prelude::*;
7use std::collections::{HashMap, HashSet, VecDeque};
8use std::sync::Arc;
9
10use crate::tree::{UiDebugInvalidationDetail, UiDebugInvalidationSource};
11
12fn keep_alive_view_cache_scratch_disabled() -> bool {
13 crate::runtime_config::ui_runtime_config().keep_alive_view_cache_scratch_disabled
14}
15
16fn validate_element_tree_unique_ids_enabled() -> (bool, bool) {
17 let strict = crate::strict_runtime::strict_runtime_enabled();
18 let cfg = crate::runtime_config::ui_runtime_config();
19 let enabled = strict
20 || cfg.validate_element_tree_unique_ids
21 || cfg.validate_element_tree_unique_ids_panic;
22 let should_panic = strict || cfg.validate_element_tree_unique_ids_panic;
23 (enabled, should_panic)
24}
25
26pub(super) fn element_tree_duplicate_ids(elements: &[AnyElement]) -> Vec<GlobalElementId> {
27 let mut seen: HashSet<GlobalElementId> = HashSet::new();
28 let mut duplicates: HashSet<GlobalElementId> = HashSet::new();
29 let mut stack: Vec<&AnyElement> = Vec::new();
30 stack.extend(elements.iter());
31
32 while let Some(el) = stack.pop() {
33 if !seen.insert(el.id) {
34 duplicates.insert(el.id);
35 }
36 stack.extend(el.children.iter());
37 }
38
39 let mut out: Vec<GlobalElementId> = duplicates.into_iter().collect();
40 out.sort_by_key(|id| id.0);
41 out
42}
43
44enum GcNodeRetentionDecision {
45 Keep,
46 Drop,
47 NeedLayerReachability,
48}
49
50fn gc_node_retention_decision(
51 id: GlobalElementId,
52 entry_node: NodeId,
53 entry_last_seen_frame: &mut FrameId,
54 entry_root: GlobalElementId,
55 root_id: GlobalElementId,
56 frame_id: FrameId,
57 cutoff: u64,
58 keep_alive_view_cache_elements: &HashSet<GlobalElementId>,
59 reachable_from_layers: Option<&HashSet<NodeId>>,
60 reachable_from_view_cache_roots_active: bool,
61 reachable_from_view_cache_roots: &HashSet<NodeId>,
62) -> GcNodeRetentionDecision {
63 if id == root_id {
64 return GcNodeRetentionDecision::Keep;
65 }
66 if entry_root != root_id {
67 return GcNodeRetentionDecision::Keep;
68 }
69 if !keep_alive_view_cache_elements.is_empty() && keep_alive_view_cache_elements.contains(&id) {
70 *entry_last_seen_frame = frame_id;
71 return GcNodeRetentionDecision::Keep;
72 }
73 if entry_last_seen_frame.0 >= cutoff {
74 return GcNodeRetentionDecision::Keep;
75 }
76 if reachable_from_view_cache_roots_active
80 && reachable_from_view_cache_roots.contains(&entry_node)
81 {
82 return GcNodeRetentionDecision::Keep;
83 }
84 let Some(reachable_from_layers) = reachable_from_layers else {
85 return GcNodeRetentionDecision::NeedLayerReachability;
86 };
87 if reachable_from_layers.contains(&entry_node) {
88 return GcNodeRetentionDecision::Keep;
89 }
90 GcNodeRetentionDecision::Drop
91}
92
93fn collect_live_retained_keep_alive_roots<H: UiHost>(
94 ui: &UiTree<H>,
95 window_state: &mut crate::elements::WindowElementState,
96) -> Vec<NodeId> {
97 window_state.retain_retained_virtual_list_keep_alive_roots(|node| ui.node_exists(node));
100 window_state
101 .retained_virtual_list_keep_alive_roots()
102 .collect()
103}
104
105fn element_resolves_live_attached_node<H: UiHost>(
106 ui: &UiTree<H>,
107 window_state: &crate::elements::WindowElementState,
108 element: GlobalElementId,
109) -> bool {
110 let seeded = window_state.node_entry(element).map(|entry| entry.node);
111 ui.resolve_live_attached_node_for_element_seeded(element, seeded)
112 .is_some()
113}
114
115fn collect_keep_alive_view_cache_elements_in_place<H: UiHost>(
116 ui: &UiTree<H>,
117 window_state: &crate::elements::WindowElementState,
118 out: &mut HashSet<GlobalElementId>,
119 visited_roots: &mut HashSet<GlobalElementId>,
120 stack: &mut Vec<GlobalElementId>,
121) {
122 out.clear();
123 visited_roots.clear();
124 stack.clear();
125 stack.extend(window_state.view_cache_reuse_roots());
126
127 while let Some(root) = stack.pop() {
128 if !visited_roots.insert(root) {
129 continue;
130 }
131 if !element_resolves_live_attached_node(ui, window_state, root) {
132 continue;
133 }
134 let Some(elements) = window_state.view_cache_elements_for_root(root) else {
135 continue;
136 };
137 for &element in elements {
138 if !element_resolves_live_attached_node(ui, window_state, element) {
139 continue;
140 }
141 out.insert(element);
142 if !visited_roots.contains(&element)
143 && window_state.view_cache_elements_for_root(element).is_some()
144 {
145 stack.push(element);
146 }
147 }
148 }
149}
150
151fn debug_path_for_element(
152 runtime: &crate::elements::ElementRuntime,
153 window: AppWindowId,
154 element: GlobalElementId,
155) -> Option<String> {
156 #[cfg(feature = "diagnostics")]
157 {
158 runtime.debug_path_for_element(window, element)
159 }
160 #[cfg(not(feature = "diagnostics"))]
161 {
162 let _ = (runtime, window, element);
163 None
164 }
165}
166
167fn validate_element_tree_unique_ids_or_log(
168 window: AppWindowId,
169 root_name: &str,
170 frame_id: fret_runtime::FrameId,
171 runtime: &crate::elements::ElementRuntime,
172 elements: &[AnyElement],
173) {
174 let (enabled, should_panic) = validate_element_tree_unique_ids_enabled();
175 if !enabled {
176 return;
177 }
178
179 let duplicates = element_tree_duplicate_ids(elements);
180 if duplicates.is_empty() {
181 return;
182 }
183
184 let mut msg = String::new();
185 use std::fmt::Write;
186 let _ = writeln!(
187 &mut msg,
188 "duplicate element ids detected while building declarative element tree: window={window:?} root_name={root_name:?} frame_id={}",
189 frame_id.0
190 );
191 for (idx, id) in duplicates.iter().take(12).enumerate() {
192 let path = debug_path_for_element(runtime, window, *id);
193 let _ = writeln!(&mut msg, " {idx}. element={id:?} debug_path={path:?}");
194 }
195 if duplicates.len() > 12 {
196 let _ = writeln!(&mut msg, " ... ({} more)", duplicates.len() - 12);
197 }
198 let _ = writeln!(
199 &mut msg,
200 "hint: this usually means the same AnyElement value was reused in multiple places (e.g. via .clone()), or the same keyed element id was produced twice under one parent"
201 );
202
203 if should_panic {
204 panic!("{msg}");
205 }
206 tracing::error!("{msg}");
207}
208
209#[cfg(feature = "unstable-retained-bridge")]
210#[derive(Default)]
211struct RetainedSubtreeHostState {
212 root: Option<NodeId>,
213}
214
215pub struct RenderRootContext<'a, H: UiHost> {
216 pub ui: &'a mut UiTree<H>,
217 pub app: &'a mut H,
218 pub services: &'a mut dyn fret_core::UiServices,
219 pub window: AppWindowId,
220 pub bounds: Rect,
221}
222
223impl<'a, H: UiHost + 'static> RenderRootContext<'a, H> {
224 pub fn new(
225 ui: &'a mut UiTree<H>,
226 app: &'a mut H,
227 services: &'a mut dyn fret_core::UiServices,
228 window: AppWindowId,
229 bounds: Rect,
230 ) -> Self {
231 Self {
232 ui,
233 app,
234 services,
235 window,
236 bounds,
237 }
238 }
239
240 pub fn render_root<I>(
241 self,
242 root_name: &str,
243 render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
244 ) -> NodeId
245 where
246 I: IntoIterator<Item = AnyElement>,
247 {
248 crate::declarative::render_root(
249 self.ui,
250 self.app,
251 self.services,
252 self.window,
253 self.bounds,
254 root_name,
255 render,
256 )
257 }
258
259 pub fn render_dismissible_root_with_hooks<I>(
260 self,
261 root_name: &str,
262 render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
263 ) -> NodeId
264 where
265 I: IntoIterator<Item = AnyElement>,
266 {
267 crate::declarative::render_dismissible_root_with_hooks(
268 self.ui,
269 self.app,
270 self.services,
271 self.window,
272 self.bounds,
273 root_name,
274 render,
275 )
276 }
277}
278
279pub(crate) fn with_window_frame<H: UiHost, R>(
280 app: &mut H,
281 window: AppWindowId,
282 f: impl FnOnce(Option<&WindowFrame>) -> R,
283) -> R {
284 app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
285 f(frame.windows.get(&window))
286 })
287}
288
289pub(crate) fn node_for_element_in_window_frame<H: UiHost>(
290 app: &mut H,
291 window: AppWindowId,
292 element: GlobalElementId,
293) -> Option<NodeId> {
294 with_window_frame(app, window, |window_frame| {
295 let window_frame = window_frame?;
296 window_frame
297 .instances
298 .iter()
299 .find_map(|(node, record)| (record.element == element).then_some(node))
300 })
301}
302
303#[derive(Clone, Copy)]
304struct StaleNodeRecord {
305 node: NodeId,
306 element: GlobalElementId,
307 #[cfg(feature = "diagnostics")]
308 element_root: GlobalElementId,
309}
310
311fn prepare_window_frame_for_frame(window_frame: &mut WindowFrame, frame_id: FrameId) {
312 if window_frame.frame_id != frame_id {
313 window_frame.frame_id = frame_id;
314 }
315}
316
317fn sync_window_frame_children(window_frame: &mut WindowFrame, parent: NodeId, children: &[NodeId]) {
318 if let Some(prev) = window_frame.children.get(parent)
319 && prev.as_ref() == children
320 {
321 return;
322 }
323 window_frame
324 .children
325 .insert(parent, Arc::<[NodeId]>::from(children));
326}
327
328pub(crate) fn children_for_node_in_window_frame<H: UiHost>(
329 app: &mut H,
330 window: AppWindowId,
331 node: NodeId,
332) -> Vec<NodeId> {
333 with_window_frame(app, window, |window_frame| {
334 window_frame
335 .and_then(|w| w.children.get(node))
336 .map(|children| children.as_ref().to_vec())
337 .unwrap_or_default()
338 })
339}
340
341pub(crate) fn node_contains_in_window_frame<H: UiHost>(
342 app: &mut H,
343 window: AppWindowId,
344 root: NodeId,
345 needle: NodeId,
346) -> bool {
347 if root == needle {
348 return true;
349 }
350
351 let mut stack = vec![root];
352 while let Some(node) = stack.pop() {
353 let children = children_for_node_in_window_frame(app, window, node);
354 for child in children {
355 if child == needle {
356 return true;
357 }
358 stack.push(child);
359 }
360 }
361
362 false
363}
364
365pub fn render_root<H, I>(
369 ui: &mut UiTree<H>,
370 app: &mut H,
371 services: &mut dyn fret_core::UiServices,
372 window: AppWindowId,
373 bounds: Rect,
374 root_name: &str,
375 render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
376) -> NodeId
377where
378 H: UiHost + 'static,
379 I: IntoIterator<Item = AnyElement>,
380{
381 let frame_id = app.frame_id();
382 #[cfg(debug_assertions)]
383 ui.debug_note_declarative_render_root_called(frame_id);
384 let focused = ui.focus();
385 ui.begin_debug_frame_if_needed(frame_id);
386
387 app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, _app| {
390 runtime.prepare_window_for_frame(window, frame_id);
391 let window_state = runtime.for_window_mut(window);
392 window_state.record_committed_viewport_bounds(bounds);
393 if let Some(svc) = _app.global::<fret_core::window::WindowMetricsService>() {
394 let scale_factor = svc.scale_factor(window).unwrap_or(1.0);
395 window_state.record_committed_scale_factor(scale_factor);
396
397 if svc.safe_area_insets_is_known(window) {
398 window_state.record_committed_safe_area_insets(svc.safe_area_insets(window));
399 }
400 if svc.occlusion_insets_is_known(window) {
401 window_state.record_committed_occlusion_insets(svc.occlusion_insets(window));
402 }
403 } else {
404 window_state.record_committed_scale_factor(1.0);
405 }
406 });
407
408 ui.invalidate_scroll_handle_bindings_for_changed_handles(
412 app,
413 crate::layout_pass::LayoutPassKind::Final,
414 false,
415 false,
416 );
417
418 let ui_ref: &UiTree<H> = &*ui;
419 let children: Vec<AnyElement> =
420 app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
421 runtime.prepare_window_for_frame(window, frame_id);
422 let mut should_reuse_view_cache =
423 |node: NodeId| ui_ref.should_reuse_view_cache_node(node);
424 let mut cx = crate::elements::ElementContext::new_for_root_name(
425 app, runtime, window, bounds, root_name,
426 );
427 cx.set_view_cache_should_reuse(&mut should_reuse_view_cache);
428 cx.sync_focused_element_from_focused_node(focused);
429 cx.dismissible_clear_on_dismiss_request();
430 cx.dismissible_clear_on_pointer_move();
431 let built = render(&mut cx);
432 let children = cx.collect_children(built);
433 validate_element_tree_unique_ids_or_log(
434 window, root_name, frame_id, &*runtime, &children,
435 );
436 children
437 });
438
439 let root_node =
440 app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
441 runtime.prepare_window_for_frame(window, frame_id);
442 let lag = runtime.gc_lag_frames();
443 let cutoff = frame_id.0.saturating_sub(lag);
444
445 let window_state = runtime.for_window_mut(window);
446 ui.debug_set_element_children_vec_pool_stats(
447 window_state.element_children_vec_pool_reuses(),
448 window_state.element_children_vec_pool_misses(),
449 );
450 let root_id = crate::elements::global_root(window, root_name);
451 let mut scroll_bindings: Vec<crate::declarative::frame::ScrollHandleBinding> =
452 Vec::new();
453
454 let seeded_root = window_state.node_entry(root_id).map(|e| e.node);
455 let root_node = ui
456 .resolve_reusable_node_for_element_seeded(root_id, seeded_root)
457 .unwrap_or_else(|| {
458 let node = ui.create_node(ElementHostWidget::new(root_id));
459 ui.set_node_element(node, Some(root_id));
460 window_state.set_node_entry(
461 root_id,
462 NodeEntry {
463 node,
464 last_seen_frame: frame_id,
465 root: root_id,
466 },
467 );
468 node
469 });
470 ui.set_node_element(root_node, Some(root_id));
471
472 window_state.set_node_entry(
473 root_id,
474 NodeEntry {
475 node: root_node,
476 last_seen_frame: frame_id,
477 root: root_id,
478 },
479 );
480
481 if ui.node_layer(root_node).is_none() && ui.base_root().is_none() {
488 ui.set_root(root_node);
489 }
490
491 let mut pending_invalidations = ui.take_scratch_pending_invalidations();
492 pending_invalidations.clear();
493 app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
494 let window_frame = frame.windows.entry(window).or_default();
495 prepare_window_frame_for_frame(window_frame, frame_id);
496
497 let mut root_stack = crate::element::StackProps::default();
498 root_stack.layout.size.width = crate::element::Length::Fill;
499 root_stack.layout.size.height = crate::element::Length::Fill;
500 let inserted = window_frame
501 .instances
502 .insert(
503 root_node,
504 ElementRecord {
505 element: root_id,
506 instance: ElementInstance::Stack(root_stack),
507 inherited_foreground: None,
508 inherited_text_style: None,
509 semantics_decoration: None,
510 key_context: None,
511 },
512 )
513 .is_none();
514
515 let mut mounted_children: Vec<NodeId> = Vec::with_capacity(children.len());
516 let mut children = children;
517 for child in children.drain(..) {
518 mounted_children.push(mount_element(
519 ui,
520 window,
521 root_id,
522 frame_id,
523 window_state,
524 window_frame,
525 child,
526 None,
527 &mut scroll_bindings,
528 &mut pending_invalidations,
529 ));
530 }
531 ui.set_children(root_node, mounted_children);
532 sync_window_frame_children(window_frame, root_node, ui.children_ref(root_node));
533 if inserted {
534 window_frame.revision = window_frame.revision.saturating_add(1);
535 }
536 window_state.restore_scratch_element_children_vec(children);
537
538 let retained_virtual_lists = window_state.take_retained_virtual_list_reconciles();
539 if !retained_virtual_lists.is_empty() {
540 reconcile_retained_virtual_list_hosts(
541 ui,
542 _app,
543 window,
544 bounds,
545 root_id,
546 frame_id,
547 window_state,
548 window_frame,
549 &mut scroll_bindings,
550 &mut pending_invalidations,
551 retained_virtual_lists,
552 );
553 }
554 });
555
556 if ui.view_cache_enabled() {
562 let _ = ui.repair_parent_pointers_from_layer_roots();
563 }
564
565 apply_pending_invalidations(ui, &mut pending_invalidations);
566 ui.restore_scratch_pending_invalidations(pending_invalidations);
567
568 if ui.view_cache_enabled() {
569 ui.propagate_auto_sized_view_cache_root_invalidations();
570 }
571
572 for element in window_state.take_notify_for_animation_frame() {
573 let seeded = window_state.node_entry(element).map(|e| e.node);
574 if let Some(node) =
575 ui.resolve_live_attached_node_for_element_seeded(element, seeded)
576 {
577 ui.invalidate_with_source_and_detail(
578 node,
579 Invalidation::Paint,
580 UiDebugInvalidationSource::Notify,
581 UiDebugInvalidationDetail::AnimationFrameRequest,
582 );
583 }
584 }
585
586 crate::declarative::frame::register_scroll_handle_bindings_batch(
587 app,
588 window,
589 frame_id,
590 scroll_bindings,
591 );
592
593 window_state.set_root_bounds(root_id, bounds);
595
596 let mut keep_alive_view_cache_elements: HashSet<GlobalElementId>;
597 let keep_alive_view_cache_elements_from_scratch: bool;
598 if keep_alive_view_cache_scratch_disabled() {
599 keep_alive_view_cache_elements = HashSet::new();
600 keep_alive_view_cache_elements_from_scratch = false;
601 let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
602 let mut stack: Vec<GlobalElementId> = Vec::new();
603 collect_keep_alive_view_cache_elements_in_place(
604 ui,
605 window_state,
606 &mut keep_alive_view_cache_elements,
607 &mut visited_roots,
608 &mut stack,
609 );
610 } else {
611 keep_alive_view_cache_elements =
612 window_state.take_scratch_view_cache_keep_alive_elements();
613 keep_alive_view_cache_elements_from_scratch = true;
614 {
615 let mut visited_roots =
616 window_state.take_scratch_view_cache_keep_alive_visited_roots();
617 let mut stack = window_state.take_scratch_view_cache_keep_alive_stack();
618 collect_keep_alive_view_cache_elements_in_place(
619 ui,
620 window_state,
621 &mut keep_alive_view_cache_elements,
622 &mut visited_roots,
623 &mut stack,
624 );
625 window_state.restore_scratch_view_cache_keep_alive_visited_roots(visited_roots);
626 window_state.restore_scratch_view_cache_keep_alive_stack(stack);
627 }
628 }
629
630 if window_state
634 .view_cache_transitioned_reuse_roots()
635 .next()
636 .is_some()
637 {
638 with_window_frame(app, window, |window_frame| {
639 touch_existing_declarative_subtree_seen(
640 ui,
641 window_state,
642 window_frame,
643 root_id,
644 frame_id,
645 root_node,
646 );
647 });
648 }
649
650 let liveness_roots = ui.all_layer_roots();
667 let keep_alive_roots = collect_live_retained_keep_alive_roots(ui, window_state);
668 let mut stale: Vec<StaleNodeRecord> = Vec::new();
669 let mut reachable_from_layers = ui.take_scratch_gc_reachable_from_layers();
670 reachable_from_layers.clear();
671 let mut reachable_from_layers_computed = false;
672 let view_cache_has_reuse_roots = window_state.view_cache_reuse_roots().next().is_some();
673 let mut reachable_from_view_cache_roots =
674 ui.take_scratch_gc_reachable_from_view_cache_roots();
675 reachable_from_view_cache_roots.clear();
676 let mut reachable_from_view_cache_roots_active = false;
677 let mut gc_stack = ui.take_scratch_gc_stack();
678 gc_stack.clear();
679
680 if view_cache_has_reuse_roots {
681 let view_cache_reuse_roots: Vec<GlobalElementId> =
682 window_state.view_cache_reuse_roots().collect();
683 let view_cache_reuse_root_nodes: Vec<NodeId> = view_cache_reuse_roots
684 .iter()
685 .filter_map(|root| {
686 let seeded = window_state.node_entry(*root).map(|e| e.node);
687 ui.resolve_live_attached_node_for_element_seeded(*root, seeded)
688 })
689 .collect();
690
691 if !view_cache_reuse_root_nodes.is_empty() {
692 with_window_frame(app, window, |window_frame| {
693 collect_reachable_nodes_for_gc_in_place(
694 ui,
695 window_frame,
696 view_cache_reuse_root_nodes.iter().copied(),
697 &mut reachable_from_view_cache_roots,
698 &mut gc_stack,
699 );
700 });
701 }
702
703 for root in view_cache_reuse_roots {
707 if let Some(elements) = window_state.view_cache_elements_for_root(root) {
708 for &element in elements {
709 let seeded = window_state.node_entry(element).map(|entry| entry.node);
710 if let Some(node) =
711 ui.resolve_live_attached_node_for_element_seeded(element, seeded)
712 {
713 reachable_from_view_cache_roots.insert(node);
714 }
715 }
716 }
717 }
718 reachable_from_view_cache_roots_active = true;
719 }
720 window_state.retain_nodes(|id, entry| {
721 let mut decision = gc_node_retention_decision(
722 *id,
723 entry.node,
724 &mut entry.last_seen_frame,
725 entry.root,
726 root_id,
727 frame_id,
728 cutoff,
729 &keep_alive_view_cache_elements,
730 reachable_from_layers_computed.then_some(&reachable_from_layers),
731 reachable_from_view_cache_roots_active,
732 &reachable_from_view_cache_roots,
733 );
734 if matches!(decision, GcNodeRetentionDecision::NeedLayerReachability) {
735 with_window_frame(app, window, |window_frame| {
736 if liveness_roots.is_empty() {
737 collect_reachable_nodes_for_gc_in_place(
738 ui,
739 window_frame,
740 std::iter::once(root_node).chain(keep_alive_roots.iter().copied()),
741 &mut reachable_from_layers,
742 &mut gc_stack,
743 );
744 } else {
745 collect_reachable_nodes_for_gc_in_place(
746 ui,
747 window_frame,
748 liveness_roots
749 .iter()
750 .copied()
751 .chain(keep_alive_roots.iter().copied()),
752 &mut reachable_from_layers,
753 &mut gc_stack,
754 )
755 }
756 });
757 reachable_from_layers_computed = true;
758 decision = gc_node_retention_decision(
759 *id,
760 entry.node,
761 &mut entry.last_seen_frame,
762 entry.root,
763 root_id,
764 frame_id,
765 cutoff,
766 &keep_alive_view_cache_elements,
767 Some(&reachable_from_layers),
768 reachable_from_view_cache_roots_active,
769 &reachable_from_view_cache_roots,
770 );
771 }
772 if matches!(decision, GcNodeRetentionDecision::Keep) {
773 return true;
774 }
775 stale.push(StaleNodeRecord {
776 node: entry.node,
777 element: *id,
778 #[cfg(feature = "diagnostics")]
779 element_root: entry.root,
780 });
781 false
782 });
783
784 for record in &stale {
785 window_state.forget_view_cache_subtree_elements(record.element);
786 }
787
788 for record in stale {
789 let node = record.node;
790 #[cfg(feature = "diagnostics")]
791 if let Some(ctx) = with_window_frame(app, window, |window_frame| {
792 let window_frame = window_frame?;
793 let parent = ui.node_parent(node);
794 let parent_frame_children = parent.and_then(|p| window_frame.children.get(p));
795 let root_reachable_from_layer_roots =
796 reachable_from_layers_computed && reachable_from_layers.contains(&node);
797 let root_reachable_from_view_cache_roots =
798 reachable_from_view_cache_roots_active
799 .then(|| reachable_from_view_cache_roots.contains(&node));
800 let view_cache_reuse_roots: Vec<GlobalElementId> =
801 window_state.view_cache_reuse_roots().collect();
802 let liveness_layer_roots_len =
803 liveness_roots.len().min(u32::MAX as usize) as u32;
804 let view_cache_reuse_roots_len =
805 view_cache_reuse_roots.len().min(u32::MAX as usize) as u32;
806 let view_cache_reuse_root_nodes_len = view_cache_reuse_roots
807 .iter()
808 .filter(|root| window_state.node_entry(**root).is_some())
809 .count()
810 .min(u32::MAX as usize)
811 as u32;
812 let mut path_edge_frame_contains_child: [u8; 16] = [2u8; 16];
813 let mut path_edge_len: u8 = 0;
814 let mut current = Some(node);
815 while let Some(child) = current {
816 let Some(parent) = ui.node_parent(child) else {
817 break;
818 };
819 if (path_edge_len as usize) >= path_edge_frame_contains_child.len() {
820 break;
821 }
822 let contains = window_frame
823 .children
824 .get(parent)
825 .map(|children| children.contains(&child));
826 path_edge_frame_contains_child[path_edge_len as usize] = match contains {
827 Some(true) => 1,
828 Some(false) => 0,
829 None => 2,
830 };
831 path_edge_len = path_edge_len.saturating_add(1);
832 current = Some(parent);
833 }
834 Some(crate::tree::UiDebugRemoveSubtreeFrameContext {
835 parent_frame_children_len: parent_frame_children
836 .map(|v| v.len().min(u32::MAX as usize) as u32),
837 parent_frame_children_contains_root: parent_frame_children
838 .map(|v| v.contains(&node)),
839 root_frame_instance_present: window_frame.instances.contains_key(node),
840 root_frame_children_len: window_frame
841 .children
842 .get(node)
843 .map(|v| v.len().min(u32::MAX as usize) as u32),
844 root_reachable_from_layer_roots,
845 root_reachable_from_view_cache_roots,
846 liveness_layer_roots_len,
847 view_cache_reuse_roots_len,
848 view_cache_reuse_root_nodes_len,
849 trigger_element: Some(record.element),
850 trigger_element_root: Some(record.element_root),
851 trigger_element_in_view_cache_keep_alive: Some(
852 keep_alive_view_cache_elements.contains(&record.element),
853 ),
854 trigger_element_listed_under_reuse_root: window_state
855 .view_cache_reuse_roots()
856 .find(|&root| {
857 window_state
858 .view_cache_elements_for_root(root)
859 .is_some_and(|elements| elements.contains(&record.element))
860 }),
861 path_edge_len,
862 path_edge_frame_contains_child,
863 })
864 }) {
865 ui.debug_set_remove_subtree_frame_context(node, ctx);
866 }
867
868 let removed = ui.remove_subtree(services, node);
869 app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
870 let window_frame = frame.windows.entry(window).or_default();
871 let any_removed = !removed.is_empty();
872 for removed in removed {
873 window_frame.instances.remove(removed);
874 window_frame.children.remove(removed);
875 }
876 if any_removed {
877 window_frame.revision = window_frame.revision.saturating_add(1);
878 }
879 });
880 }
881
882 reachable_from_layers.clear();
883 reachable_from_view_cache_roots.clear();
884 gc_stack.clear();
885 ui.restore_scratch_gc_reachable_from_layers(reachable_from_layers);
886 ui.restore_scratch_gc_reachable_from_view_cache_roots(reachable_from_view_cache_roots);
887 ui.restore_scratch_gc_stack(gc_stack);
888
889 if keep_alive_view_cache_elements_from_scratch {
890 keep_alive_view_cache_elements.clear();
891 window_state
892 .restore_scratch_view_cache_keep_alive_elements(keep_alive_view_cache_elements);
893 }
894
895 if window_state.wants_continuous_frames() {
896 app.push_effect(Effect::RequestAnimationFrame(window));
897 }
898
899 root_node
900 });
901 ui.publish_window_runtime_snapshots(app);
902 root_node
903}
904
905#[allow(clippy::too_many_arguments)]
919pub fn render_dismissible_root_with_hooks<H, I>(
920 ui: &mut UiTree<H>,
921 app: &mut H,
922 services: &mut dyn fret_core::UiServices,
923 window: AppWindowId,
924 bounds: Rect,
925 root_name: &str,
926 render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
927) -> NodeId
928where
929 H: UiHost + 'static,
930 I: IntoIterator<Item = AnyElement>,
931{
932 render_dismissible_root_impl(ui, app, services, window, bounds, root_name, render)
933}
934
935#[allow(clippy::too_many_arguments)]
936fn render_dismissible_root_impl<H: UiHost + 'static, F, I>(
937 ui: &mut UiTree<H>,
938 app: &mut H,
939 services: &mut dyn fret_core::UiServices,
940 window: AppWindowId,
941 bounds: Rect,
942 root_name: &str,
943 render: F,
944) -> NodeId
945where
946 F: FnOnce(&mut ElementContext<'_, H>) -> I,
947 I: IntoIterator<Item = AnyElement>,
948{
949 let frame_id = app.frame_id();
950 #[cfg(debug_assertions)]
951 ui.debug_note_declarative_render_root_called(frame_id);
952 let focused = ui.focus();
953 ui.begin_debug_frame_if_needed(frame_id);
954
955 ui.invalidate_scroll_handle_bindings_for_changed_handles(
958 app,
959 crate::layout_pass::LayoutPassKind::Final,
960 false,
961 false,
962 );
963
964 let ui_ref: &UiTree<H> = &*ui;
965 let children: Vec<AnyElement> =
966 app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
967 runtime.prepare_window_for_frame(window, frame_id);
968 let mut should_reuse_view_cache =
969 |node: NodeId| ui_ref.should_reuse_view_cache_node(node);
970 let mut cx = crate::elements::ElementContext::new_for_root_name(
971 app, runtime, window, bounds, root_name,
972 );
973 cx.set_view_cache_should_reuse(&mut should_reuse_view_cache);
974 cx.sync_focused_element_from_focused_node(focused);
975 cx.dismissible_clear_on_dismiss_request();
976 cx.dismissible_clear_on_pointer_move();
977 let built = render(&mut cx);
978 cx.collect_children(built)
979 });
980
981 let root_node =
982 app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
983 runtime.prepare_window_for_frame(window, frame_id);
984 let lag = runtime.gc_lag_frames();
985 let cutoff = frame_id.0.saturating_sub(lag);
986
987 let window_state = runtime.for_window_mut(window);
988 ui.debug_set_element_children_vec_pool_stats(
989 window_state.element_children_vec_pool_reuses(),
990 window_state.element_children_vec_pool_misses(),
991 );
992 let root_id = crate::elements::global_root(window, root_name);
993 let mut scroll_bindings: Vec<crate::declarative::frame::ScrollHandleBinding> =
994 Vec::new();
995
996 let seeded_root = window_state.node_entry(root_id).map(|e| e.node);
997 let root_node = ui
998 .resolve_reusable_node_for_element_seeded(root_id, seeded_root)
999 .unwrap_or_else(|| {
1000 let node = ui.create_node(ElementHostWidget::new(root_id));
1001 ui.set_node_element(node, Some(root_id));
1002 window_state.set_node_entry(
1003 root_id,
1004 NodeEntry {
1005 node,
1006 last_seen_frame: frame_id,
1007 root: root_id,
1008 },
1009 );
1010 node
1011 });
1012 ui.set_node_element(root_node, Some(root_id));
1013
1014 window_state.set_node_entry(
1015 root_id,
1016 NodeEntry {
1017 node: root_node,
1018 last_seen_frame: frame_id,
1019 root: root_id,
1020 },
1021 );
1022
1023 let mut pending_invalidations = ui.take_scratch_pending_invalidations();
1024 pending_invalidations.clear();
1025 app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
1026 let window_frame = frame.windows.entry(window).or_default();
1027 prepare_window_frame_for_frame(window_frame, frame_id);
1028
1029 let inserted = window_frame
1030 .instances
1031 .insert(
1032 root_node,
1033 ElementRecord {
1034 element: root_id,
1035 instance: ElementInstance::DismissibleLayer(
1036 DismissibleLayerProps::default(),
1037 ),
1038 inherited_foreground: None,
1039 inherited_text_style: None,
1040 semantics_decoration: None,
1041 key_context: None,
1042 },
1043 )
1044 .is_none();
1045 if inserted {
1046 window_frame.revision = window_frame.revision.saturating_add(1);
1047 }
1048
1049 let mut mounted_children: Vec<NodeId> = Vec::with_capacity(children.len());
1050 let mut children = children;
1051 for child in children.drain(..) {
1052 mounted_children.push(mount_element(
1053 ui,
1054 window,
1055 root_id,
1056 frame_id,
1057 window_state,
1058 window_frame,
1059 child,
1060 None,
1061 &mut scroll_bindings,
1062 &mut pending_invalidations,
1063 ));
1064 }
1065 ui.set_children(root_node, mounted_children);
1066 window_state.restore_scratch_element_children_vec(children);
1067 });
1068
1069 if ui.view_cache_enabled() {
1070 let _ = ui.repair_parent_pointers_from_layer_roots();
1071 }
1072
1073 apply_pending_invalidations(ui, &mut pending_invalidations);
1074 ui.restore_scratch_pending_invalidations(pending_invalidations);
1075
1076 crate::declarative::frame::register_scroll_handle_bindings_batch(
1077 app,
1078 window,
1079 frame_id,
1080 scroll_bindings,
1081 );
1082
1083 window_state.set_root_bounds(root_id, bounds);
1085
1086 let mut keep_alive_view_cache_elements: HashSet<GlobalElementId>;
1087 let keep_alive_view_cache_elements_from_scratch: bool;
1088 if keep_alive_view_cache_scratch_disabled() {
1089 keep_alive_view_cache_elements = HashSet::new();
1090 keep_alive_view_cache_elements_from_scratch = false;
1091 let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
1092 let mut stack: Vec<GlobalElementId> = Vec::new();
1093 collect_keep_alive_view_cache_elements_in_place(
1094 ui,
1095 window_state,
1096 &mut keep_alive_view_cache_elements,
1097 &mut visited_roots,
1098 &mut stack,
1099 );
1100 } else {
1101 keep_alive_view_cache_elements =
1102 window_state.take_scratch_view_cache_keep_alive_elements();
1103 keep_alive_view_cache_elements_from_scratch = true;
1104 {
1105 let mut visited_roots =
1106 window_state.take_scratch_view_cache_keep_alive_visited_roots();
1107 let mut stack = window_state.take_scratch_view_cache_keep_alive_stack();
1108 collect_keep_alive_view_cache_elements_in_place(
1109 ui,
1110 window_state,
1111 &mut keep_alive_view_cache_elements,
1112 &mut visited_roots,
1113 &mut stack,
1114 );
1115 window_state.restore_scratch_view_cache_keep_alive_visited_roots(visited_roots);
1116 window_state.restore_scratch_view_cache_keep_alive_stack(stack);
1117 }
1118 }
1119
1120 if window_state
1123 .view_cache_transitioned_reuse_roots()
1124 .next()
1125 .is_some()
1126 {
1127 with_window_frame(app, window, |window_frame| {
1128 touch_existing_declarative_subtree_seen(
1129 ui,
1130 window_state,
1131 window_frame,
1132 root_id,
1133 frame_id,
1134 root_node,
1135 );
1136 });
1137 }
1138
1139 let liveness_roots = ui.all_layer_roots();
1142 let keep_alive_roots = collect_live_retained_keep_alive_roots(ui, window_state);
1143 let mut stale: Vec<StaleNodeRecord> = Vec::new();
1144 let mut reachable_from_layers = ui.take_scratch_gc_reachable_from_layers();
1145 reachable_from_layers.clear();
1146 let mut reachable_from_layers_computed = false;
1147 let view_cache_has_reuse_roots = window_state.view_cache_reuse_roots().next().is_some();
1148 let mut reachable_from_view_cache_roots =
1149 ui.take_scratch_gc_reachable_from_view_cache_roots();
1150 reachable_from_view_cache_roots.clear();
1151 let mut reachable_from_view_cache_roots_active = false;
1152 let mut gc_stack = ui.take_scratch_gc_stack();
1153 gc_stack.clear();
1154
1155 if view_cache_has_reuse_roots {
1156 let view_cache_reuse_roots: Vec<GlobalElementId> =
1157 window_state.view_cache_reuse_roots().collect();
1158 let view_cache_reuse_root_nodes: Vec<NodeId> = view_cache_reuse_roots
1159 .iter()
1160 .filter_map(|root| {
1161 let seeded = window_state.node_entry(*root).map(|e| e.node);
1162 ui.resolve_live_attached_node_for_element_seeded(*root, seeded)
1163 })
1164 .collect();
1165
1166 if !view_cache_reuse_root_nodes.is_empty() {
1167 with_window_frame(app, window, |window_frame| {
1168 collect_reachable_nodes_for_gc_in_place(
1169 ui,
1170 window_frame,
1171 view_cache_reuse_root_nodes.iter().copied(),
1172 &mut reachable_from_view_cache_roots,
1173 &mut gc_stack,
1174 );
1175 });
1176 }
1177
1178 for root in view_cache_reuse_roots {
1179 if let Some(elements) = window_state.view_cache_elements_for_root(root) {
1180 for &element in elements {
1181 let seeded = window_state.node_entry(element).map(|entry| entry.node);
1182 if let Some(node) =
1183 ui.resolve_live_attached_node_for_element_seeded(element, seeded)
1184 {
1185 reachable_from_view_cache_roots.insert(node);
1186 }
1187 }
1188 }
1189 }
1190
1191 reachable_from_view_cache_roots_active = true;
1192 }
1193 window_state.retain_nodes(|id, entry| {
1194 let mut decision = gc_node_retention_decision(
1195 *id,
1196 entry.node,
1197 &mut entry.last_seen_frame,
1198 entry.root,
1199 root_id,
1200 frame_id,
1201 cutoff,
1202 &keep_alive_view_cache_elements,
1203 reachable_from_layers_computed.then_some(&reachable_from_layers),
1204 reachable_from_view_cache_roots_active,
1205 &reachable_from_view_cache_roots,
1206 );
1207 if matches!(decision, GcNodeRetentionDecision::NeedLayerReachability) {
1208 with_window_frame(app, window, |window_frame| {
1209 if liveness_roots.is_empty() {
1210 collect_reachable_nodes_for_gc_in_place(
1211 ui,
1212 window_frame,
1213 std::iter::once(root_node).chain(keep_alive_roots.iter().copied()),
1214 &mut reachable_from_layers,
1215 &mut gc_stack,
1216 );
1217 } else {
1218 collect_reachable_nodes_for_gc_in_place(
1219 ui,
1220 window_frame,
1221 liveness_roots
1222 .iter()
1223 .copied()
1224 .chain(keep_alive_roots.iter().copied()),
1225 &mut reachable_from_layers,
1226 &mut gc_stack,
1227 )
1228 }
1229 });
1230 reachable_from_layers_computed = true;
1231 decision = gc_node_retention_decision(
1232 *id,
1233 entry.node,
1234 &mut entry.last_seen_frame,
1235 entry.root,
1236 root_id,
1237 frame_id,
1238 cutoff,
1239 &keep_alive_view_cache_elements,
1240 Some(&reachable_from_layers),
1241 reachable_from_view_cache_roots_active,
1242 &reachable_from_view_cache_roots,
1243 );
1244 }
1245 if matches!(decision, GcNodeRetentionDecision::Keep) {
1246 return true;
1247 }
1248 stale.push(StaleNodeRecord {
1249 node: entry.node,
1250 element: *id,
1251 #[cfg(feature = "diagnostics")]
1252 element_root: entry.root,
1253 });
1254 false
1255 });
1256
1257 for record in &stale {
1258 window_state.forget_view_cache_subtree_elements(record.element);
1259 }
1260
1261 for record in stale {
1262 let node = record.node;
1263 #[cfg(feature = "diagnostics")]
1264 if let Some(ctx) = with_window_frame(app, window, |window_frame| {
1265 let window_frame = window_frame?;
1266 let parent = ui.node_parent(node);
1267 let parent_frame_children = parent.and_then(|p| window_frame.children.get(p));
1268 let root_reachable_from_layer_roots =
1269 reachable_from_layers_computed && reachable_from_layers.contains(&node);
1270 let root_reachable_from_view_cache_roots =
1271 reachable_from_view_cache_roots_active
1272 .then(|| reachable_from_view_cache_roots.contains(&node));
1273 let view_cache_reuse_roots: Vec<GlobalElementId> =
1274 window_state.view_cache_reuse_roots().collect();
1275 let liveness_layer_roots_len =
1276 liveness_roots.len().min(u32::MAX as usize) as u32;
1277 let view_cache_reuse_roots_len =
1278 view_cache_reuse_roots.len().min(u32::MAX as usize) as u32;
1279 let view_cache_reuse_root_nodes_len = view_cache_reuse_roots
1280 .iter()
1281 .filter(|root| window_state.node_entry(**root).is_some())
1282 .count()
1283 .min(u32::MAX as usize)
1284 as u32;
1285 let mut path_edge_frame_contains_child: [u8; 16] = [2u8; 16];
1286 let mut path_edge_len: u8 = 0;
1287 let mut current = Some(node);
1288 while let Some(child) = current {
1289 let Some(parent) = ui.node_parent(child) else {
1290 break;
1291 };
1292 if (path_edge_len as usize) >= path_edge_frame_contains_child.len() {
1293 break;
1294 }
1295 let contains = window_frame
1296 .children
1297 .get(parent)
1298 .map(|children| children.contains(&child));
1299 path_edge_frame_contains_child[path_edge_len as usize] = match contains {
1300 Some(true) => 1,
1301 Some(false) => 0,
1302 None => 2,
1303 };
1304 path_edge_len = path_edge_len.saturating_add(1);
1305 current = Some(parent);
1306 }
1307 Some(crate::tree::UiDebugRemoveSubtreeFrameContext {
1308 parent_frame_children_len: parent_frame_children
1309 .map(|v| v.len().min(u32::MAX as usize) as u32),
1310 parent_frame_children_contains_root: parent_frame_children
1311 .map(|v| v.contains(&node)),
1312 root_frame_instance_present: window_frame.instances.contains_key(node),
1313 root_frame_children_len: window_frame
1314 .children
1315 .get(node)
1316 .map(|v| v.len().min(u32::MAX as usize) as u32),
1317 root_reachable_from_layer_roots,
1318 root_reachable_from_view_cache_roots,
1319 liveness_layer_roots_len,
1320 view_cache_reuse_roots_len,
1321 view_cache_reuse_root_nodes_len,
1322 trigger_element: Some(record.element),
1323 trigger_element_root: Some(record.element_root),
1324 trigger_element_in_view_cache_keep_alive: Some(
1325 keep_alive_view_cache_elements.contains(&record.element),
1326 ),
1327 trigger_element_listed_under_reuse_root: window_state
1328 .view_cache_reuse_roots()
1329 .find(|&root| {
1330 window_state
1331 .view_cache_elements_for_root(root)
1332 .is_some_and(|elements| elements.contains(&record.element))
1333 }),
1334 path_edge_len,
1335 path_edge_frame_contains_child,
1336 })
1337 }) {
1338 ui.debug_set_remove_subtree_frame_context(node, ctx);
1339 }
1340
1341 let removed = ui.remove_subtree(services, node);
1342 app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
1343 let window_frame = frame.windows.entry(window).or_default();
1344 let any_removed = !removed.is_empty();
1345 for removed in removed {
1346 window_frame.instances.remove(removed);
1347 window_frame.children.remove(removed);
1348 }
1349 if any_removed {
1350 window_frame.revision = window_frame.revision.saturating_add(1);
1351 }
1352 });
1353 }
1354
1355 reachable_from_layers.clear();
1356 reachable_from_view_cache_roots.clear();
1357 gc_stack.clear();
1358 ui.restore_scratch_gc_reachable_from_layers(reachable_from_layers);
1359 ui.restore_scratch_gc_reachable_from_view_cache_roots(reachable_from_view_cache_roots);
1360 ui.restore_scratch_gc_stack(gc_stack);
1361
1362 if keep_alive_view_cache_elements_from_scratch {
1363 keep_alive_view_cache_elements.clear();
1364 window_state
1365 .restore_scratch_view_cache_keep_alive_elements(keep_alive_view_cache_elements);
1366 }
1367
1368 if window_state.wants_continuous_frames() {
1369 app.push_effect(Effect::RequestAnimationFrame(window));
1370 }
1371
1372 root_node
1373 });
1374 let root_attached = ui.node_layer(root_node).is_some() || ui.node_parent(root_node).is_some();
1375 if root_attached {
1376 ui.clear_declarative_window_snapshot_commit(root_node);
1377 ui.publish_window_runtime_snapshots(app);
1378 } else {
1379 ui.defer_declarative_window_snapshot_commit(root_node);
1380 }
1381 root_node
1382}
1383
1384#[allow(clippy::too_many_arguments)]
1385fn mount_element<H: UiHost + 'static>(
1386 ui: &mut UiTree<H>,
1387 _window: AppWindowId,
1388 root_id: GlobalElementId,
1389 frame_id: fret_runtime::FrameId,
1390 window_state: &mut crate::elements::WindowElementState,
1391 window_frame: &mut WindowFrame,
1392 element: AnyElement,
1393 parent_inherited_text_style: Option<fret_core::TextStyleRefinement>,
1394 scroll_bindings: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
1395 pending_invalidations: &mut HashMap<NodeId, u8>,
1396) -> NodeId {
1397 let mut element = element;
1398 let id = element.id;
1399 let inherited_foreground = element.inherited_foreground;
1400 let local_inherited_text_style = element.inherited_text_style.clone();
1401 let inherited_text_style = match (
1402 parent_inherited_text_style.as_ref(),
1403 local_inherited_text_style.as_ref(),
1404 ) {
1405 (Some(parent), Some(local)) => Some(parent.merged(local)),
1406 (Some(parent), None) => Some(parent.clone()),
1407 (None, Some(local)) => Some(local.clone()),
1408 (None, None) => None,
1409 }
1410 .filter(|style| !style.is_empty());
1411 let semantics_decoration = element.semantics_decoration.clone();
1412 let key_context = element.key_context.clone();
1413 let mut children = std::mem::take(&mut element.children);
1414 let existing_node_entry = window_state.node_entry(id);
1415 let had_existing_node_entry = existing_node_entry.is_some();
1416 let had_existing_node = existing_node_entry
1417 .map(|e| ui.node_exists(e.node))
1418 .unwrap_or(false);
1419 let view_cache_props = match &element.kind {
1420 ElementKind::ViewCache(props) => Some(*props),
1421 _ => None,
1422 };
1423 let reuse_view_cache =
1424 view_cache_props.is_some() && window_state.should_reuse_view_cache_root(id);
1425
1426 #[cfg(feature = "unstable-retained-bridge")]
1427 let retained_subtree_props = match &element.kind {
1428 ElementKind::RetainedSubtree(props) => Some(props.clone()),
1429 _ => None,
1430 };
1431
1432 let span = if view_cache_props.is_some() && tracing::enabled!(tracing::Level::TRACE) {
1433 tracing::trace_span!(
1434 "ui.cache_root.mount",
1435 element = ?id,
1436 node = tracing::field::Empty,
1437 cache_hit = reuse_view_cache,
1438 contained_layout = view_cache_props
1439 .map(|p| p.contained_layout)
1440 .unwrap_or(false),
1441 frame_id = frame_id.0,
1442 )
1443 } else {
1444 tracing::Span::none()
1445 };
1446 let _span_guard = span.enter();
1447
1448 let seeded = window_state.node_entry(id).map(|e| e.node);
1449 let node = ui
1450 .resolve_reusable_node_for_element_seeded(id, seeded)
1451 .unwrap_or_else(|| {
1452 let node = ui.create_node(ElementHostWidget::new(id));
1453 ui.set_node_element(node, Some(id));
1454 window_state.set_node_entry(
1455 id,
1456 NodeEntry {
1457 node,
1458 last_seen_frame: frame_id,
1459 root: root_id,
1460 },
1461 );
1462 node
1463 });
1464 ui.set_node_element(node, Some(id));
1465
1466 window_state.set_node_entry(
1467 id,
1468 NodeEntry {
1469 node,
1470 last_seen_frame: frame_id,
1471 root: root_id,
1472 },
1473 );
1474
1475 if reuse_view_cache
1476 && view_cache_root_needs_layout_for_deferred_scroll_requests(ui, window_frame, node)
1477 {
1478 ui.invalidate(node, Invalidation::Layout);
1485 }
1486
1487 if view_cache_props.is_some() && tracing::enabled!(tracing::Level::TRACE) {
1488 span.record("node", tracing::field::debug(node));
1489 }
1490
1491 match &element.kind {
1492 ElementKind::ViewCache(props) => {
1493 let layout_definite = !matches!(props.layout.size.width, crate::element::Length::Auto)
1494 && !matches!(props.layout.size.height, crate::element::Length::Auto);
1495 ui.set_node_view_cache_flags(node, true, props.contained_layout, layout_definite);
1496 if !reuse_view_cache {
1497 ui.set_node_view_cache_needs_rerender(node, false);
1498 }
1499 let reuse_reason = if !had_existing_node_entry {
1500 crate::tree::UiDebugCacheRootReuseReason::FirstMount
1501 } else if !had_existing_node {
1502 crate::tree::UiDebugCacheRootReuseReason::NodeRecreated
1503 } else if reuse_view_cache {
1504 crate::tree::UiDebugCacheRootReuseReason::MarkedReuseRoot
1505 } else if !ui.view_cache_enabled() {
1506 crate::tree::UiDebugCacheRootReuseReason::ViewCacheDisabled
1507 } else if ui.inspection_active() {
1508 crate::tree::UiDebugCacheRootReuseReason::InspectionActive
1509 } else if window_state.view_cache_key_mismatch(id) {
1510 crate::tree::UiDebugCacheRootReuseReason::CacheKeyMismatch
1511 } else if ui.view_cache_node_needs_rerender(node) {
1512 crate::tree::UiDebugCacheRootReuseReason::NeedsRerender
1513 } else if ui.node_layout_invalidated(node) {
1514 crate::tree::UiDebugCacheRootReuseReason::LayoutInvalidated
1515 } else {
1516 crate::tree::UiDebugCacheRootReuseReason::NotMarkedReuseRoot
1517 };
1518 ui.debug_record_view_cache_root(
1519 node,
1520 reuse_view_cache,
1521 props.contained_layout,
1522 reuse_reason,
1523 );
1524 }
1525 _ => {
1526 ui.set_node_view_cache_flags(node, false, false, false);
1527 }
1528 }
1529
1530 match &element.kind {
1531 ElementKind::TextInputRegion(props) => {
1532 ui.set_node_text_boundary_mode_override(node, props.text_boundary_mode_override);
1533 }
1534 _ => {
1535 ui.set_node_text_boundary_mode_override(node, None);
1536 }
1537 }
1538
1539 let instance = match element.kind {
1540 ElementKind::Container(p) => ElementInstance::Container(p),
1541 ElementKind::Semantics(p) => ElementInstance::Semantics(p),
1542 ElementKind::SemanticFlex(p) => ElementInstance::SemanticFlex(p),
1543 ElementKind::FocusScope(p) => ElementInstance::FocusScope(p),
1544 ElementKind::LayoutQueryRegion(p) => ElementInstance::LayoutQueryRegion(p),
1545 ElementKind::InteractivityGate(p) => ElementInstance::InteractivityGate(p),
1546 ElementKind::HitTestGate(p) => ElementInstance::HitTestGate(p),
1547 ElementKind::FocusTraversalGate(p) => ElementInstance::FocusTraversalGate(p),
1548 ElementKind::ForegroundScope(p) => ElementInstance::ForegroundScope(p),
1549 ElementKind::Opacity(p) => ElementInstance::Opacity(p),
1550 ElementKind::EffectLayer(p) => ElementInstance::EffectLayer(p),
1551 ElementKind::BackdropSourceGroup(p) => ElementInstance::BackdropSourceGroup(p),
1552 ElementKind::MaskLayer(p) => ElementInstance::MaskLayer(p),
1553 ElementKind::CompositeGroup(p) => ElementInstance::CompositeGroup(p),
1554 ElementKind::ViewCache(p) => ElementInstance::ViewCache(p),
1555 ElementKind::VisualTransform(p) => ElementInstance::VisualTransform(p),
1556 ElementKind::RenderTransform(p) => ElementInstance::RenderTransform(p),
1557 ElementKind::FractionalRenderTransform(p) => ElementInstance::FractionalRenderTransform(p),
1558 ElementKind::Anchored(p) => ElementInstance::Anchored(p),
1559 ElementKind::Pressable(p) => ElementInstance::Pressable(p),
1560 ElementKind::PointerRegion(p) => ElementInstance::PointerRegion(p),
1561 ElementKind::TextInputRegion(p) => ElementInstance::TextInputRegion(p),
1562 ElementKind::InternalDragRegion(p) => ElementInstance::InternalDragRegion(p),
1563 ElementKind::ExternalDragRegion(p) => ElementInstance::ExternalDragRegion(p),
1564 ElementKind::RovingFlex(p) => ElementInstance::RovingFlex(p),
1565 ElementKind::Stack(p) => ElementInstance::Stack(p),
1566 ElementKind::Column(p) => ElementInstance::Flex(FlexProps {
1567 layout: p.layout,
1568 direction: fret_core::Axis::Vertical,
1569 gap: p.gap,
1570 padding: p.padding,
1571 justify: p.justify,
1572 align: p.align,
1573 wrap: false,
1574 }),
1575 ElementKind::Row(p) => ElementInstance::Flex(FlexProps {
1576 layout: p.layout,
1577 direction: fret_core::Axis::Horizontal,
1578 gap: p.gap,
1579 padding: p.padding,
1580 justify: p.justify,
1581 align: p.align,
1582 wrap: false,
1583 }),
1584 ElementKind::Spacer(p) => ElementInstance::Spacer(p),
1585 ElementKind::Text(p) => ElementInstance::Text(p),
1586 ElementKind::StyledText(p) => ElementInstance::StyledText(p),
1587 ElementKind::SelectableText(p) => ElementInstance::SelectableText(p),
1588 ElementKind::TextInput(p) => ElementInstance::TextInput(p),
1589 ElementKind::TextArea(p) => ElementInstance::TextArea(p),
1590 ElementKind::ResizablePanelGroup(p) => ElementInstance::ResizablePanelGroup(p),
1591 ElementKind::VirtualList(p) => ElementInstance::VirtualList(p),
1592 ElementKind::Flex(p) => ElementInstance::Flex(p),
1593 ElementKind::Grid(p) => ElementInstance::Grid(p),
1594 ElementKind::Image(p) => ElementInstance::Image(p),
1595 ElementKind::Canvas(p) => ElementInstance::Canvas(p),
1596 #[cfg(feature = "unstable-retained-bridge")]
1597 ElementKind::RetainedSubtree(p) => ElementInstance::RetainedSubtree(p),
1598 ElementKind::ViewportSurface(p) => ElementInstance::ViewportSurface(p),
1599 ElementKind::SvgIcon(p) => ElementInstance::SvgIcon(p),
1600 ElementKind::Spinner(p) => ElementInstance::Spinner(p),
1601 ElementKind::HoverRegion(p) => ElementInstance::HoverRegion(p),
1602 ElementKind::WheelRegion(p) => ElementInstance::WheelRegion(p),
1603 ElementKind::Scroll(p) => ElementInstance::Scroll(p),
1604 ElementKind::Scrollbar(p) => ElementInstance::Scrollbar(p),
1605 };
1606
1607 collect_scroll_handle_bindings(id, &instance, scroll_bindings);
1608 let interactivity_gate_state = match &instance {
1609 ElementInstance::InteractivityGate(p) => Some((p.present, p.interactive)),
1610 _ => None,
1611 };
1612 let hit_test_gate_state = match &instance {
1613 ElementInstance::HitTestGate(p) => Some(p.hit_test),
1614 _ => None,
1615 };
1616 let focus_traversal_gate_state = match &instance {
1617 ElementInstance::FocusTraversalGate(p) => Some(p.traverse),
1618 _ => None,
1619 };
1620 let use_barrier_set_children = matches!(
1621 &instance,
1622 ElementInstance::VirtualList(props) if virtual_list_can_be_layout_barrier(props)
1623 );
1624
1625 let previous_record = window_frame.instances.get(node);
1626 let previous_instance = previous_record.map(|r| &r.instance);
1627 let previous_inherited_text_style =
1628 previous_record.and_then(|r| r.inherited_text_style.as_ref());
1629 if !reuse_view_cache {
1630 let mut mask = declarative_instance_change_mask(previous_instance, &instance);
1631 if previous_inherited_text_style != inherited_text_style.as_ref() {
1632 mask |= INVALIDATION_LAYOUT | INVALIDATION_PAINT;
1633 }
1634 if ui.interactive_resize_active() && ui.hover_edge_changed_this_frame() {
1635 mask &= !INVALIDATION_LAYOUT;
1636 }
1637 if mask != 0 {
1638 ui.debug_record_hover_declarative_invalidation(
1639 node,
1640 (mask & INVALIDATION_HIT_TEST) != 0,
1641 (mask & INVALIDATION_LAYOUT) != 0,
1642 (mask & INVALIDATION_PAINT) != 0,
1643 );
1644 pending_invalidations
1645 .entry(node)
1646 .and_modify(|m| *m |= mask)
1647 .or_insert(mask);
1648 }
1649 }
1650
1651 if let Some((present, interactive)) = interactivity_gate_state {
1652 ui.sync_interactivity_gate_widget(node, present, interactive);
1653 }
1654 if let Some(hit_test) = hit_test_gate_state {
1655 ui.sync_hit_test_gate_widget(node, hit_test);
1656 }
1657 if let Some(traverse) = focus_traversal_gate_state {
1658 ui.sync_focus_traversal_gate_widget(node, traverse);
1659 }
1660 let inserted = window_frame
1661 .instances
1662 .insert(
1663 node,
1664 ElementRecord {
1665 element: id,
1666 instance,
1667 inherited_foreground,
1668 inherited_text_style: inherited_text_style.clone(),
1669 semantics_decoration,
1670 key_context,
1671 },
1672 )
1673 .is_none();
1674 if inserted {
1675 window_frame.revision = window_frame.revision.saturating_add(1);
1676 }
1677
1678 if reuse_view_cache {
1679 let reuse_span = if tracing::enabled!(tracing::Level::TRACE) {
1680 tracing::trace_span!(
1681 "ui.cache_root.reuse",
1682 element = ?id,
1683 node = ?node,
1684 cache_hit = true,
1685 contained_layout = view_cache_props
1686 .map(|p| p.contained_layout)
1687 .unwrap_or(false),
1688 frame_id = frame_id.0,
1689 reason = "marked_reuse_root",
1690 )
1691 } else {
1692 tracing::Span::none()
1693 };
1694 let _reuse_guard = reuse_span.enter();
1695
1696 if window_frame.children.get(node).is_none() {
1697 sync_window_frame_children(window_frame, node, ui.children_ref(node));
1698 }
1699
1700 let transitioned_into_reuse = window_state.record_view_cache_reuse_frame(id, frame_id);
1701 window_state.touch_view_cache_authoring_identities_if_recorded(id);
1702 let touched = window_state.touch_view_cache_subtree_elements_if_recorded(
1703 id,
1704 frame_id,
1705 root_id,
1706 |element, seeded| ui.resolve_live_attached_node_for_element_seeded(element, seeded),
1707 );
1708 if transitioned_into_reuse && !touched {
1709 mark_existing_declarative_subtree_seen(
1713 ui,
1714 window_state,
1715 window_frame,
1716 root_id,
1717 frame_id,
1718 node,
1719 );
1720 window_state.record_view_cache_subtree_elements(
1721 id,
1722 collect_declarative_elements_for_existing_subtree(
1723 ui,
1724 window_state,
1725 window_frame,
1726 node,
1727 ),
1728 );
1729 } else if !touched {
1730 mark_existing_declarative_subtree_seen(
1731 ui,
1732 window_state,
1733 window_frame,
1734 root_id,
1735 frame_id,
1736 node,
1737 );
1738 window_state.record_view_cache_subtree_elements(
1739 id,
1740 collect_declarative_elements_for_existing_subtree(
1741 ui,
1742 window_state,
1743 window_frame,
1744 node,
1745 ),
1746 );
1747 }
1748
1749 window_state.touch_view_cache_action_hook_state_for_subtree_elements(id);
1753
1754 inherit_observations_for_existing_subtree(ui, window_state, window_frame, node);
1755 collect_scroll_handle_bindings_for_existing_subtree(
1756 ui,
1757 window_frame,
1758 scroll_bindings,
1759 node,
1760 );
1761 window_state.restore_scratch_element_children_vec(children);
1762 return node;
1763 }
1764
1765 #[cfg(feature = "unstable-retained-bridge")]
1766 if let Some(props) = retained_subtree_props {
1767 if !element.children.is_empty() {
1768 tracing::warn!(
1769 element = ?id,
1770 children = element.children.len(),
1771 "RetainedSubtree ignores declarative children (expected leaf element)",
1772 );
1773 }
1774
1775 let retained_root = window_state.with_state_mut(
1776 id,
1777 RetainedSubtreeHostState::default,
1778 |st: &mut RetainedSubtreeHostState| {
1779 if let Some(root) = st.root
1780 && ui.node_exists(root)
1781 {
1782 return root;
1783 }
1784
1785 let root = props.factory.build(ui);
1786 st.root = Some(root);
1787 root
1788 },
1789 );
1790
1791 let child_nodes = vec![retained_root];
1792 if had_existing_node {
1793 ui.set_children(node, child_nodes.clone());
1794 } else {
1795 ui.set_children_in_mount(node, child_nodes.clone());
1796 }
1797 window_frame
1798 .children
1799 .insert(node, Arc::<[NodeId]>::from(child_nodes));
1800 return node;
1801 }
1802
1803 if view_cache_props.is_some() {
1804 let reuse_span = if tracing::enabled!(tracing::Level::TRACE) {
1805 tracing::trace_span!(
1806 "ui.cache_root.reuse",
1807 element = ?id,
1808 node = ?node,
1809 cache_hit = false,
1810 contained_layout = view_cache_props
1811 .map(|p| p.contained_layout)
1812 .unwrap_or(false),
1813 frame_id = frame_id.0,
1814 reason = "not_marked_reuse_root",
1815 )
1816 } else {
1817 tracing::Span::none()
1818 };
1819 let _reuse_guard = reuse_span.enter();
1820
1821 let mut child_nodes: Vec<NodeId> = Vec::with_capacity(children.len());
1822 for child in children.drain(..) {
1823 child_nodes.push(mount_element(
1824 ui,
1825 _window,
1826 root_id,
1827 frame_id,
1828 window_state,
1829 window_frame,
1830 child,
1831 inherited_text_style.clone(),
1832 scroll_bindings,
1833 pending_invalidations,
1834 ));
1835 }
1836 if use_barrier_set_children {
1837 ui.set_children_barrier(node, child_nodes);
1838 } else if had_existing_node {
1839 ui.set_children(node, child_nodes);
1840 } else {
1841 ui.set_children_in_mount(node, child_nodes);
1842 }
1843 sync_window_frame_children(window_frame, node, ui.children_ref(node));
1844
1845 window_state.record_view_cache_subtree_elements(
1848 id,
1849 collect_declarative_elements_for_existing_subtree(ui, window_state, window_frame, node),
1850 );
1851 } else {
1852 let mut child_nodes: Vec<NodeId> = Vec::with_capacity(children.len());
1853 for child in children.drain(..) {
1854 child_nodes.push(mount_element(
1855 ui,
1856 _window,
1857 root_id,
1858 frame_id,
1859 window_state,
1860 window_frame,
1861 child,
1862 inherited_text_style.clone(),
1863 scroll_bindings,
1864 pending_invalidations,
1865 ));
1866 }
1867 if use_barrier_set_children {
1868 ui.set_children_barrier(node, child_nodes);
1869 } else if had_existing_node {
1870 ui.set_children(node, child_nodes);
1871 } else {
1872 ui.set_children_in_mount(node, child_nodes);
1873 }
1874 sync_window_frame_children(window_frame, node, ui.children_ref(node));
1875 }
1876
1877 window_state.restore_scratch_element_children_vec(children);
1878 node
1879}
1880
1881#[allow(clippy::too_many_arguments)]
1882fn reconcile_retained_virtual_list_hosts<H: UiHost + 'static>(
1883 ui: &mut UiTree<H>,
1884 app: &mut H,
1885 window: AppWindowId,
1886 bounds: Rect,
1887 root_id: GlobalElementId,
1888 frame_id: FrameId,
1889 window_state: &mut crate::elements::WindowElementState,
1890 window_frame: &mut WindowFrame,
1891 scroll_bindings: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
1892 pending_invalidations: &mut HashMap<NodeId, u8>,
1893 elements: Vec<(
1894 GlobalElementId,
1895 crate::tree::UiDebugRetainedVirtualListReconcileKind,
1896 )>,
1897) {
1898 if elements.is_empty() {
1899 return;
1900 }
1901
1902 enum RetainedVirtualListReconcileItems {
1903 Ready(Vec<crate::virtual_list::VirtualItem>),
1904 DeferUntilViewportKnown,
1905 }
1906
1907 for (element, reconcile_kind) in elements {
1908 let seeded = window_state.node_entry(element).map(|e| e.node);
1909 let node = ui
1910 .resolve_live_attached_node_for_element_seeded(element, seeded)
1911 .or_else(|| {
1912 let node = window_frame
1918 .instances
1919 .iter()
1920 .find_map(|(node, record)| (record.element == element).then_some(node))?;
1921 window_state.set_node_entry(
1922 element,
1923 NodeEntry {
1924 node,
1925 last_seen_frame: frame_id,
1926 root: root_id,
1927 },
1928 );
1929 Some(node)
1930 });
1931 let Some(node) = node else {
1932 continue;
1933 };
1934 if seeded != Some(node) {
1935 window_state.set_node_entry(
1936 element,
1937 NodeEntry {
1938 node,
1939 last_seen_frame: frame_id,
1940 root: root_id,
1941 },
1942 );
1943 }
1944
1945 let Some(record) = window_frame.instances.get(node) else {
1946 continue;
1947 };
1948 let ElementInstance::VirtualList(props) = &record.instance else {
1949 continue;
1950 };
1951 if !virtual_list_can_be_layout_barrier(props) {
1952 continue;
1953 }
1954 let props = props.clone();
1955
1956 let Some((key_at, row, range_extractor)) = window_state
1957 .try_with_state_mut::<crate::windowed_surface_host::RetainedVirtualListHostCallbacks<H>, _>(
1958 element,
1959 |st| (Arc::clone(&st.key_at), Arc::clone(&st.row), st.range_extractor),
1960 )
1961 else {
1962 continue;
1963 };
1964
1965 let desired_items = window_state.with_state_mut(
1966 element,
1967 crate::element::VirtualListState::default,
1968 |state| {
1969 state.metrics.ensure_with_mode(
1970 props.measure_mode,
1971 props.len,
1972 props.estimate_row_height,
1973 props.gap,
1974 props.scroll_margin,
1975 );
1976
1977 if props.len == 0 {
1978 state.window_range = None;
1979 state.render_window_range = None;
1980 return RetainedVirtualListReconcileItems::Ready(Vec::new());
1981 }
1982
1983 let viewport = match props.axis {
1984 fret_core::Axis::Vertical => Px(state.viewport_h.0.max(0.0)),
1985 fret_core::Axis::Horizontal => Px(state.viewport_w.0.max(0.0)),
1986 };
1987
1988 let mut window_range =
1992 state
1993 .window_range
1994 .or(state.render_window_range)
1995 .filter(|r| {
1996 r.count == props.len
1997 && r.overscan == props.overscan
1998 && r.start_index <= r.end_index
1999 && r.end_index < r.count
2000 });
2001
2002 if window_range.is_none() {
2003 if viewport.0 <= 0.0 {
2004 return RetainedVirtualListReconcileItems::DeferUntilViewportKnown;
2005 }
2006
2007 let offset_point = props.scroll_handle.offset();
2008 let offset_axis = match props.axis {
2009 fret_core::Axis::Vertical => offset_point.y,
2010 fret_core::Axis::Horizontal => offset_point.x,
2011 };
2012 let offset_axis = state.metrics.clamp_offset(offset_axis, viewport);
2013 window_range =
2014 state
2015 .metrics
2016 .visible_range(offset_axis, viewport, props.overscan);
2017 }
2018
2019 let Some(range) = window_range else {
2020 return RetainedVirtualListReconcileItems::DeferUntilViewportKnown;
2021 };
2022
2023 state.window_range = Some(range);
2024 state.render_window_range = Some(range);
2025
2026 let mut indices = (range_extractor)(range)
2027 .into_iter()
2028 .filter(|&idx| idx < props.len)
2029 .collect::<Vec<_>>();
2030 indices.sort_unstable();
2031 indices.dedup();
2032
2033 let items = indices
2034 .iter()
2035 .copied()
2036 .map(|idx| {
2037 let key = (key_at)(idx);
2038 state.metrics.virtual_item(idx, key)
2039 })
2040 .collect::<Vec<_>>();
2041 RetainedVirtualListReconcileItems::Ready(items)
2042 },
2043 );
2044
2045 let desired_items = match desired_items {
2046 RetainedVirtualListReconcileItems::Ready(items) => items,
2047 RetainedVirtualListReconcileItems::DeferUntilViewportKnown => {
2048 window_state.mark_retained_virtual_list_needs_reconcile(element, reconcile_kind);
2049 ui.request_redraw_coalesced(app);
2050 continue;
2051 }
2052 };
2053
2054 let reconcile_start = fret_core::time::Instant::now();
2055
2056 let prev_items_len = props.visible_items.len();
2057 let next_items_len = desired_items.len();
2058 let keep_alive_budget = props.keep_alive;
2059 let desired_keys: HashSet<crate::ItemKey> =
2060 desired_items.iter().map(|item| item.key).collect();
2061
2062 let mut existing_by_key: HashMap<crate::ItemKey, NodeId> = HashMap::new();
2063 let mut detached_by_key: Vec<(crate::ItemKey, NodeId)> = Vec::new();
2064 {
2065 let current_children = ui.children(node);
2066 for (&child, item) in current_children.iter().zip(props.visible_items.iter()) {
2067 existing_by_key.insert(item.key, child);
2068 }
2069
2070 if keep_alive_budget > 0 {
2071 for (&child, item) in current_children.iter().zip(props.visible_items.iter()) {
2072 if !desired_keys.contains(&item.key) {
2073 detached_by_key.push((item.key, child));
2074 }
2075 }
2076 }
2077 }
2078
2079 let mut keep_alive_state = window_state.with_state_mut(
2080 element,
2081 crate::windowed_surface_host::RetainedVirtualListKeepAliveState::default,
2082 std::mem::take,
2083 );
2084 let mut keep_alive_by_key: HashMap<crate::ItemKey, NodeId> = keep_alive_state.by_key;
2085 let mut keep_alive_order = keep_alive_state.order;
2086 let keep_alive_pool_len_before = keep_alive_by_key.len().min(u32::MAX as usize) as u32;
2087
2088 let mut preserved: u32 = 0;
2089 let mut attached: u32 = 0;
2090 let mut reused_from_keep_alive: u32 = 0;
2091 let mut kept_alive: u32 = 0;
2092 let mut evicted_keep_alive: u32 = 0;
2093 let mut next_children: Vec<NodeId> = Vec::with_capacity(desired_items.len());
2094 for item in &desired_items {
2095 if let Some(existing) = existing_by_key.get(&item.key).copied() {
2096 next_children.push(existing);
2097 preserved = preserved.saturating_add(1);
2098 continue;
2099 }
2100
2101 if let Some(existing) = keep_alive_by_key.remove(&item.key) {
2102 window_state.remove_retained_virtual_list_keep_alive_root(existing);
2103 next_children.push(existing);
2104 preserved = preserved.saturating_add(1);
2105 reused_from_keep_alive = reused_from_keep_alive.saturating_add(1);
2106 continue;
2107 }
2108
2109 attached = attached.saturating_add(1);
2110 let child_element = {
2111 let mut cx = crate::elements::ElementContext::new_for_existing_window_state(
2112 app,
2113 window,
2114 bounds,
2115 element,
2116 window_state,
2117 );
2118 let ui_ref: &UiTree<H> = &*ui;
2119 let mut should_reuse_view_cache =
2120 |node: NodeId| ui_ref.should_reuse_view_cache_node(node);
2121 cx.set_view_cache_should_reuse(&mut should_reuse_view_cache);
2122 cx.retained_virtual_list_row_any_element(item.key, item.index, &row)
2123 };
2124
2125 let child_node = mount_element(
2126 ui,
2127 window,
2128 root_id,
2129 frame_id,
2130 window_state,
2131 window_frame,
2132 child_element,
2133 None,
2134 scroll_bindings,
2135 pending_invalidations,
2136 );
2137 next_children.push(child_node);
2138 }
2139
2140 let detached =
2141 (prev_items_len.saturating_sub(preserved as usize)).min(u32::MAX as usize) as u32;
2142
2143 if keep_alive_budget == 0 {
2144 if !keep_alive_by_key.is_empty() {
2145 for node in keep_alive_by_key.values().copied() {
2146 window_state.remove_retained_virtual_list_keep_alive_root(node);
2147 }
2148 keep_alive_by_key.clear();
2149 }
2150 keep_alive_order.clear();
2151 } else {
2152 for (key, child) in detached_by_key {
2153 if let Some(prev) = keep_alive_by_key.remove(&key) {
2154 window_state.remove_retained_virtual_list_keep_alive_root(prev);
2155 }
2156 keep_alive_by_key.insert(key, child);
2157 keep_alive_order.push_back(key);
2158 window_state.add_retained_virtual_list_keep_alive_root(child);
2159 kept_alive = kept_alive.saturating_add(1);
2160 while keep_alive_by_key.len() > keep_alive_budget {
2161 let Some(evict_key) = keep_alive_order.pop_front() else {
2162 break;
2163 };
2164 let Some(evicted) = keep_alive_by_key.remove(&evict_key) else {
2165 continue;
2166 };
2167 window_state.remove_retained_virtual_list_keep_alive_root(evicted);
2168 evicted_keep_alive = evicted_keep_alive.saturating_add(1);
2169 }
2170 }
2171 let order_len = keep_alive_order.len();
2177 let budget = keep_alive_budget.max(1);
2178 if order_len > budget.saturating_mul(16).saturating_add(256) {
2179 let mut seen: HashSet<crate::ItemKey> = HashSet::new();
2180 let mut compact_rev: Vec<crate::ItemKey> =
2181 Vec::with_capacity(keep_alive_by_key.len());
2182 for &k in keep_alive_order.iter().rev() {
2183 if !keep_alive_by_key.contains_key(&k) {
2184 continue;
2185 }
2186 if seen.insert(k) {
2187 compact_rev.push(k);
2188 }
2189 }
2190 compact_rev.reverse();
2191 keep_alive_order = VecDeque::from(compact_rev);
2192 }
2193 }
2194
2195 let keep_alive_pool_len_after = keep_alive_by_key.len().min(u32::MAX as usize) as u32;
2196
2197 keep_alive_state.by_key = keep_alive_by_key;
2198 keep_alive_state.order = keep_alive_order;
2199 window_state.with_state_mut(
2200 element,
2201 crate::windowed_surface_host::RetainedVirtualListKeepAliveState::default,
2202 |st| *st = keep_alive_state,
2203 );
2204 ui.set_children_barrier(node, next_children.clone());
2205 window_frame
2206 .children
2207 .insert(node, Arc::<[NodeId]>::from(next_children));
2208
2209 if let Some(record) = window_frame.instances.get_mut(node)
2210 && let ElementInstance::VirtualList(props) = &mut record.instance
2211 {
2212 props.visible_items = desired_items;
2213 }
2214
2215 refresh_view_cache_membership_for_ancestor_roots(ui, window_state, window_frame, node);
2219
2220 let reconcile_time_us = reconcile_start.elapsed().as_micros().min(u32::MAX as u128) as u32;
2221
2222 ui.debug_record_retained_virtual_list_reconcile(
2223 crate::tree::UiDebugRetainedVirtualListReconcile {
2224 node,
2225 element,
2226 reconcile_kind,
2227 reconcile_time_us,
2228 prev_items: prev_items_len.min(u32::MAX as usize) as u32,
2229 next_items: next_items_len.min(u32::MAX as usize) as u32,
2230 preserved_items: preserved,
2231 attached_items: attached,
2232 detached_items: detached,
2233 keep_alive_pool_len_before,
2234 reused_from_keep_alive_items: reused_from_keep_alive,
2235 kept_alive_items: kept_alive,
2236 evicted_keep_alive_items: evicted_keep_alive,
2237 keep_alive_pool_len_after,
2238 },
2239 );
2240 }
2241}
2242
2243const INVALIDATION_HIT_TEST: u8 = 1 << 0;
2244const INVALIDATION_LAYOUT: u8 = 1 << 1;
2245const INVALIDATION_PAINT: u8 = 1 << 2;
2246
2247fn declarative_instance_change_mask(
2248 previous: Option<&ElementInstance>,
2249 next: &ElementInstance,
2250) -> u8 {
2251 let Some(previous) = previous else {
2252 return 0;
2256 };
2257
2258 if std::mem::discriminant(previous) != std::mem::discriminant(next) {
2259 return INVALIDATION_HIT_TEST | INVALIDATION_LAYOUT | INVALIDATION_PAINT;
2260 }
2261
2262 let mut hit_test_changed = false;
2263 let mut layout_changed = layout_style_for_instance(previous) != layout_style_for_instance(next);
2264 let mut paint_changed = false;
2265
2266 match (previous, next) {
2267 (ElementInstance::Container(a), ElementInstance::Container(b)) => {
2268 if a.padding != b.padding || a.border != b.border {
2270 layout_changed = true;
2271 }
2272
2273 if a.background != b.background
2274 || a.background_paint != b.background_paint
2275 || a.shadow != b.shadow
2276 || a.border_color != b.border_color
2277 || a.border_paint != b.border_paint
2278 || a.border_dash != b.border_dash
2279 || a.focus_ring != b.focus_ring
2280 || a.focus_border_color != b.focus_border_color
2281 || a.focus_within != b.focus_within
2282 || a.corner_radii != b.corner_radii
2283 || a.snap_to_device_pixels != b.snap_to_device_pixels
2284 {
2285 paint_changed = true;
2286 }
2287 }
2288 (ElementInstance::InteractivityGate(a), ElementInstance::InteractivityGate(b)) => {
2289 if a.present != b.present || a.interactive != b.interactive {
2293 layout_changed = true;
2294 paint_changed = true;
2295 }
2296 }
2297 (ElementInstance::HitTestGate(a), ElementInstance::HitTestGate(b)) => {
2298 if a.hit_test != b.hit_test {
2299 hit_test_changed = true;
2300 }
2301 }
2302 (ElementInstance::FocusTraversalGate(a), ElementInstance::FocusTraversalGate(b)) => {
2303 if a.traverse != b.traverse {
2304 layout_changed = true;
2305 paint_changed = true;
2306 }
2307 }
2308 (ElementInstance::Opacity(a), ElementInstance::Opacity(b)) => {
2309 if a.opacity != b.opacity {
2310 paint_changed = true;
2311 }
2312 }
2313 (ElementInstance::MaskLayer(a), ElementInstance::MaskLayer(b)) => {
2314 if a.mask != b.mask {
2315 paint_changed = true;
2316 }
2317 }
2318 (ElementInstance::CompositeGroup(a), ElementInstance::CompositeGroup(b)) => {
2319 if a.mode != b.mode || a.quality != b.quality {
2320 paint_changed = true;
2321 }
2322 }
2323 (ElementInstance::VisualTransform(a), ElementInstance::VisualTransform(b)) => {
2324 if a.transform != b.transform {
2325 paint_changed = true;
2326 }
2327 }
2328 (ElementInstance::RenderTransform(a), ElementInstance::RenderTransform(b)) => {
2329 if a.transform != b.transform {
2332 layout_changed = true;
2333 paint_changed = true;
2334 }
2335 }
2336 (
2337 ElementInstance::FractionalRenderTransform(a),
2338 ElementInstance::FractionalRenderTransform(b),
2339 ) => {
2340 if a.translate_x_fraction != b.translate_x_fraction
2343 || a.translate_y_fraction != b.translate_y_fraction
2344 {
2345 layout_changed = true;
2346 paint_changed = true;
2347 }
2348 }
2349 (ElementInstance::Anchored(a), ElementInstance::Anchored(b)) => {
2350 if a.outer_margin != b.outer_margin || a.anchor != b.anchor || a.options != b.options {
2353 layout_changed = true;
2354 paint_changed = true;
2355 }
2356 }
2357 (ElementInstance::Text(a), ElementInstance::Text(b)) => {
2358 if a.text != b.text
2359 || a.style != b.style
2360 || a.color != b.color
2361 || a.wrap != b.wrap
2362 || a.overflow != b.overflow
2363 {
2364 layout_changed = true;
2365 paint_changed = true;
2366 }
2367 }
2368 (ElementInstance::StyledText(a), ElementInstance::StyledText(b)) => {
2369 if a.rich != b.rich
2370 || a.style != b.style
2371 || a.color != b.color
2372 || a.wrap != b.wrap
2373 || a.overflow != b.overflow
2374 {
2375 layout_changed = true;
2376 paint_changed = true;
2377 }
2378 }
2379 (ElementInstance::SelectableText(a), ElementInstance::SelectableText(b)) => {
2380 if a.rich != b.rich
2381 || a.style != b.style
2382 || a.color != b.color
2383 || a.wrap != b.wrap
2384 || a.overflow != b.overflow
2385 {
2386 layout_changed = true;
2387 paint_changed = true;
2388 }
2389 }
2390 _ => {}
2391 }
2392
2393 let mut mask = 0;
2394 if hit_test_changed {
2395 mask |= INVALIDATION_HIT_TEST;
2396 }
2397 if layout_changed {
2398 mask |= INVALIDATION_HIT_TEST | INVALIDATION_LAYOUT | INVALIDATION_PAINT;
2399 } else if paint_changed {
2400 mask |= INVALIDATION_PAINT;
2401 }
2402 mask
2403}
2404
2405fn virtual_list_can_be_layout_barrier(props: &crate::element::VirtualListProps) -> bool {
2406 match props.axis {
2407 fret_core::Axis::Vertical => {
2408 !matches!(props.layout.size.height, crate::element::Length::Auto)
2409 }
2410 fret_core::Axis::Horizontal => {
2411 !matches!(props.layout.size.width, crate::element::Length::Auto)
2412 }
2413 }
2414}
2415
2416fn apply_pending_invalidations<H: UiHost>(ui: &mut UiTree<H>, pending: &mut HashMap<NodeId, u8>) {
2417 for (node, mask) in pending.drain() {
2418 if (mask & INVALIDATION_HIT_TEST) != 0 {
2419 ui.invalidate(node, Invalidation::HitTest);
2420 }
2421 if (mask & INVALIDATION_LAYOUT) != 0 {
2422 ui.invalidate(node, Invalidation::Layout);
2423 }
2424 if (mask & INVALIDATION_PAINT) != 0 {
2425 ui.invalidate(node, Invalidation::Paint);
2426 }
2427 }
2428}
2429
2430fn mark_existing_declarative_subtree_seen<H: UiHost>(
2431 ui: &UiTree<H>,
2432 window_state: &mut crate::elements::WindowElementState,
2433 window_frame: &WindowFrame,
2434 root_id: GlobalElementId,
2435 frame_id: FrameId,
2436 root: NodeId,
2437) {
2438 let mut stack: Vec<NodeId> = vec![root];
2439 while let Some(node) = stack.pop() {
2440 if !ui.node_exists(node) {
2441 continue;
2442 }
2443 if let Some(element) = window_frame
2444 .instances
2445 .get(node)
2446 .map(|r| r.element)
2447 .or_else(|| ui.node_element(node))
2448 .or_else(|| window_state.element_for_node(node))
2449 {
2450 let root = window_state
2451 .node_entry(element)
2452 .map(|e| e.root)
2453 .unwrap_or(root_id);
2454 window_state.set_node_entry(
2455 element,
2456 NodeEntry {
2457 node,
2458 last_seen_frame: frame_id,
2459 root,
2460 },
2461 );
2462
2463 #[cfg(feature = "diagnostics")]
2464 window_state.touch_debug_identity_for_element(frame_id, element);
2465 }
2466
2467 push_existing_subtree_children(ui, window_frame, node, &mut stack);
2468 }
2469}
2470
2471fn touch_existing_declarative_subtree_seen<H: UiHost>(
2472 ui: &UiTree<H>,
2473 window_state: &mut crate::elements::WindowElementState,
2474 window_frame: Option<&WindowFrame>,
2475 root_id: GlobalElementId,
2476 frame_id: FrameId,
2477 root: NodeId,
2478) {
2479 let mut stack: Vec<NodeId> = vec![root];
2480 while let Some(node) = stack.pop() {
2481 if !ui.node_exists(node) {
2482 continue;
2483 }
2484 if let Some(element) = window_frame
2485 .and_then(|window_frame| window_frame.instances.get(node).map(|r| r.element))
2486 .or_else(|| ui.node_element(node))
2487 .or_else(|| window_state.element_for_node(node))
2488 {
2489 let root = window_state
2490 .node_entry(element)
2491 .map(|e| e.root)
2492 .unwrap_or(root_id);
2493 window_state.set_node_entry(
2494 element,
2495 NodeEntry {
2496 node,
2497 last_seen_frame: frame_id,
2498 root,
2499 },
2500 );
2501
2502 #[cfg(feature = "diagnostics")]
2503 window_state.touch_debug_identity_for_element(frame_id, element);
2504 }
2505
2506 if let Some(window_frame) = window_frame {
2507 push_existing_subtree_children(ui, window_frame, node, &mut stack);
2508 } else {
2509 for child in ui.children(node) {
2510 stack.push(child);
2511 }
2512 }
2513 }
2514}
2515
2516fn collect_declarative_elements_for_existing_subtree<H: UiHost>(
2517 ui: &UiTree<H>,
2518 window_state: &crate::elements::WindowElementState,
2519 window_frame: &WindowFrame,
2520 root: NodeId,
2521) -> Vec<GlobalElementId> {
2522 let mut out: Vec<GlobalElementId> = Vec::new();
2523 let mut seen: HashSet<GlobalElementId> = HashSet::new();
2524 let mut stack: Vec<NodeId> = vec![root];
2525 while let Some(node) = stack.pop() {
2526 if !ui.node_exists(node) {
2527 continue;
2528 }
2529 if let Some(element) = window_frame
2530 .instances
2531 .get(node)
2532 .map(|r| r.element)
2533 .or_else(|| ui.node_element(node))
2534 .or_else(|| window_state.element_for_node(node))
2535 && seen.insert(element)
2536 {
2537 out.push(element);
2538 }
2539
2540 push_existing_subtree_children(ui, window_frame, node, &mut stack);
2541 }
2542 out
2543}
2544
2545fn refresh_view_cache_membership_for_ancestor_roots<H: UiHost>(
2546 ui: &UiTree<H>,
2547 window_state: &mut crate::elements::WindowElementState,
2548 window_frame: &WindowFrame,
2549 node: NodeId,
2550) {
2551 let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
2552 let mut current = Some(node);
2553 while let Some(node) = current {
2554 if let Some(record) = window_frame.instances.get(node)
2555 && matches!(record.instance, ElementInstance::ViewCache(_))
2556 && visited_roots.insert(record.element)
2557 {
2558 window_state.record_view_cache_subtree_elements(
2559 record.element,
2560 collect_declarative_elements_for_existing_subtree(
2561 ui,
2562 window_state,
2563 window_frame,
2564 node,
2565 ),
2566 );
2567 }
2568 current = ui.node_parent(node);
2569 }
2570}
2571
2572#[cfg(test)]
2573fn collect_reachable_nodes_for_gc<H: UiHost>(
2574 ui: &UiTree<H>,
2575 window_frame: Option<&WindowFrame>,
2576 roots: impl IntoIterator<Item = NodeId>,
2577) -> HashSet<NodeId> {
2578 let mut out: HashSet<NodeId> = HashSet::new();
2579 let mut stack: Vec<NodeId> = Vec::new();
2580 collect_reachable_nodes_for_gc_in_place(ui, window_frame, roots, &mut out, &mut stack);
2581 out
2582}
2583
2584fn collect_reachable_nodes_for_gc_in_place<H: UiHost>(
2585 ui: &UiTree<H>,
2586 window_frame: Option<&WindowFrame>,
2587 roots: impl IntoIterator<Item = NodeId>,
2588 out: &mut HashSet<NodeId>,
2589 stack: &mut Vec<NodeId>,
2590) {
2591 out.clear();
2592 stack.clear();
2593 stack.extend(roots);
2594 while let Some(node) = stack.pop() {
2595 if !ui.node_exists(node) {
2596 continue;
2597 }
2598 if !out.insert(node) {
2599 continue;
2600 }
2601 if let Some(window_frame) = window_frame {
2602 push_existing_subtree_children(ui, window_frame, node, stack);
2603 } else {
2604 stack.extend(ui.children(node));
2605 }
2606 }
2607}
2608
2609fn collect_scroll_handle_bindings_for_existing_subtree<H: UiHost>(
2610 ui: &UiTree<H>,
2611 window_frame: &WindowFrame,
2612 out: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
2613 root: NodeId,
2614) {
2615 let mut stack: Vec<NodeId> = vec![root];
2616 while let Some(node) = stack.pop() {
2617 if let Some(record) = window_frame.instances.get(node) {
2618 collect_scroll_handle_bindings(record.element, &record.instance, out);
2619 }
2620
2621 push_existing_subtree_children(ui, window_frame, node, &mut stack);
2622 }
2623}
2624
2625#[cfg(test)]
2626#[allow(clippy::items_after_test_module)]
2627mod tests {
2628 use super::*;
2629 use fret_core::{PathConstraints, PathMetrics, PathStyle, TextConstraints, TextMetrics};
2630
2631 #[derive(Default)]
2632 struct TestWidget;
2633
2634 impl<H: UiHost> Widget<H> for TestWidget {
2635 fn layout(&mut self, cx: &mut LayoutCx<'_, H>) -> Size {
2636 for &child in cx.children {
2637 let _ = cx.layout_in(child, cx.bounds);
2638 }
2639 cx.available
2640 }
2641
2642 fn paint(&mut self, _cx: &mut PaintCx<'_, H>) {}
2643 }
2644
2645 #[derive(Default)]
2646 struct FakeUiServices;
2647
2648 impl fret_core::TextService for FakeUiServices {
2649 fn prepare(
2650 &mut self,
2651 _input: &fret_core::TextInput,
2652 _constraints: TextConstraints,
2653 ) -> (fret_core::TextBlobId, TextMetrics) {
2654 (
2655 fret_core::TextBlobId::default(),
2656 TextMetrics {
2657 size: Size::new(fret_core::Px(10.0), fret_core::Px(10.0)),
2658 baseline: fret_core::Px(8.0),
2659 },
2660 )
2661 }
2662
2663 fn release(&mut self, _blob: fret_core::TextBlobId) {}
2664 }
2665
2666 impl fret_core::PathService for FakeUiServices {
2667 fn prepare(
2668 &mut self,
2669 _commands: &[fret_core::PathCommand],
2670 _style: PathStyle,
2671 _constraints: PathConstraints,
2672 ) -> (fret_core::PathId, PathMetrics) {
2673 (fret_core::PathId::default(), PathMetrics::default())
2674 }
2675
2676 fn release(&mut self, _path: fret_core::PathId) {}
2677 }
2678
2679 impl fret_core::SvgService for FakeUiServices {
2680 fn register_svg(&mut self, _bytes: &[u8]) -> fret_core::SvgId {
2681 fret_core::SvgId::default()
2682 }
2683
2684 fn unregister_svg(&mut self, _svg: fret_core::SvgId) -> bool {
2685 false
2686 }
2687 }
2688
2689 impl fret_core::MaterialService for FakeUiServices {
2690 fn register_material(
2691 &mut self,
2692 _desc: fret_core::MaterialDescriptor,
2693 ) -> Result<fret_core::MaterialId, fret_core::MaterialRegistrationError> {
2694 Err(fret_core::MaterialRegistrationError::Unsupported)
2695 }
2696
2697 fn unregister_material(&mut self, _id: fret_core::MaterialId) -> bool {
2698 false
2699 }
2700 }
2701
2702 #[test]
2703 fn gc_reachability_unions_ui_and_window_frame_children() {
2704 use fret_runtime::FrameId;
2705
2706 let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
2707 ui.set_window(AppWindowId::default());
2708 ui.set_debug_enabled(true);
2709 ui.begin_debug_frame_if_needed(FrameId(1));
2710
2711 let root = ui.create_node(TestWidget);
2712 let ui_child = ui.create_node(TestWidget);
2713 let frame_child = ui.create_node(TestWidget);
2714
2715 ui.set_root(root);
2716 ui.set_children(root, vec![ui_child]);
2717
2718 let mut window_frame = WindowFrame::default();
2719 window_frame
2720 .children
2721 .insert(root, Arc::<[NodeId]>::from(vec![ui_child, frame_child]));
2722
2723 let reachable = collect_reachable_nodes_for_gc(&ui, Some(&window_frame), [root]);
2724 assert!(reachable.contains(&root));
2725 assert!(reachable.contains(&ui_child));
2726 assert!(reachable.contains(&frame_child));
2727 }
2728
2729 #[test]
2730 fn touch_existing_subtree_can_walk_window_frame_children() {
2731 use crate::UiHost;
2732 use crate::declarative::frame::WindowFrame;
2733 use crate::tree::UiTree;
2734 use crate::widget::{LayoutCx, PaintCx, Widget};
2735 use fret_runtime::FrameId;
2736
2737 #[derive(Default)]
2738 struct TestWidget;
2739
2740 impl<H: UiHost> Widget<H> for TestWidget {
2741 fn layout(&mut self, cx: &mut LayoutCx<'_, H>) -> Size {
2742 for &child in cx.children {
2743 let _ = cx.layout_in(child, cx.bounds);
2744 }
2745 cx.available
2746 }
2747
2748 fn paint(&mut self, _cx: &mut PaintCx<'_, H>) {}
2749 }
2750
2751 let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
2752 ui.set_window(AppWindowId::default());
2753
2754 let root_node = ui.create_node(TestWidget);
2755 let child_node = ui.create_node(TestWidget);
2756
2757 let root_element = GlobalElementId(1);
2758 let child_element = GlobalElementId(2);
2759 let root_id = GlobalElementId(999);
2760
2761 ui.set_node_element(root_node, Some(root_element));
2762 ui.set_node_element(child_node, Some(child_element));
2763
2764 let mut window_frame = WindowFrame::default();
2766 window_frame
2767 .children
2768 .insert(root_node, Arc::<[NodeId]>::from(vec![child_node]));
2769
2770 let mut window_state = crate::elements::WindowElementState::default();
2771
2772 touch_existing_declarative_subtree_seen(
2773 &ui,
2774 &mut window_state,
2775 Some(&window_frame),
2776 root_id,
2777 FrameId(1),
2778 root_node,
2779 );
2780
2781 let entry = window_state
2782 .node_entry(child_element)
2783 .expect("child touched");
2784 assert_eq!(entry.node, child_node);
2785 assert_eq!(entry.last_seen_frame, FrameId(1));
2786 assert_eq!(entry.root, root_id);
2787 }
2788
2789 #[test]
2790 fn gc_retention_ignores_stale_parent_pointer_layer_membership() {
2791 let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
2792 ui.set_window(AppWindowId::default());
2793
2794 let root = ui.create_node(TestWidget);
2795 let stale = ui.create_node(TestWidget);
2796 let root_id = GlobalElementId(900);
2797 let stale_id = GlobalElementId(901);
2798 let frame_id = FrameId(2);
2799 let cutoff = 1;
2800
2801 ui.set_root(root);
2802 ui.test_set_node_parent(stale, Some(root));
2803
2804 assert!(
2805 ui.node_layer(stale).is_some(),
2806 "reproducer requires a stale parent path that still resolves to a layer"
2807 );
2808
2809 let reachable = collect_reachable_nodes_for_gc(&ui, None, [root]);
2810 assert!(
2811 !reachable.contains(&stale),
2812 "stale node must stay unreachable from authoritative children traversal"
2813 );
2814
2815 let keep_alive_view_cache_elements: HashSet<GlobalElementId> = HashSet::new();
2816 let reachable_from_view_cache_roots: HashSet<NodeId> = HashSet::new();
2817 let mut last_seen_frame = FrameId(0);
2818
2819 assert!(matches!(
2820 gc_node_retention_decision(
2821 stale_id,
2822 stale,
2823 &mut last_seen_frame,
2824 root_id,
2825 root_id,
2826 frame_id,
2827 cutoff,
2828 &keep_alive_view_cache_elements,
2829 None,
2830 false,
2831 &reachable_from_view_cache_roots,
2832 ),
2833 GcNodeRetentionDecision::NeedLayerReachability
2834 ));
2835
2836 assert!(matches!(
2837 gc_node_retention_decision(
2838 stale_id,
2839 stale,
2840 &mut last_seen_frame,
2841 root_id,
2842 root_id,
2843 frame_id,
2844 cutoff,
2845 &keep_alive_view_cache_elements,
2846 Some(&reachable),
2847 false,
2848 &reachable_from_view_cache_roots,
2849 ),
2850 GcNodeRetentionDecision::Drop
2851 ));
2852 }
2853
2854 #[test]
2855 fn gc_prunes_removed_retained_keep_alive_roots_before_reachability() {
2856 let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
2857 ui.set_window(AppWindowId::default());
2858
2859 let root = ui.create_node(TestWidget);
2860 let stale = ui.create_node(TestWidget);
2861 let root_id = GlobalElementId(950);
2862 let stale_id = GlobalElementId(951);
2863 let frame_id = FrameId(2);
2864 let cutoff = 1;
2865
2866 ui.set_root(root);
2867
2868 let mut services = FakeUiServices;
2869 let removed = ui.remove_subtree(&mut services, stale);
2870 assert_eq!(
2871 removed,
2872 vec![stale],
2873 "expected stale keep-alive root to be removed"
2874 );
2875 assert!(
2876 !ui.node_exists(stale),
2877 "removed keep-alive roots must not remain live in UiTree"
2878 );
2879
2880 let raw_reachable = collect_reachable_nodes_for_gc(&ui, None, [stale]);
2881 assert!(
2882 raw_reachable.is_empty(),
2883 "dead NodeId roots must not be treated as reachable by the GC walk"
2884 );
2885
2886 let mut window_state = crate::elements::WindowElementState::default();
2887 window_state.add_retained_virtual_list_keep_alive_root(stale);
2888 window_state.set_node_entry(
2889 stale_id,
2890 NodeEntry {
2891 node: stale,
2892 last_seen_frame: FrameId(0),
2893 root: root_id,
2894 },
2895 );
2896
2897 let keep_alive_roots = collect_live_retained_keep_alive_roots(&ui, &mut window_state);
2898 assert!(
2899 keep_alive_roots.is_empty(),
2900 "removed keep-alive roots must be pruned before they participate in GC liveness"
2901 );
2902 assert!(
2903 window_state
2904 .retained_virtual_list_keep_alive_roots()
2905 .next()
2906 .is_none(),
2907 "pruning should update the retained keep-alive root set itself"
2908 );
2909
2910 let reachable = collect_reachable_nodes_for_gc(
2911 &ui,
2912 None,
2913 std::iter::once(root).chain(keep_alive_roots.iter().copied()),
2914 );
2915 assert!(
2916 !reachable.contains(&stale),
2917 "a removed keep-alive root must not keep the stale node reachable"
2918 );
2919
2920 let keep_alive_view_cache_elements: HashSet<GlobalElementId> = HashSet::new();
2921 let reachable_from_view_cache_roots: HashSet<NodeId> = HashSet::new();
2922 let mut last_seen_frame = FrameId(0);
2923 assert!(matches!(
2924 gc_node_retention_decision(
2925 stale_id,
2926 stale,
2927 &mut last_seen_frame,
2928 root_id,
2929 root_id,
2930 frame_id,
2931 cutoff,
2932 &keep_alive_view_cache_elements,
2933 Some(&reachable),
2934 false,
2935 &reachable_from_view_cache_roots,
2936 ),
2937 GcNodeRetentionDecision::Drop
2938 ));
2939 }
2940
2941 #[test]
2942 fn keep_alive_view_cache_membership_ignores_stale_nested_cache_roots() {
2943 let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
2944 ui.set_window(AppWindowId::default());
2945
2946 let outer_node = ui.create_node(TestWidget);
2947 let stale_inner_node = ui.create_node(TestWidget);
2948 let stale_leaf_node = ui.create_node(TestWidget);
2949 let outer = GlobalElementId(960);
2950 let inner = GlobalElementId(961);
2951 let leaf = GlobalElementId(962);
2952
2953 ui.set_root(outer_node);
2954 ui.set_node_element(outer_node, Some(outer));
2955 ui.set_node_element(stale_inner_node, Some(inner));
2956 ui.set_node_element(stale_leaf_node, Some(leaf));
2957
2958 let mut window_state = crate::elements::WindowElementState::default();
2959 window_state.set_node_entry(
2960 outer,
2961 NodeEntry {
2962 node: outer_node,
2963 last_seen_frame: FrameId(1),
2964 root: outer,
2965 },
2966 );
2967 window_state.set_node_entry(
2968 inner,
2969 NodeEntry {
2970 node: stale_inner_node,
2971 last_seen_frame: FrameId(0),
2972 root: outer,
2973 },
2974 );
2975 window_state.set_node_entry(
2976 leaf,
2977 NodeEntry {
2978 node: stale_leaf_node,
2979 last_seen_frame: FrameId(0),
2980 root: outer,
2981 },
2982 );
2983 window_state.mark_view_cache_reuse_root(outer);
2984 window_state.record_view_cache_subtree_elements(outer, vec![outer, inner, leaf]);
2985 window_state.record_view_cache_subtree_elements(inner, vec![inner, leaf]);
2986
2987 let mut keep_alive_view_cache_elements: HashSet<GlobalElementId> = HashSet::new();
2988 let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
2989 let mut stack: Vec<GlobalElementId> = Vec::new();
2990 collect_keep_alive_view_cache_elements_in_place(
2991 &ui,
2992 &window_state,
2993 &mut keep_alive_view_cache_elements,
2994 &mut visited_roots,
2995 &mut stack,
2996 );
2997
2998 assert!(
2999 keep_alive_view_cache_elements.contains(&outer),
3000 "expected live reused cache root to stay in the keep-alive closure"
3001 );
3002 assert!(
3003 !keep_alive_view_cache_elements.contains(&inner),
3004 "stale nested cache root membership must not recurse into detached roots"
3005 );
3006 assert!(
3007 !keep_alive_view_cache_elements.contains(&leaf),
3008 "stale nested descendants must not remain in the keep-alive closure"
3009 );
3010 }
3011}
3012
3013fn view_cache_root_needs_layout_for_deferred_scroll_requests<H: UiHost>(
3014 ui: &UiTree<H>,
3015 window_frame: &WindowFrame,
3016 root: NodeId,
3017) -> bool {
3018 let mut stack: Vec<NodeId> = vec![root];
3019 while let Some(node) = stack.pop() {
3020 if let Some(record) = window_frame.instances.get(node)
3021 && let ElementInstance::VirtualList(props) = &record.instance
3022 && props.scroll_handle.deferred_scroll_to_item().is_some()
3023 {
3024 return true;
3025 }
3026
3027 push_existing_subtree_children(ui, window_frame, node, &mut stack);
3028 }
3029 false
3030}
3031
3032fn push_existing_subtree_children<H: UiHost>(
3033 ui: &UiTree<H>,
3034 window_frame: &WindowFrame,
3035 node: NodeId,
3036 stack: &mut Vec<NodeId>,
3037) {
3038 let ui_children = ui.children(node);
3043 if !ui_children.is_empty() {
3044 stack.extend(ui_children.iter().copied());
3045 }
3046 if let Some(frame_children) = window_frame.children.get(node) {
3047 if ui_children.is_empty() {
3048 stack.extend(frame_children.iter().copied());
3049 } else {
3050 for &child in frame_children.iter() {
3051 if !ui_children.contains(&child) {
3052 stack.push(child);
3053 }
3054 }
3055 }
3056 }
3057}
3058
3059fn inherit_observations_for_existing_subtree<H: UiHost>(
3060 ui: &UiTree<H>,
3061 window_state: &mut crate::elements::WindowElementState,
3062 window_frame: &WindowFrame,
3063 root: NodeId,
3064) {
3065 let mut stack: Vec<NodeId> = vec![root];
3066 while let Some(node) = stack.pop() {
3067 if let Some(record) = window_frame.instances.get(node) {
3068 let element = record.element;
3069 window_state.touch_observed_models_for_element_if_recorded(element);
3070 window_state.touch_observed_globals_for_element_if_recorded(element);
3071 if matches!(record.instance, ElementInstance::ViewCache(_)) {
3072 window_state.touch_view_cache_state_keys_if_recorded(element);
3073 }
3074 }
3075
3076 push_existing_subtree_children(ui, window_frame, node, &mut stack);
3077 }
3078}
3079
3080fn collect_scroll_handle_bindings(
3081 element: GlobalElementId,
3082 instance: &ElementInstance,
3083 out: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
3084) {
3085 match instance {
3086 ElementInstance::VirtualList(props) => {
3087 let handle = props.scroll_handle.base_handle();
3088 out.push(crate::declarative::frame::ScrollHandleBinding {
3089 handle_key: handle.binding_key(),
3090 element,
3091 handle: handle.clone(),
3092 });
3093 }
3094 ElementInstance::Scroll(props) => {
3095 if let Some(handle) = props.scroll_handle.as_ref() {
3096 out.push(crate::declarative::frame::ScrollHandleBinding {
3097 handle_key: handle.binding_key(),
3098 element,
3099 handle: handle.clone(),
3100 });
3101 }
3102 }
3103 ElementInstance::WheelRegion(props) => {
3104 out.push(crate::declarative::frame::ScrollHandleBinding {
3105 handle_key: props.scroll_handle.binding_key(),
3106 element,
3107 handle: props.scroll_handle.clone(),
3108 });
3109 }
3110 ElementInstance::Scrollbar(props) => {
3111 out.push(crate::declarative::frame::ScrollHandleBinding {
3112 handle_key: props.scroll_handle.binding_key(),
3113 element,
3114 handle: props.scroll_handle.clone(),
3115 });
3116 }
3117 _ => {}
3118 }
3119}