Skip to main content

fret_ui/declarative/
mount.rs

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    // Parent-pointer-derived layer membership is not authoritative for GC: fearless refactors can
77    // temporarily or accidentally leave a stale parent path even after the child is detached from
78    // the authoritative UI/window-frame child lists.
79    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    // Retained keep-alive roots are authoritative only while the retained node still exists.
98    // Structural removal must not leave a dead NodeId widening GC reachability forever.
99    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
365/// Render a declarative element tree into an existing `UiTree` root.
366///
367/// Call this once per frame *before* `layout_all`/`paint_all`, for the relevant window.
368pub 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    // Prepare per-window element runtime state up-front so render code can observe last-frame
388    // geometry via `ElementContext::last_bounds_for_element` (e.g. measured-height motion).
389    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    // Out-of-band scroll handle mutations (e.g. deferred scroll-to-item) must be visible to view
409    // caching decisions. Apply scroll-handle-driven invalidations before running the declarative
410    // render closure so cache-hit frames cannot replay stale virtual-surface output.
411    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            // Declarative GC walks from the current layer roots. On the first frame, the base layer is
482            // typically registered by the app after `render_root` returns (e.g. `ui.set_root(root_node)`),
483            // which is too late for the GC pass below unless we install the base root here first.
484            //
485            // However, `render_root` is also used for non-base roots (e.g. overlay helper roots). Do not
486            // steal the base layer if it is already installed.
487            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            // View-cache experiments rely on explicit liveness bookkeeping (layer roots + view-cache
557            // reuse roots + subtree membership lists; ADR 0176). Parent pointers are still required
558            // for cache-root discovery and `node_layer` detachment checks, so repair any reachable
559            // inconsistencies before applying invalidations that may need to propagate across cache-root
560            // boundaries.
561            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            // Record the root's coordinate space for placement/collision logic (anchored overlays).
594            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 any cache root transitions into reuse this frame, proactively touch the entire
631            // retained subtree from the window root. This avoids GC sweeping still-live nodes in the
632            // transition frame when the producer subtree starts skipping renders.
633            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            // Node GC is keyed off `last_seen_frame`. Cache-hit frames can legitimately skip
651            // re-mounting cached subtrees, so view-cache reuse must keep the retained subtree alive
652            // via explicit liveness bookkeeping (ADR 0176).
653            //
654            // We only sweep nodes that are both stale and unreachable from the window's liveness
655            // roots:
656            // - layer roots (base + overlays),
657            // - view-cache reuse roots, and
658            // - retained windowed-surface keep-alive roots (ADR 0177),
659            // - recorded view-cache subtree memberships (to tolerate temporarily-incomplete child
660            //   edges on cache-hit frames).
661            //
662            // Note: `UiTree::node_layer` relies on parent pointers. Parent pointers can transiently
663            // drift under fearless refactors (e.g. when reusing cached subtrees), so `node_layer == None`
664            // must never be treated as a sufficient signal for liveness. Reachability from the
665            // liveness roots is the authoritative predicate for GC.
666            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                // Also treat recorded view-cache subtree memberships as authoritative reachability,
704                // so cache hits can keep subtrees alive even when child edges are temporarily
705                // incomplete (ADR 0176).
706                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/// Render a declarative element tree into a full-window, input-transparent overlay root.
906///
907/// The root handles:
908/// - Escape dismissal (bubbling from any focused descendant).
909/// - Outside-press dismissal via the runtime outside-press observer pass (ADR 0069).
910///
911/// Notes:
912/// - If the returned root is already attached to a parent or layer, this helper finishes the
913///   authoritative window-snapshot commit before returning.
914/// - If the root is still detached (for example, it will be attached later via
915///   `push_overlay_root(...)` or `set_children(...)`), the window-snapshot commit is deferred.
916///   Call `UiTree::commit_pending_declarative_window_runtime_snapshots(...)` after attachment when
917///   same-frame window-level consumers must observe the rebuilt root immediately.
918#[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    // Match `render_root`: apply out-of-band scroll handle invalidations before render so view
956    // caching can make a correct reuse decision.
957    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            // Record the root's coordinate space for placement/collision logic (anchored overlays).
1084            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            // See `render_root`: on the first cache-hit frame for a previously dirty root, ensure the
1121            // overlay subtree stays alive even if it won't rerender this frame.
1122            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            // See `render_root`: cache-hit frames can skip re-mounting cached subtrees, so we sweep
1140            // only detached nodes that have been stale beyond the configured lag window.
1141            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        // A deferred scroll request means we must run a contained relayout for this cache root,
1479        // even when the child render closure was skipped (cache hit).
1480        //
1481        // Importantly, do *not* disable reuse here: when the render closure is skipped the
1482        // declarative element list is intentionally empty, and treating that as authoritative would
1483        // detach the retained subtree (breaking semantics + scripted interactions).
1484        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            // If a cache root transitions into reuse without having a recorded subtree list yet,
1710            // fall back to walking the retained subtree so GC liveness bookkeeping remains
1711            // correct on the first cache-hit frame.
1712            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        // View-cache reuse skips rerendering declarative closures, so component-owned action hooks
1750        // (stored as element state) must be kept alive explicitly while the cached subtree
1751        // remains interactive.
1752        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        // Keep a complete retained-subtree element list for this cache root so cache-hit frames
1846        // can refresh liveness without re-running the render closure.
1847        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                // View-cache reuse can skip declarative re-mounting of subtrees, which means the
1913                // element runtime may not have a fresh `NodeEntry` mapping for all elements that
1914                // still exist in the current `WindowFrame`. Retained-host reconciles must remain
1915                // viable on cache-hit frames, so fall back to scanning the frame and (if found)
1916                // refresh the runtime mapping.
1917                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                // Prefer the prepaint-derived window range (ADR 0175). This lets retained
1989                // virtual surfaces update row membership on cache-hit frames without
1990                // re-deriving the window from scroll state during reconcile.
1991                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            // We allow `keep_alive_order` to contain duplicates to keep the per-detach hot path O(1)
2172            // (no `retain` scans). Eviction skips keys that no longer exist in the map.
2173            //
2174            // Occasionally compact to keep memory bounded and preserve an LRU-like ordering for
2175            // evictions (least recent detached keys at the front).
2176            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        // Retained virtual-list reconcile mutates descendants under a live cache root without
2216        // rerendering the cache-root closure. Refresh ancestor cache-root membership lists so
2217        // later cache-hit frames touch the updated subtree rather than an older visible window.
2218        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        // Newly mounted nodes already start invalidated (layout/paint/hit-test) and structural
2253        // changes are handled via parent `set_children` updates. Avoid redundant invalidation
2254        // propagation in large rerender frames (e.g. VirtualList window jumps).
2255        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            // Container padding/border affect child layout (box-sizing: border-box).
2269            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            // Presence/interactivity gates affect layout participation, hit-testing, focus traversal,
2290            // and semantics inclusion. Even when the wrapper layout is unchanged, we need a layout
2291            // refresh so the host widget can recompute its derived flags.
2292            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            // Render transforms affect paint and hit-testing. We treat them as a layout refresh so
2330            // the retained tree updates its per-node transform stack for hit-test/debug queries.
2331            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            // Fractional transforms are resolved during layout (dependent on bounds), but any input
2341            // change requires a layout refresh so we can recompute the pixel transform.
2342            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            // Anchored placement is resolved during layout and affects the render transform stack.
2351            // Treat any meaningful input change as requiring a layout refresh.
2352            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        // Intentionally omit `ui.set_children(root_node, ..)` so `UiTree` has no child edges.
2765        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    // GC reachability should be conservative: a retained subtree can temporarily have incomplete
3039    // `UiTree` child edges (eg. during view-cache reuse) while the `WindowFrame` still retains the
3040    // authoritative element-tree edges. Prefer the union of both sources so we don't misclassify
3041    // a still-live subtree as detached.
3042    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}