use super::frame::layout_style_for_instance;
use super::frame::{
DismissibleLayerProps, ElementFrame, ElementInstance, ElementRecord, WindowFrame,
};
use super::host_widget::ElementHostWidget;
use super::prelude::*;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use crate::tree::{UiDebugInvalidationDetail, UiDebugInvalidationSource};
fn keep_alive_view_cache_scratch_disabled() -> bool {
crate::runtime_config::ui_runtime_config().keep_alive_view_cache_scratch_disabled
}
fn validate_element_tree_unique_ids_enabled() -> (bool, bool) {
let strict = crate::strict_runtime::strict_runtime_enabled();
let cfg = crate::runtime_config::ui_runtime_config();
let enabled = strict
|| cfg.validate_element_tree_unique_ids
|| cfg.validate_element_tree_unique_ids_panic;
let should_panic = strict || cfg.validate_element_tree_unique_ids_panic;
(enabled, should_panic)
}
pub(super) fn element_tree_duplicate_ids(elements: &[AnyElement]) -> Vec<GlobalElementId> {
let mut seen: HashSet<GlobalElementId> = HashSet::new();
let mut duplicates: HashSet<GlobalElementId> = HashSet::new();
let mut stack: Vec<&AnyElement> = Vec::new();
stack.extend(elements.iter());
while let Some(el) = stack.pop() {
if !seen.insert(el.id) {
duplicates.insert(el.id);
}
stack.extend(el.children.iter());
}
let mut out: Vec<GlobalElementId> = duplicates.into_iter().collect();
out.sort_by_key(|id| id.0);
out
}
enum GcNodeRetentionDecision {
Keep,
Drop,
NeedLayerReachability,
}
fn gc_node_retention_decision(
id: GlobalElementId,
entry_node: NodeId,
entry_last_seen_frame: &mut FrameId,
entry_root: GlobalElementId,
root_id: GlobalElementId,
frame_id: FrameId,
cutoff: u64,
keep_alive_view_cache_elements: &HashSet<GlobalElementId>,
reachable_from_layers: Option<&HashSet<NodeId>>,
reachable_from_view_cache_roots_active: bool,
reachable_from_view_cache_roots: &HashSet<NodeId>,
) -> GcNodeRetentionDecision {
if id == root_id {
return GcNodeRetentionDecision::Keep;
}
if entry_root != root_id {
return GcNodeRetentionDecision::Keep;
}
if !keep_alive_view_cache_elements.is_empty() && keep_alive_view_cache_elements.contains(&id) {
*entry_last_seen_frame = frame_id;
return GcNodeRetentionDecision::Keep;
}
if entry_last_seen_frame.0 >= cutoff {
return GcNodeRetentionDecision::Keep;
}
if reachable_from_view_cache_roots_active
&& reachable_from_view_cache_roots.contains(&entry_node)
{
return GcNodeRetentionDecision::Keep;
}
let Some(reachable_from_layers) = reachable_from_layers else {
return GcNodeRetentionDecision::NeedLayerReachability;
};
if reachable_from_layers.contains(&entry_node) {
return GcNodeRetentionDecision::Keep;
}
GcNodeRetentionDecision::Drop
}
fn collect_live_retained_keep_alive_roots<H: UiHost>(
ui: &UiTree<H>,
window_state: &mut crate::elements::WindowElementState,
) -> Vec<NodeId> {
window_state.retain_retained_virtual_list_keep_alive_roots(|node| ui.node_exists(node));
window_state
.retained_virtual_list_keep_alive_roots()
.collect()
}
fn element_resolves_live_attached_node<H: UiHost>(
ui: &UiTree<H>,
window_state: &crate::elements::WindowElementState,
element: GlobalElementId,
) -> bool {
let seeded = window_state.node_entry(element).map(|entry| entry.node);
ui.resolve_live_attached_node_for_element_seeded(element, seeded)
.is_some()
}
fn collect_keep_alive_view_cache_elements_in_place<H: UiHost>(
ui: &UiTree<H>,
window_state: &crate::elements::WindowElementState,
out: &mut HashSet<GlobalElementId>,
visited_roots: &mut HashSet<GlobalElementId>,
stack: &mut Vec<GlobalElementId>,
) {
out.clear();
visited_roots.clear();
stack.clear();
stack.extend(window_state.view_cache_reuse_roots());
while let Some(root) = stack.pop() {
if !visited_roots.insert(root) {
continue;
}
if !element_resolves_live_attached_node(ui, window_state, root) {
continue;
}
let Some(elements) = window_state.view_cache_elements_for_root(root) else {
continue;
};
for &element in elements {
if !element_resolves_live_attached_node(ui, window_state, element) {
continue;
}
out.insert(element);
if !visited_roots.contains(&element)
&& window_state.view_cache_elements_for_root(element).is_some()
{
stack.push(element);
}
}
}
}
fn debug_path_for_element(
runtime: &crate::elements::ElementRuntime,
window: AppWindowId,
element: GlobalElementId,
) -> Option<String> {
#[cfg(feature = "diagnostics")]
{
runtime.debug_path_for_element(window, element)
}
#[cfg(not(feature = "diagnostics"))]
{
let _ = (runtime, window, element);
None
}
}
fn validate_element_tree_unique_ids_or_log(
window: AppWindowId,
root_name: &str,
frame_id: fret_runtime::FrameId,
runtime: &crate::elements::ElementRuntime,
elements: &[AnyElement],
) {
let (enabled, should_panic) = validate_element_tree_unique_ids_enabled();
if !enabled {
return;
}
let duplicates = element_tree_duplicate_ids(elements);
if duplicates.is_empty() {
return;
}
let mut msg = String::new();
use std::fmt::Write;
let _ = writeln!(
&mut msg,
"duplicate element ids detected while building declarative element tree: window={window:?} root_name={root_name:?} frame_id={}",
frame_id.0
);
for (idx, id) in duplicates.iter().take(12).enumerate() {
let path = debug_path_for_element(runtime, window, *id);
let _ = writeln!(&mut msg, " {idx}. element={id:?} debug_path={path:?}");
}
if duplicates.len() > 12 {
let _ = writeln!(&mut msg, " ... ({} more)", duplicates.len() - 12);
}
let _ = writeln!(
&mut msg,
"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"
);
if should_panic {
panic!("{msg}");
}
tracing::error!("{msg}");
}
#[cfg(feature = "unstable-retained-bridge")]
#[derive(Default)]
struct RetainedSubtreeHostState {
root: Option<NodeId>,
}
pub struct RenderRootContext<'a, H: UiHost> {
pub ui: &'a mut UiTree<H>,
pub app: &'a mut H,
pub services: &'a mut dyn fret_core::UiServices,
pub window: AppWindowId,
pub bounds: Rect,
}
impl<'a, H: UiHost + 'static> RenderRootContext<'a, H> {
pub fn new(
ui: &'a mut UiTree<H>,
app: &'a mut H,
services: &'a mut dyn fret_core::UiServices,
window: AppWindowId,
bounds: Rect,
) -> Self {
Self {
ui,
app,
services,
window,
bounds,
}
}
pub fn render_root<I>(
self,
root_name: &str,
render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
) -> NodeId
where
I: IntoIterator<Item = AnyElement>,
{
crate::declarative::render_root(
self.ui,
self.app,
self.services,
self.window,
self.bounds,
root_name,
render,
)
}
pub fn render_dismissible_root_with_hooks<I>(
self,
root_name: &str,
render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
) -> NodeId
where
I: IntoIterator<Item = AnyElement>,
{
crate::declarative::render_dismissible_root_with_hooks(
self.ui,
self.app,
self.services,
self.window,
self.bounds,
root_name,
render,
)
}
}
pub(crate) fn with_window_frame<H: UiHost, R>(
app: &mut H,
window: AppWindowId,
f: impl FnOnce(Option<&WindowFrame>) -> R,
) -> R {
app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
f(frame.windows.get(&window))
})
}
pub(crate) fn node_for_element_in_window_frame<H: UiHost>(
app: &mut H,
window: AppWindowId,
element: GlobalElementId,
) -> Option<NodeId> {
with_window_frame(app, window, |window_frame| {
let window_frame = window_frame?;
window_frame
.instances
.iter()
.find_map(|(node, record)| (record.element == element).then_some(node))
})
}
#[derive(Clone, Copy)]
struct StaleNodeRecord {
node: NodeId,
element: GlobalElementId,
#[cfg(feature = "diagnostics")]
element_root: GlobalElementId,
}
fn prepare_window_frame_for_frame(window_frame: &mut WindowFrame, frame_id: FrameId) {
if window_frame.frame_id != frame_id {
window_frame.frame_id = frame_id;
}
}
fn sync_window_frame_children(window_frame: &mut WindowFrame, parent: NodeId, children: &[NodeId]) {
if let Some(prev) = window_frame.children.get(parent)
&& prev.as_ref() == children
{
return;
}
window_frame
.children
.insert(parent, Arc::<[NodeId]>::from(children));
}
pub(crate) fn children_for_node_in_window_frame<H: UiHost>(
app: &mut H,
window: AppWindowId,
node: NodeId,
) -> Vec<NodeId> {
with_window_frame(app, window, |window_frame| {
window_frame
.and_then(|w| w.children.get(node))
.map(|children| children.as_ref().to_vec())
.unwrap_or_default()
})
}
pub(crate) fn node_contains_in_window_frame<H: UiHost>(
app: &mut H,
window: AppWindowId,
root: NodeId,
needle: NodeId,
) -> bool {
if root == needle {
return true;
}
let mut stack = vec![root];
while let Some(node) = stack.pop() {
let children = children_for_node_in_window_frame(app, window, node);
for child in children {
if child == needle {
return true;
}
stack.push(child);
}
}
false
}
pub fn render_root<H, I>(
ui: &mut UiTree<H>,
app: &mut H,
services: &mut dyn fret_core::UiServices,
window: AppWindowId,
bounds: Rect,
root_name: &str,
render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
) -> NodeId
where
H: UiHost + 'static,
I: IntoIterator<Item = AnyElement>,
{
let frame_id = app.frame_id();
#[cfg(debug_assertions)]
ui.debug_note_declarative_render_root_called(frame_id);
let focused = ui.focus();
ui.begin_debug_frame_if_needed(frame_id);
app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, _app| {
runtime.prepare_window_for_frame(window, frame_id);
let window_state = runtime.for_window_mut(window);
window_state.record_committed_viewport_bounds(bounds);
if let Some(svc) = _app.global::<fret_core::window::WindowMetricsService>() {
let scale_factor = svc.scale_factor(window).unwrap_or(1.0);
window_state.record_committed_scale_factor(scale_factor);
if svc.safe_area_insets_is_known(window) {
window_state.record_committed_safe_area_insets(svc.safe_area_insets(window));
}
if svc.occlusion_insets_is_known(window) {
window_state.record_committed_occlusion_insets(svc.occlusion_insets(window));
}
} else {
window_state.record_committed_scale_factor(1.0);
}
});
ui.invalidate_scroll_handle_bindings_for_changed_handles(
app,
crate::layout_pass::LayoutPassKind::Final,
false,
false,
);
let ui_ref: &UiTree<H> = &*ui;
let children: Vec<AnyElement> =
app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
runtime.prepare_window_for_frame(window, frame_id);
let mut should_reuse_view_cache =
|node: NodeId| ui_ref.should_reuse_view_cache_node(node);
let mut cx = crate::elements::ElementContext::new_for_root_name(
app, runtime, window, bounds, root_name,
);
cx.set_view_cache_should_reuse(&mut should_reuse_view_cache);
cx.sync_focused_element_from_focused_node(focused);
cx.dismissible_clear_on_dismiss_request();
cx.dismissible_clear_on_pointer_move();
let built = render(&mut cx);
let children = cx.collect_children(built);
validate_element_tree_unique_ids_or_log(
window, root_name, frame_id, &*runtime, &children,
);
children
});
let root_node =
app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
runtime.prepare_window_for_frame(window, frame_id);
let lag = runtime.gc_lag_frames();
let cutoff = frame_id.0.saturating_sub(lag);
let window_state = runtime.for_window_mut(window);
ui.debug_set_element_children_vec_pool_stats(
window_state.element_children_vec_pool_reuses(),
window_state.element_children_vec_pool_misses(),
);
let root_id = crate::elements::global_root(window, root_name);
let mut scroll_bindings: Vec<crate::declarative::frame::ScrollHandleBinding> =
Vec::new();
let seeded_root = window_state.node_entry(root_id).map(|e| e.node);
let root_node = ui
.resolve_reusable_node_for_element_seeded(root_id, seeded_root)
.unwrap_or_else(|| {
let node = ui.create_node(ElementHostWidget::new(root_id));
ui.set_node_element(node, Some(root_id));
window_state.set_node_entry(
root_id,
NodeEntry {
node,
last_seen_frame: frame_id,
root: root_id,
},
);
node
});
ui.set_node_element(root_node, Some(root_id));
window_state.set_node_entry(
root_id,
NodeEntry {
node: root_node,
last_seen_frame: frame_id,
root: root_id,
},
);
if ui.node_layer(root_node).is_none() && ui.base_root().is_none() {
ui.set_root(root_node);
}
let mut pending_invalidations = ui.take_scratch_pending_invalidations();
pending_invalidations.clear();
app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
let window_frame = frame.windows.entry(window).or_default();
prepare_window_frame_for_frame(window_frame, frame_id);
let mut root_stack = crate::element::StackProps::default();
root_stack.layout.size.width = crate::element::Length::Fill;
root_stack.layout.size.height = crate::element::Length::Fill;
let inserted = window_frame
.instances
.insert(
root_node,
ElementRecord {
element: root_id,
instance: ElementInstance::Stack(root_stack),
inherited_foreground: None,
inherited_text_style: None,
semantics_decoration: None,
key_context: None,
},
)
.is_none();
let mut mounted_children: Vec<NodeId> = Vec::with_capacity(children.len());
let mut children = children;
for child in children.drain(..) {
mounted_children.push(mount_element(
ui,
window,
root_id,
frame_id,
window_state,
window_frame,
child,
None,
&mut scroll_bindings,
&mut pending_invalidations,
));
}
ui.set_children(root_node, mounted_children);
sync_window_frame_children(window_frame, root_node, ui.children_ref(root_node));
if inserted {
window_frame.revision = window_frame.revision.saturating_add(1);
}
window_state.restore_scratch_element_children_vec(children);
let retained_virtual_lists = window_state.take_retained_virtual_list_reconciles();
if !retained_virtual_lists.is_empty() {
reconcile_retained_virtual_list_hosts(
ui,
_app,
window,
bounds,
root_id,
frame_id,
window_state,
window_frame,
&mut scroll_bindings,
&mut pending_invalidations,
retained_virtual_lists,
);
}
});
if ui.view_cache_enabled() {
let _ = ui.repair_parent_pointers_from_layer_roots();
}
apply_pending_invalidations(ui, &mut pending_invalidations);
ui.restore_scratch_pending_invalidations(pending_invalidations);
if ui.view_cache_enabled() {
ui.propagate_auto_sized_view_cache_root_invalidations();
}
for element in window_state.take_notify_for_animation_frame() {
let seeded = window_state.node_entry(element).map(|e| e.node);
if let Some(node) =
ui.resolve_live_attached_node_for_element_seeded(element, seeded)
{
ui.invalidate_with_source_and_detail(
node,
Invalidation::Paint,
UiDebugInvalidationSource::Notify,
UiDebugInvalidationDetail::AnimationFrameRequest,
);
}
}
crate::declarative::frame::register_scroll_handle_bindings_batch(
app,
window,
frame_id,
scroll_bindings,
);
window_state.set_root_bounds(root_id, bounds);
let mut keep_alive_view_cache_elements: HashSet<GlobalElementId>;
let keep_alive_view_cache_elements_from_scratch: bool;
if keep_alive_view_cache_scratch_disabled() {
keep_alive_view_cache_elements = HashSet::new();
keep_alive_view_cache_elements_from_scratch = false;
let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
let mut stack: Vec<GlobalElementId> = Vec::new();
collect_keep_alive_view_cache_elements_in_place(
ui,
window_state,
&mut keep_alive_view_cache_elements,
&mut visited_roots,
&mut stack,
);
} else {
keep_alive_view_cache_elements =
window_state.take_scratch_view_cache_keep_alive_elements();
keep_alive_view_cache_elements_from_scratch = true;
{
let mut visited_roots =
window_state.take_scratch_view_cache_keep_alive_visited_roots();
let mut stack = window_state.take_scratch_view_cache_keep_alive_stack();
collect_keep_alive_view_cache_elements_in_place(
ui,
window_state,
&mut keep_alive_view_cache_elements,
&mut visited_roots,
&mut stack,
);
window_state.restore_scratch_view_cache_keep_alive_visited_roots(visited_roots);
window_state.restore_scratch_view_cache_keep_alive_stack(stack);
}
}
if window_state
.view_cache_transitioned_reuse_roots()
.next()
.is_some()
{
with_window_frame(app, window, |window_frame| {
touch_existing_declarative_subtree_seen(
ui,
window_state,
window_frame,
root_id,
frame_id,
root_node,
);
});
}
let liveness_roots = ui.all_layer_roots();
let keep_alive_roots = collect_live_retained_keep_alive_roots(ui, window_state);
let mut stale: Vec<StaleNodeRecord> = Vec::new();
let mut reachable_from_layers = ui.take_scratch_gc_reachable_from_layers();
reachable_from_layers.clear();
let mut reachable_from_layers_computed = false;
let view_cache_has_reuse_roots = window_state.view_cache_reuse_roots().next().is_some();
let mut reachable_from_view_cache_roots =
ui.take_scratch_gc_reachable_from_view_cache_roots();
reachable_from_view_cache_roots.clear();
let mut reachable_from_view_cache_roots_active = false;
let mut gc_stack = ui.take_scratch_gc_stack();
gc_stack.clear();
if view_cache_has_reuse_roots {
let view_cache_reuse_roots: Vec<GlobalElementId> =
window_state.view_cache_reuse_roots().collect();
let view_cache_reuse_root_nodes: Vec<NodeId> = view_cache_reuse_roots
.iter()
.filter_map(|root| {
let seeded = window_state.node_entry(*root).map(|e| e.node);
ui.resolve_live_attached_node_for_element_seeded(*root, seeded)
})
.collect();
if !view_cache_reuse_root_nodes.is_empty() {
with_window_frame(app, window, |window_frame| {
collect_reachable_nodes_for_gc_in_place(
ui,
window_frame,
view_cache_reuse_root_nodes.iter().copied(),
&mut reachable_from_view_cache_roots,
&mut gc_stack,
);
});
}
for root in view_cache_reuse_roots {
if let Some(elements) = window_state.view_cache_elements_for_root(root) {
for &element in elements {
let seeded = window_state.node_entry(element).map(|entry| entry.node);
if let Some(node) =
ui.resolve_live_attached_node_for_element_seeded(element, seeded)
{
reachable_from_view_cache_roots.insert(node);
}
}
}
}
reachable_from_view_cache_roots_active = true;
}
window_state.retain_nodes(|id, entry| {
let mut decision = gc_node_retention_decision(
*id,
entry.node,
&mut entry.last_seen_frame,
entry.root,
root_id,
frame_id,
cutoff,
&keep_alive_view_cache_elements,
reachable_from_layers_computed.then_some(&reachable_from_layers),
reachable_from_view_cache_roots_active,
&reachable_from_view_cache_roots,
);
if matches!(decision, GcNodeRetentionDecision::NeedLayerReachability) {
with_window_frame(app, window, |window_frame| {
if liveness_roots.is_empty() {
collect_reachable_nodes_for_gc_in_place(
ui,
window_frame,
std::iter::once(root_node).chain(keep_alive_roots.iter().copied()),
&mut reachable_from_layers,
&mut gc_stack,
);
} else {
collect_reachable_nodes_for_gc_in_place(
ui,
window_frame,
liveness_roots
.iter()
.copied()
.chain(keep_alive_roots.iter().copied()),
&mut reachable_from_layers,
&mut gc_stack,
)
}
});
reachable_from_layers_computed = true;
decision = gc_node_retention_decision(
*id,
entry.node,
&mut entry.last_seen_frame,
entry.root,
root_id,
frame_id,
cutoff,
&keep_alive_view_cache_elements,
Some(&reachable_from_layers),
reachable_from_view_cache_roots_active,
&reachable_from_view_cache_roots,
);
}
if matches!(decision, GcNodeRetentionDecision::Keep) {
return true;
}
stale.push(StaleNodeRecord {
node: entry.node,
element: *id,
#[cfg(feature = "diagnostics")]
element_root: entry.root,
});
false
});
for record in &stale {
window_state.forget_view_cache_subtree_elements(record.element);
}
for record in stale {
let node = record.node;
#[cfg(feature = "diagnostics")]
if let Some(ctx) = with_window_frame(app, window, |window_frame| {
let window_frame = window_frame?;
let parent = ui.node_parent(node);
let parent_frame_children = parent.and_then(|p| window_frame.children.get(p));
let root_reachable_from_layer_roots =
reachable_from_layers_computed && reachable_from_layers.contains(&node);
let root_reachable_from_view_cache_roots =
reachable_from_view_cache_roots_active
.then(|| reachable_from_view_cache_roots.contains(&node));
let view_cache_reuse_roots: Vec<GlobalElementId> =
window_state.view_cache_reuse_roots().collect();
let liveness_layer_roots_len =
liveness_roots.len().min(u32::MAX as usize) as u32;
let view_cache_reuse_roots_len =
view_cache_reuse_roots.len().min(u32::MAX as usize) as u32;
let view_cache_reuse_root_nodes_len = view_cache_reuse_roots
.iter()
.filter(|root| window_state.node_entry(**root).is_some())
.count()
.min(u32::MAX as usize)
as u32;
let mut path_edge_frame_contains_child: [u8; 16] = [2u8; 16];
let mut path_edge_len: u8 = 0;
let mut current = Some(node);
while let Some(child) = current {
let Some(parent) = ui.node_parent(child) else {
break;
};
if (path_edge_len as usize) >= path_edge_frame_contains_child.len() {
break;
}
let contains = window_frame
.children
.get(parent)
.map(|children| children.contains(&child));
path_edge_frame_contains_child[path_edge_len as usize] = match contains {
Some(true) => 1,
Some(false) => 0,
None => 2,
};
path_edge_len = path_edge_len.saturating_add(1);
current = Some(parent);
}
Some(crate::tree::UiDebugRemoveSubtreeFrameContext {
parent_frame_children_len: parent_frame_children
.map(|v| v.len().min(u32::MAX as usize) as u32),
parent_frame_children_contains_root: parent_frame_children
.map(|v| v.contains(&node)),
root_frame_instance_present: window_frame.instances.contains_key(node),
root_frame_children_len: window_frame
.children
.get(node)
.map(|v| v.len().min(u32::MAX as usize) as u32),
root_reachable_from_layer_roots,
root_reachable_from_view_cache_roots,
liveness_layer_roots_len,
view_cache_reuse_roots_len,
view_cache_reuse_root_nodes_len,
trigger_element: Some(record.element),
trigger_element_root: Some(record.element_root),
trigger_element_in_view_cache_keep_alive: Some(
keep_alive_view_cache_elements.contains(&record.element),
),
trigger_element_listed_under_reuse_root: window_state
.view_cache_reuse_roots()
.find(|&root| {
window_state
.view_cache_elements_for_root(root)
.is_some_and(|elements| elements.contains(&record.element))
}),
path_edge_len,
path_edge_frame_contains_child,
})
}) {
ui.debug_set_remove_subtree_frame_context(node, ctx);
}
let removed = ui.remove_subtree(services, node);
app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
let window_frame = frame.windows.entry(window).or_default();
let any_removed = !removed.is_empty();
for removed in removed {
window_frame.instances.remove(removed);
window_frame.children.remove(removed);
}
if any_removed {
window_frame.revision = window_frame.revision.saturating_add(1);
}
});
}
reachable_from_layers.clear();
reachable_from_view_cache_roots.clear();
gc_stack.clear();
ui.restore_scratch_gc_reachable_from_layers(reachable_from_layers);
ui.restore_scratch_gc_reachable_from_view_cache_roots(reachable_from_view_cache_roots);
ui.restore_scratch_gc_stack(gc_stack);
if keep_alive_view_cache_elements_from_scratch {
keep_alive_view_cache_elements.clear();
window_state
.restore_scratch_view_cache_keep_alive_elements(keep_alive_view_cache_elements);
}
if window_state.wants_continuous_frames() {
app.push_effect(Effect::RequestAnimationFrame(window));
}
root_node
});
ui.publish_window_runtime_snapshots(app);
root_node
}
#[allow(clippy::too_many_arguments)]
pub fn render_dismissible_root_with_hooks<H, I>(
ui: &mut UiTree<H>,
app: &mut H,
services: &mut dyn fret_core::UiServices,
window: AppWindowId,
bounds: Rect,
root_name: &str,
render: impl FnOnce(&mut ElementContext<'_, H>) -> I,
) -> NodeId
where
H: UiHost + 'static,
I: IntoIterator<Item = AnyElement>,
{
render_dismissible_root_impl(ui, app, services, window, bounds, root_name, render)
}
#[allow(clippy::too_many_arguments)]
fn render_dismissible_root_impl<H: UiHost + 'static, F, I>(
ui: &mut UiTree<H>,
app: &mut H,
services: &mut dyn fret_core::UiServices,
window: AppWindowId,
bounds: Rect,
root_name: &str,
render: F,
) -> NodeId
where
F: FnOnce(&mut ElementContext<'_, H>) -> I,
I: IntoIterator<Item = AnyElement>,
{
let frame_id = app.frame_id();
#[cfg(debug_assertions)]
ui.debug_note_declarative_render_root_called(frame_id);
let focused = ui.focus();
ui.begin_debug_frame_if_needed(frame_id);
ui.invalidate_scroll_handle_bindings_for_changed_handles(
app,
crate::layout_pass::LayoutPassKind::Final,
false,
false,
);
let ui_ref: &UiTree<H> = &*ui;
let children: Vec<AnyElement> =
app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
runtime.prepare_window_for_frame(window, frame_id);
let mut should_reuse_view_cache =
|node: NodeId| ui_ref.should_reuse_view_cache_node(node);
let mut cx = crate::elements::ElementContext::new_for_root_name(
app, runtime, window, bounds, root_name,
);
cx.set_view_cache_should_reuse(&mut should_reuse_view_cache);
cx.sync_focused_element_from_focused_node(focused);
cx.dismissible_clear_on_dismiss_request();
cx.dismissible_clear_on_pointer_move();
let built = render(&mut cx);
cx.collect_children(built)
});
let root_node =
app.with_global_mut_untracked(crate::elements::ElementRuntime::new, |runtime, app| {
runtime.prepare_window_for_frame(window, frame_id);
let lag = runtime.gc_lag_frames();
let cutoff = frame_id.0.saturating_sub(lag);
let window_state = runtime.for_window_mut(window);
ui.debug_set_element_children_vec_pool_stats(
window_state.element_children_vec_pool_reuses(),
window_state.element_children_vec_pool_misses(),
);
let root_id = crate::elements::global_root(window, root_name);
let mut scroll_bindings: Vec<crate::declarative::frame::ScrollHandleBinding> =
Vec::new();
let seeded_root = window_state.node_entry(root_id).map(|e| e.node);
let root_node = ui
.resolve_reusable_node_for_element_seeded(root_id, seeded_root)
.unwrap_or_else(|| {
let node = ui.create_node(ElementHostWidget::new(root_id));
ui.set_node_element(node, Some(root_id));
window_state.set_node_entry(
root_id,
NodeEntry {
node,
last_seen_frame: frame_id,
root: root_id,
},
);
node
});
ui.set_node_element(root_node, Some(root_id));
window_state.set_node_entry(
root_id,
NodeEntry {
node: root_node,
last_seen_frame: frame_id,
root: root_id,
},
);
let mut pending_invalidations = ui.take_scratch_pending_invalidations();
pending_invalidations.clear();
app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
let window_frame = frame.windows.entry(window).or_default();
prepare_window_frame_for_frame(window_frame, frame_id);
let inserted = window_frame
.instances
.insert(
root_node,
ElementRecord {
element: root_id,
instance: ElementInstance::DismissibleLayer(
DismissibleLayerProps::default(),
),
inherited_foreground: None,
inherited_text_style: None,
semantics_decoration: None,
key_context: None,
},
)
.is_none();
if inserted {
window_frame.revision = window_frame.revision.saturating_add(1);
}
let mut mounted_children: Vec<NodeId> = Vec::with_capacity(children.len());
let mut children = children;
for child in children.drain(..) {
mounted_children.push(mount_element(
ui,
window,
root_id,
frame_id,
window_state,
window_frame,
child,
None,
&mut scroll_bindings,
&mut pending_invalidations,
));
}
ui.set_children(root_node, mounted_children);
window_state.restore_scratch_element_children_vec(children);
});
if ui.view_cache_enabled() {
let _ = ui.repair_parent_pointers_from_layer_roots();
}
apply_pending_invalidations(ui, &mut pending_invalidations);
ui.restore_scratch_pending_invalidations(pending_invalidations);
crate::declarative::frame::register_scroll_handle_bindings_batch(
app,
window,
frame_id,
scroll_bindings,
);
window_state.set_root_bounds(root_id, bounds);
let mut keep_alive_view_cache_elements: HashSet<GlobalElementId>;
let keep_alive_view_cache_elements_from_scratch: bool;
if keep_alive_view_cache_scratch_disabled() {
keep_alive_view_cache_elements = HashSet::new();
keep_alive_view_cache_elements_from_scratch = false;
let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
let mut stack: Vec<GlobalElementId> = Vec::new();
collect_keep_alive_view_cache_elements_in_place(
ui,
window_state,
&mut keep_alive_view_cache_elements,
&mut visited_roots,
&mut stack,
);
} else {
keep_alive_view_cache_elements =
window_state.take_scratch_view_cache_keep_alive_elements();
keep_alive_view_cache_elements_from_scratch = true;
{
let mut visited_roots =
window_state.take_scratch_view_cache_keep_alive_visited_roots();
let mut stack = window_state.take_scratch_view_cache_keep_alive_stack();
collect_keep_alive_view_cache_elements_in_place(
ui,
window_state,
&mut keep_alive_view_cache_elements,
&mut visited_roots,
&mut stack,
);
window_state.restore_scratch_view_cache_keep_alive_visited_roots(visited_roots);
window_state.restore_scratch_view_cache_keep_alive_stack(stack);
}
}
if window_state
.view_cache_transitioned_reuse_roots()
.next()
.is_some()
{
with_window_frame(app, window, |window_frame| {
touch_existing_declarative_subtree_seen(
ui,
window_state,
window_frame,
root_id,
frame_id,
root_node,
);
});
}
let liveness_roots = ui.all_layer_roots();
let keep_alive_roots = collect_live_retained_keep_alive_roots(ui, window_state);
let mut stale: Vec<StaleNodeRecord> = Vec::new();
let mut reachable_from_layers = ui.take_scratch_gc_reachable_from_layers();
reachable_from_layers.clear();
let mut reachable_from_layers_computed = false;
let view_cache_has_reuse_roots = window_state.view_cache_reuse_roots().next().is_some();
let mut reachable_from_view_cache_roots =
ui.take_scratch_gc_reachable_from_view_cache_roots();
reachable_from_view_cache_roots.clear();
let mut reachable_from_view_cache_roots_active = false;
let mut gc_stack = ui.take_scratch_gc_stack();
gc_stack.clear();
if view_cache_has_reuse_roots {
let view_cache_reuse_roots: Vec<GlobalElementId> =
window_state.view_cache_reuse_roots().collect();
let view_cache_reuse_root_nodes: Vec<NodeId> = view_cache_reuse_roots
.iter()
.filter_map(|root| {
let seeded = window_state.node_entry(*root).map(|e| e.node);
ui.resolve_live_attached_node_for_element_seeded(*root, seeded)
})
.collect();
if !view_cache_reuse_root_nodes.is_empty() {
with_window_frame(app, window, |window_frame| {
collect_reachable_nodes_for_gc_in_place(
ui,
window_frame,
view_cache_reuse_root_nodes.iter().copied(),
&mut reachable_from_view_cache_roots,
&mut gc_stack,
);
});
}
for root in view_cache_reuse_roots {
if let Some(elements) = window_state.view_cache_elements_for_root(root) {
for &element in elements {
let seeded = window_state.node_entry(element).map(|entry| entry.node);
if let Some(node) =
ui.resolve_live_attached_node_for_element_seeded(element, seeded)
{
reachable_from_view_cache_roots.insert(node);
}
}
}
}
reachable_from_view_cache_roots_active = true;
}
window_state.retain_nodes(|id, entry| {
let mut decision = gc_node_retention_decision(
*id,
entry.node,
&mut entry.last_seen_frame,
entry.root,
root_id,
frame_id,
cutoff,
&keep_alive_view_cache_elements,
reachable_from_layers_computed.then_some(&reachable_from_layers),
reachable_from_view_cache_roots_active,
&reachable_from_view_cache_roots,
);
if matches!(decision, GcNodeRetentionDecision::NeedLayerReachability) {
with_window_frame(app, window, |window_frame| {
if liveness_roots.is_empty() {
collect_reachable_nodes_for_gc_in_place(
ui,
window_frame,
std::iter::once(root_node).chain(keep_alive_roots.iter().copied()),
&mut reachable_from_layers,
&mut gc_stack,
);
} else {
collect_reachable_nodes_for_gc_in_place(
ui,
window_frame,
liveness_roots
.iter()
.copied()
.chain(keep_alive_roots.iter().copied()),
&mut reachable_from_layers,
&mut gc_stack,
)
}
});
reachable_from_layers_computed = true;
decision = gc_node_retention_decision(
*id,
entry.node,
&mut entry.last_seen_frame,
entry.root,
root_id,
frame_id,
cutoff,
&keep_alive_view_cache_elements,
Some(&reachable_from_layers),
reachable_from_view_cache_roots_active,
&reachable_from_view_cache_roots,
);
}
if matches!(decision, GcNodeRetentionDecision::Keep) {
return true;
}
stale.push(StaleNodeRecord {
node: entry.node,
element: *id,
#[cfg(feature = "diagnostics")]
element_root: entry.root,
});
false
});
for record in &stale {
window_state.forget_view_cache_subtree_elements(record.element);
}
for record in stale {
let node = record.node;
#[cfg(feature = "diagnostics")]
if let Some(ctx) = with_window_frame(app, window, |window_frame| {
let window_frame = window_frame?;
let parent = ui.node_parent(node);
let parent_frame_children = parent.and_then(|p| window_frame.children.get(p));
let root_reachable_from_layer_roots =
reachable_from_layers_computed && reachable_from_layers.contains(&node);
let root_reachable_from_view_cache_roots =
reachable_from_view_cache_roots_active
.then(|| reachable_from_view_cache_roots.contains(&node));
let view_cache_reuse_roots: Vec<GlobalElementId> =
window_state.view_cache_reuse_roots().collect();
let liveness_layer_roots_len =
liveness_roots.len().min(u32::MAX as usize) as u32;
let view_cache_reuse_roots_len =
view_cache_reuse_roots.len().min(u32::MAX as usize) as u32;
let view_cache_reuse_root_nodes_len = view_cache_reuse_roots
.iter()
.filter(|root| window_state.node_entry(**root).is_some())
.count()
.min(u32::MAX as usize)
as u32;
let mut path_edge_frame_contains_child: [u8; 16] = [2u8; 16];
let mut path_edge_len: u8 = 0;
let mut current = Some(node);
while let Some(child) = current {
let Some(parent) = ui.node_parent(child) else {
break;
};
if (path_edge_len as usize) >= path_edge_frame_contains_child.len() {
break;
}
let contains = window_frame
.children
.get(parent)
.map(|children| children.contains(&child));
path_edge_frame_contains_child[path_edge_len as usize] = match contains {
Some(true) => 1,
Some(false) => 0,
None => 2,
};
path_edge_len = path_edge_len.saturating_add(1);
current = Some(parent);
}
Some(crate::tree::UiDebugRemoveSubtreeFrameContext {
parent_frame_children_len: parent_frame_children
.map(|v| v.len().min(u32::MAX as usize) as u32),
parent_frame_children_contains_root: parent_frame_children
.map(|v| v.contains(&node)),
root_frame_instance_present: window_frame.instances.contains_key(node),
root_frame_children_len: window_frame
.children
.get(node)
.map(|v| v.len().min(u32::MAX as usize) as u32),
root_reachable_from_layer_roots,
root_reachable_from_view_cache_roots,
liveness_layer_roots_len,
view_cache_reuse_roots_len,
view_cache_reuse_root_nodes_len,
trigger_element: Some(record.element),
trigger_element_root: Some(record.element_root),
trigger_element_in_view_cache_keep_alive: Some(
keep_alive_view_cache_elements.contains(&record.element),
),
trigger_element_listed_under_reuse_root: window_state
.view_cache_reuse_roots()
.find(|&root| {
window_state
.view_cache_elements_for_root(root)
.is_some_and(|elements| elements.contains(&record.element))
}),
path_edge_len,
path_edge_frame_contains_child,
})
}) {
ui.debug_set_remove_subtree_frame_context(node, ctx);
}
let removed = ui.remove_subtree(services, node);
app.with_global_mut_untracked(ElementFrame::default, |frame, _app| {
let window_frame = frame.windows.entry(window).or_default();
let any_removed = !removed.is_empty();
for removed in removed {
window_frame.instances.remove(removed);
window_frame.children.remove(removed);
}
if any_removed {
window_frame.revision = window_frame.revision.saturating_add(1);
}
});
}
reachable_from_layers.clear();
reachable_from_view_cache_roots.clear();
gc_stack.clear();
ui.restore_scratch_gc_reachable_from_layers(reachable_from_layers);
ui.restore_scratch_gc_reachable_from_view_cache_roots(reachable_from_view_cache_roots);
ui.restore_scratch_gc_stack(gc_stack);
if keep_alive_view_cache_elements_from_scratch {
keep_alive_view_cache_elements.clear();
window_state
.restore_scratch_view_cache_keep_alive_elements(keep_alive_view_cache_elements);
}
if window_state.wants_continuous_frames() {
app.push_effect(Effect::RequestAnimationFrame(window));
}
root_node
});
let root_attached = ui.node_layer(root_node).is_some() || ui.node_parent(root_node).is_some();
if root_attached {
ui.clear_declarative_window_snapshot_commit(root_node);
ui.publish_window_runtime_snapshots(app);
} else {
ui.defer_declarative_window_snapshot_commit(root_node);
}
root_node
}
#[allow(clippy::too_many_arguments)]
fn mount_element<H: UiHost + 'static>(
ui: &mut UiTree<H>,
_window: AppWindowId,
root_id: GlobalElementId,
frame_id: fret_runtime::FrameId,
window_state: &mut crate::elements::WindowElementState,
window_frame: &mut WindowFrame,
element: AnyElement,
parent_inherited_text_style: Option<fret_core::TextStyleRefinement>,
scroll_bindings: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
pending_invalidations: &mut HashMap<NodeId, u8>,
) -> NodeId {
let mut element = element;
let id = element.id;
let inherited_foreground = element.inherited_foreground;
let local_inherited_text_style = element.inherited_text_style.clone();
let inherited_text_style = match (
parent_inherited_text_style.as_ref(),
local_inherited_text_style.as_ref(),
) {
(Some(parent), Some(local)) => Some(parent.merged(local)),
(Some(parent), None) => Some(parent.clone()),
(None, Some(local)) => Some(local.clone()),
(None, None) => None,
}
.filter(|style| !style.is_empty());
let semantics_decoration = element.semantics_decoration.clone();
let key_context = element.key_context.clone();
let mut children = std::mem::take(&mut element.children);
let existing_node_entry = window_state.node_entry(id);
let had_existing_node_entry = existing_node_entry.is_some();
let had_existing_node = existing_node_entry
.map(|e| ui.node_exists(e.node))
.unwrap_or(false);
let view_cache_props = match &element.kind {
ElementKind::ViewCache(props) => Some(*props),
_ => None,
};
let reuse_view_cache =
view_cache_props.is_some() && window_state.should_reuse_view_cache_root(id);
#[cfg(feature = "unstable-retained-bridge")]
let retained_subtree_props = match &element.kind {
ElementKind::RetainedSubtree(props) => Some(props.clone()),
_ => None,
};
let span = if view_cache_props.is_some() && tracing::enabled!(tracing::Level::TRACE) {
tracing::trace_span!(
"ui.cache_root.mount",
element = ?id,
node = tracing::field::Empty,
cache_hit = reuse_view_cache,
contained_layout = view_cache_props
.map(|p| p.contained_layout)
.unwrap_or(false),
frame_id = frame_id.0,
)
} else {
tracing::Span::none()
};
let _span_guard = span.enter();
let seeded = window_state.node_entry(id).map(|e| e.node);
let node = ui
.resolve_reusable_node_for_element_seeded(id, seeded)
.unwrap_or_else(|| {
let node = ui.create_node(ElementHostWidget::new(id));
ui.set_node_element(node, Some(id));
window_state.set_node_entry(
id,
NodeEntry {
node,
last_seen_frame: frame_id,
root: root_id,
},
);
node
});
ui.set_node_element(node, Some(id));
window_state.set_node_entry(
id,
NodeEntry {
node,
last_seen_frame: frame_id,
root: root_id,
},
);
if reuse_view_cache
&& view_cache_root_needs_layout_for_deferred_scroll_requests(ui, window_frame, node)
{
ui.invalidate(node, Invalidation::Layout);
}
if view_cache_props.is_some() && tracing::enabled!(tracing::Level::TRACE) {
span.record("node", tracing::field::debug(node));
}
match &element.kind {
ElementKind::ViewCache(props) => {
let layout_definite = !matches!(props.layout.size.width, crate::element::Length::Auto)
&& !matches!(props.layout.size.height, crate::element::Length::Auto);
ui.set_node_view_cache_flags(node, true, props.contained_layout, layout_definite);
if !reuse_view_cache {
ui.set_node_view_cache_needs_rerender(node, false);
}
let reuse_reason = if !had_existing_node_entry {
crate::tree::UiDebugCacheRootReuseReason::FirstMount
} else if !had_existing_node {
crate::tree::UiDebugCacheRootReuseReason::NodeRecreated
} else if reuse_view_cache {
crate::tree::UiDebugCacheRootReuseReason::MarkedReuseRoot
} else if !ui.view_cache_enabled() {
crate::tree::UiDebugCacheRootReuseReason::ViewCacheDisabled
} else if ui.inspection_active() {
crate::tree::UiDebugCacheRootReuseReason::InspectionActive
} else if window_state.view_cache_key_mismatch(id) {
crate::tree::UiDebugCacheRootReuseReason::CacheKeyMismatch
} else if ui.view_cache_node_needs_rerender(node) {
crate::tree::UiDebugCacheRootReuseReason::NeedsRerender
} else if ui.node_layout_invalidated(node) {
crate::tree::UiDebugCacheRootReuseReason::LayoutInvalidated
} else {
crate::tree::UiDebugCacheRootReuseReason::NotMarkedReuseRoot
};
ui.debug_record_view_cache_root(
node,
reuse_view_cache,
props.contained_layout,
reuse_reason,
);
}
_ => {
ui.set_node_view_cache_flags(node, false, false, false);
}
}
match &element.kind {
ElementKind::TextInputRegion(props) => {
ui.set_node_text_boundary_mode_override(node, props.text_boundary_mode_override);
}
_ => {
ui.set_node_text_boundary_mode_override(node, None);
}
}
let instance = match element.kind {
ElementKind::Container(p) => ElementInstance::Container(p),
ElementKind::Semantics(p) => ElementInstance::Semantics(p),
ElementKind::SemanticFlex(p) => ElementInstance::SemanticFlex(p),
ElementKind::FocusScope(p) => ElementInstance::FocusScope(p),
ElementKind::LayoutQueryRegion(p) => ElementInstance::LayoutQueryRegion(p),
ElementKind::InteractivityGate(p) => ElementInstance::InteractivityGate(p),
ElementKind::HitTestGate(p) => ElementInstance::HitTestGate(p),
ElementKind::FocusTraversalGate(p) => ElementInstance::FocusTraversalGate(p),
ElementKind::ForegroundScope(p) => ElementInstance::ForegroundScope(p),
ElementKind::Opacity(p) => ElementInstance::Opacity(p),
ElementKind::EffectLayer(p) => ElementInstance::EffectLayer(p),
ElementKind::BackdropSourceGroup(p) => ElementInstance::BackdropSourceGroup(p),
ElementKind::MaskLayer(p) => ElementInstance::MaskLayer(p),
ElementKind::CompositeGroup(p) => ElementInstance::CompositeGroup(p),
ElementKind::ViewCache(p) => ElementInstance::ViewCache(p),
ElementKind::VisualTransform(p) => ElementInstance::VisualTransform(p),
ElementKind::RenderTransform(p) => ElementInstance::RenderTransform(p),
ElementKind::FractionalRenderTransform(p) => ElementInstance::FractionalRenderTransform(p),
ElementKind::Anchored(p) => ElementInstance::Anchored(p),
ElementKind::Pressable(p) => ElementInstance::Pressable(p),
ElementKind::PointerRegion(p) => ElementInstance::PointerRegion(p),
ElementKind::TextInputRegion(p) => ElementInstance::TextInputRegion(p),
ElementKind::InternalDragRegion(p) => ElementInstance::InternalDragRegion(p),
ElementKind::ExternalDragRegion(p) => ElementInstance::ExternalDragRegion(p),
ElementKind::RovingFlex(p) => ElementInstance::RovingFlex(p),
ElementKind::Stack(p) => ElementInstance::Stack(p),
ElementKind::Column(p) => ElementInstance::Flex(FlexProps {
layout: p.layout,
direction: fret_core::Axis::Vertical,
gap: p.gap,
padding: p.padding,
justify: p.justify,
align: p.align,
wrap: false,
}),
ElementKind::Row(p) => ElementInstance::Flex(FlexProps {
layout: p.layout,
direction: fret_core::Axis::Horizontal,
gap: p.gap,
padding: p.padding,
justify: p.justify,
align: p.align,
wrap: false,
}),
ElementKind::Spacer(p) => ElementInstance::Spacer(p),
ElementKind::Text(p) => ElementInstance::Text(p),
ElementKind::StyledText(p) => ElementInstance::StyledText(p),
ElementKind::SelectableText(p) => ElementInstance::SelectableText(p),
ElementKind::TextInput(p) => ElementInstance::TextInput(p),
ElementKind::TextArea(p) => ElementInstance::TextArea(p),
ElementKind::ResizablePanelGroup(p) => ElementInstance::ResizablePanelGroup(p),
ElementKind::VirtualList(p) => ElementInstance::VirtualList(p),
ElementKind::Flex(p) => ElementInstance::Flex(p),
ElementKind::Grid(p) => ElementInstance::Grid(p),
ElementKind::Image(p) => ElementInstance::Image(p),
ElementKind::Canvas(p) => ElementInstance::Canvas(p),
#[cfg(feature = "unstable-retained-bridge")]
ElementKind::RetainedSubtree(p) => ElementInstance::RetainedSubtree(p),
ElementKind::ViewportSurface(p) => ElementInstance::ViewportSurface(p),
ElementKind::SvgIcon(p) => ElementInstance::SvgIcon(p),
ElementKind::Spinner(p) => ElementInstance::Spinner(p),
ElementKind::HoverRegion(p) => ElementInstance::HoverRegion(p),
ElementKind::WheelRegion(p) => ElementInstance::WheelRegion(p),
ElementKind::Scroll(p) => ElementInstance::Scroll(p),
ElementKind::Scrollbar(p) => ElementInstance::Scrollbar(p),
};
collect_scroll_handle_bindings(id, &instance, scroll_bindings);
let interactivity_gate_state = match &instance {
ElementInstance::InteractivityGate(p) => Some((p.present, p.interactive)),
_ => None,
};
let hit_test_gate_state = match &instance {
ElementInstance::HitTestGate(p) => Some(p.hit_test),
_ => None,
};
let focus_traversal_gate_state = match &instance {
ElementInstance::FocusTraversalGate(p) => Some(p.traverse),
_ => None,
};
let use_barrier_set_children = matches!(
&instance,
ElementInstance::VirtualList(props) if virtual_list_can_be_layout_barrier(props)
);
let previous_record = window_frame.instances.get(node);
let previous_instance = previous_record.map(|r| &r.instance);
let previous_inherited_text_style =
previous_record.and_then(|r| r.inherited_text_style.as_ref());
if !reuse_view_cache {
let mut mask = declarative_instance_change_mask(previous_instance, &instance);
if previous_inherited_text_style != inherited_text_style.as_ref() {
mask |= INVALIDATION_LAYOUT | INVALIDATION_PAINT;
}
if ui.interactive_resize_active() && ui.hover_edge_changed_this_frame() {
mask &= !INVALIDATION_LAYOUT;
}
if mask != 0 {
ui.debug_record_hover_declarative_invalidation(
node,
(mask & INVALIDATION_HIT_TEST) != 0,
(mask & INVALIDATION_LAYOUT) != 0,
(mask & INVALIDATION_PAINT) != 0,
);
pending_invalidations
.entry(node)
.and_modify(|m| *m |= mask)
.or_insert(mask);
}
}
if let Some((present, interactive)) = interactivity_gate_state {
ui.sync_interactivity_gate_widget(node, present, interactive);
}
if let Some(hit_test) = hit_test_gate_state {
ui.sync_hit_test_gate_widget(node, hit_test);
}
if let Some(traverse) = focus_traversal_gate_state {
ui.sync_focus_traversal_gate_widget(node, traverse);
}
let inserted = window_frame
.instances
.insert(
node,
ElementRecord {
element: id,
instance,
inherited_foreground,
inherited_text_style: inherited_text_style.clone(),
semantics_decoration,
key_context,
},
)
.is_none();
if inserted {
window_frame.revision = window_frame.revision.saturating_add(1);
}
if reuse_view_cache {
let reuse_span = if tracing::enabled!(tracing::Level::TRACE) {
tracing::trace_span!(
"ui.cache_root.reuse",
element = ?id,
node = ?node,
cache_hit = true,
contained_layout = view_cache_props
.map(|p| p.contained_layout)
.unwrap_or(false),
frame_id = frame_id.0,
reason = "marked_reuse_root",
)
} else {
tracing::Span::none()
};
let _reuse_guard = reuse_span.enter();
if window_frame.children.get(node).is_none() {
sync_window_frame_children(window_frame, node, ui.children_ref(node));
}
let transitioned_into_reuse = window_state.record_view_cache_reuse_frame(id, frame_id);
window_state.touch_view_cache_authoring_identities_if_recorded(id);
let touched = window_state.touch_view_cache_subtree_elements_if_recorded(
id,
frame_id,
root_id,
|element, seeded| ui.resolve_live_attached_node_for_element_seeded(element, seeded),
);
if transitioned_into_reuse && !touched {
mark_existing_declarative_subtree_seen(
ui,
window_state,
window_frame,
root_id,
frame_id,
node,
);
window_state.record_view_cache_subtree_elements(
id,
collect_declarative_elements_for_existing_subtree(
ui,
window_state,
window_frame,
node,
),
);
} else if !touched {
mark_existing_declarative_subtree_seen(
ui,
window_state,
window_frame,
root_id,
frame_id,
node,
);
window_state.record_view_cache_subtree_elements(
id,
collect_declarative_elements_for_existing_subtree(
ui,
window_state,
window_frame,
node,
),
);
}
window_state.touch_view_cache_action_hook_state_for_subtree_elements(id);
inherit_observations_for_existing_subtree(ui, window_state, window_frame, node);
collect_scroll_handle_bindings_for_existing_subtree(
ui,
window_frame,
scroll_bindings,
node,
);
window_state.restore_scratch_element_children_vec(children);
return node;
}
#[cfg(feature = "unstable-retained-bridge")]
if let Some(props) = retained_subtree_props {
if !element.children.is_empty() {
tracing::warn!(
element = ?id,
children = element.children.len(),
"RetainedSubtree ignores declarative children (expected leaf element)",
);
}
let retained_root = window_state.with_state_mut(
id,
RetainedSubtreeHostState::default,
|st: &mut RetainedSubtreeHostState| {
if let Some(root) = st.root
&& ui.node_exists(root)
{
return root;
}
let root = props.factory.build(ui);
st.root = Some(root);
root
},
);
let child_nodes = vec![retained_root];
if had_existing_node {
ui.set_children(node, child_nodes.clone());
} else {
ui.set_children_in_mount(node, child_nodes.clone());
}
window_frame
.children
.insert(node, Arc::<[NodeId]>::from(child_nodes));
return node;
}
if view_cache_props.is_some() {
let reuse_span = if tracing::enabled!(tracing::Level::TRACE) {
tracing::trace_span!(
"ui.cache_root.reuse",
element = ?id,
node = ?node,
cache_hit = false,
contained_layout = view_cache_props
.map(|p| p.contained_layout)
.unwrap_or(false),
frame_id = frame_id.0,
reason = "not_marked_reuse_root",
)
} else {
tracing::Span::none()
};
let _reuse_guard = reuse_span.enter();
let mut child_nodes: Vec<NodeId> = Vec::with_capacity(children.len());
for child in children.drain(..) {
child_nodes.push(mount_element(
ui,
_window,
root_id,
frame_id,
window_state,
window_frame,
child,
inherited_text_style.clone(),
scroll_bindings,
pending_invalidations,
));
}
if use_barrier_set_children {
ui.set_children_barrier(node, child_nodes);
} else if had_existing_node {
ui.set_children(node, child_nodes);
} else {
ui.set_children_in_mount(node, child_nodes);
}
sync_window_frame_children(window_frame, node, ui.children_ref(node));
window_state.record_view_cache_subtree_elements(
id,
collect_declarative_elements_for_existing_subtree(ui, window_state, window_frame, node),
);
} else {
let mut child_nodes: Vec<NodeId> = Vec::with_capacity(children.len());
for child in children.drain(..) {
child_nodes.push(mount_element(
ui,
_window,
root_id,
frame_id,
window_state,
window_frame,
child,
inherited_text_style.clone(),
scroll_bindings,
pending_invalidations,
));
}
if use_barrier_set_children {
ui.set_children_barrier(node, child_nodes);
} else if had_existing_node {
ui.set_children(node, child_nodes);
} else {
ui.set_children_in_mount(node, child_nodes);
}
sync_window_frame_children(window_frame, node, ui.children_ref(node));
}
window_state.restore_scratch_element_children_vec(children);
node
}
#[allow(clippy::too_many_arguments)]
fn reconcile_retained_virtual_list_hosts<H: UiHost + 'static>(
ui: &mut UiTree<H>,
app: &mut H,
window: AppWindowId,
bounds: Rect,
root_id: GlobalElementId,
frame_id: FrameId,
window_state: &mut crate::elements::WindowElementState,
window_frame: &mut WindowFrame,
scroll_bindings: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
pending_invalidations: &mut HashMap<NodeId, u8>,
elements: Vec<(
GlobalElementId,
crate::tree::UiDebugRetainedVirtualListReconcileKind,
)>,
) {
if elements.is_empty() {
return;
}
enum RetainedVirtualListReconcileItems {
Ready(Vec<crate::virtual_list::VirtualItem>),
DeferUntilViewportKnown,
}
for (element, reconcile_kind) in elements {
let seeded = window_state.node_entry(element).map(|e| e.node);
let node = ui
.resolve_live_attached_node_for_element_seeded(element, seeded)
.or_else(|| {
let node = window_frame
.instances
.iter()
.find_map(|(node, record)| (record.element == element).then_some(node))?;
window_state.set_node_entry(
element,
NodeEntry {
node,
last_seen_frame: frame_id,
root: root_id,
},
);
Some(node)
});
let Some(node) = node else {
continue;
};
if seeded != Some(node) {
window_state.set_node_entry(
element,
NodeEntry {
node,
last_seen_frame: frame_id,
root: root_id,
},
);
}
let Some(record) = window_frame.instances.get(node) else {
continue;
};
let ElementInstance::VirtualList(props) = &record.instance else {
continue;
};
if !virtual_list_can_be_layout_barrier(props) {
continue;
}
let props = props.clone();
let Some((key_at, row, range_extractor)) = window_state
.try_with_state_mut::<crate::windowed_surface_host::RetainedVirtualListHostCallbacks<H>, _>(
element,
|st| (Arc::clone(&st.key_at), Arc::clone(&st.row), st.range_extractor),
)
else {
continue;
};
let desired_items = window_state.with_state_mut(
element,
crate::element::VirtualListState::default,
|state| {
state.metrics.ensure_with_mode(
props.measure_mode,
props.len,
props.estimate_row_height,
props.gap,
props.scroll_margin,
);
if props.len == 0 {
state.window_range = None;
state.render_window_range = None;
return RetainedVirtualListReconcileItems::Ready(Vec::new());
}
let viewport = match props.axis {
fret_core::Axis::Vertical => Px(state.viewport_h.0.max(0.0)),
fret_core::Axis::Horizontal => Px(state.viewport_w.0.max(0.0)),
};
let mut window_range =
state
.window_range
.or(state.render_window_range)
.filter(|r| {
r.count == props.len
&& r.overscan == props.overscan
&& r.start_index <= r.end_index
&& r.end_index < r.count
});
if window_range.is_none() {
if viewport.0 <= 0.0 {
return RetainedVirtualListReconcileItems::DeferUntilViewportKnown;
}
let offset_point = props.scroll_handle.offset();
let offset_axis = match props.axis {
fret_core::Axis::Vertical => offset_point.y,
fret_core::Axis::Horizontal => offset_point.x,
};
let offset_axis = state.metrics.clamp_offset(offset_axis, viewport);
window_range =
state
.metrics
.visible_range(offset_axis, viewport, props.overscan);
}
let Some(range) = window_range else {
return RetainedVirtualListReconcileItems::DeferUntilViewportKnown;
};
state.window_range = Some(range);
state.render_window_range = Some(range);
let mut indices = (range_extractor)(range)
.into_iter()
.filter(|&idx| idx < props.len)
.collect::<Vec<_>>();
indices.sort_unstable();
indices.dedup();
let items = indices
.iter()
.copied()
.map(|idx| {
let key = (key_at)(idx);
state.metrics.virtual_item(idx, key)
})
.collect::<Vec<_>>();
RetainedVirtualListReconcileItems::Ready(items)
},
);
let desired_items = match desired_items {
RetainedVirtualListReconcileItems::Ready(items) => items,
RetainedVirtualListReconcileItems::DeferUntilViewportKnown => {
window_state.mark_retained_virtual_list_needs_reconcile(element, reconcile_kind);
ui.request_redraw_coalesced(app);
continue;
}
};
let reconcile_start = fret_core::time::Instant::now();
let prev_items_len = props.visible_items.len();
let next_items_len = desired_items.len();
let keep_alive_budget = props.keep_alive;
let desired_keys: HashSet<crate::ItemKey> =
desired_items.iter().map(|item| item.key).collect();
let mut existing_by_key: HashMap<crate::ItemKey, NodeId> = HashMap::new();
let mut detached_by_key: Vec<(crate::ItemKey, NodeId)> = Vec::new();
{
let current_children = ui.children(node);
for (&child, item) in current_children.iter().zip(props.visible_items.iter()) {
existing_by_key.insert(item.key, child);
}
if keep_alive_budget > 0 {
for (&child, item) in current_children.iter().zip(props.visible_items.iter()) {
if !desired_keys.contains(&item.key) {
detached_by_key.push((item.key, child));
}
}
}
}
let mut keep_alive_state = window_state.with_state_mut(
element,
crate::windowed_surface_host::RetainedVirtualListKeepAliveState::default,
std::mem::take,
);
let mut keep_alive_by_key: HashMap<crate::ItemKey, NodeId> = keep_alive_state.by_key;
let mut keep_alive_order = keep_alive_state.order;
let keep_alive_pool_len_before = keep_alive_by_key.len().min(u32::MAX as usize) as u32;
let mut preserved: u32 = 0;
let mut attached: u32 = 0;
let mut reused_from_keep_alive: u32 = 0;
let mut kept_alive: u32 = 0;
let mut evicted_keep_alive: u32 = 0;
let mut next_children: Vec<NodeId> = Vec::with_capacity(desired_items.len());
for item in &desired_items {
if let Some(existing) = existing_by_key.get(&item.key).copied() {
next_children.push(existing);
preserved = preserved.saturating_add(1);
continue;
}
if let Some(existing) = keep_alive_by_key.remove(&item.key) {
window_state.remove_retained_virtual_list_keep_alive_root(existing);
next_children.push(existing);
preserved = preserved.saturating_add(1);
reused_from_keep_alive = reused_from_keep_alive.saturating_add(1);
continue;
}
attached = attached.saturating_add(1);
let child_element = {
let mut cx = crate::elements::ElementContext::new_for_existing_window_state(
app,
window,
bounds,
element,
window_state,
);
let ui_ref: &UiTree<H> = &*ui;
let mut should_reuse_view_cache =
|node: NodeId| ui_ref.should_reuse_view_cache_node(node);
cx.set_view_cache_should_reuse(&mut should_reuse_view_cache);
cx.retained_virtual_list_row_any_element(item.key, item.index, &row)
};
let child_node = mount_element(
ui,
window,
root_id,
frame_id,
window_state,
window_frame,
child_element,
None,
scroll_bindings,
pending_invalidations,
);
next_children.push(child_node);
}
let detached =
(prev_items_len.saturating_sub(preserved as usize)).min(u32::MAX as usize) as u32;
if keep_alive_budget == 0 {
if !keep_alive_by_key.is_empty() {
for node in keep_alive_by_key.values().copied() {
window_state.remove_retained_virtual_list_keep_alive_root(node);
}
keep_alive_by_key.clear();
}
keep_alive_order.clear();
} else {
for (key, child) in detached_by_key {
if let Some(prev) = keep_alive_by_key.remove(&key) {
window_state.remove_retained_virtual_list_keep_alive_root(prev);
}
keep_alive_by_key.insert(key, child);
keep_alive_order.push_back(key);
window_state.add_retained_virtual_list_keep_alive_root(child);
kept_alive = kept_alive.saturating_add(1);
while keep_alive_by_key.len() > keep_alive_budget {
let Some(evict_key) = keep_alive_order.pop_front() else {
break;
};
let Some(evicted) = keep_alive_by_key.remove(&evict_key) else {
continue;
};
window_state.remove_retained_virtual_list_keep_alive_root(evicted);
evicted_keep_alive = evicted_keep_alive.saturating_add(1);
}
}
let order_len = keep_alive_order.len();
let budget = keep_alive_budget.max(1);
if order_len > budget.saturating_mul(16).saturating_add(256) {
let mut seen: HashSet<crate::ItemKey> = HashSet::new();
let mut compact_rev: Vec<crate::ItemKey> =
Vec::with_capacity(keep_alive_by_key.len());
for &k in keep_alive_order.iter().rev() {
if !keep_alive_by_key.contains_key(&k) {
continue;
}
if seen.insert(k) {
compact_rev.push(k);
}
}
compact_rev.reverse();
keep_alive_order = VecDeque::from(compact_rev);
}
}
let keep_alive_pool_len_after = keep_alive_by_key.len().min(u32::MAX as usize) as u32;
keep_alive_state.by_key = keep_alive_by_key;
keep_alive_state.order = keep_alive_order;
window_state.with_state_mut(
element,
crate::windowed_surface_host::RetainedVirtualListKeepAliveState::default,
|st| *st = keep_alive_state,
);
ui.set_children_barrier(node, next_children.clone());
window_frame
.children
.insert(node, Arc::<[NodeId]>::from(next_children));
if let Some(record) = window_frame.instances.get_mut(node)
&& let ElementInstance::VirtualList(props) = &mut record.instance
{
props.visible_items = desired_items;
}
refresh_view_cache_membership_for_ancestor_roots(ui, window_state, window_frame, node);
let reconcile_time_us = reconcile_start.elapsed().as_micros().min(u32::MAX as u128) as u32;
ui.debug_record_retained_virtual_list_reconcile(
crate::tree::UiDebugRetainedVirtualListReconcile {
node,
element,
reconcile_kind,
reconcile_time_us,
prev_items: prev_items_len.min(u32::MAX as usize) as u32,
next_items: next_items_len.min(u32::MAX as usize) as u32,
preserved_items: preserved,
attached_items: attached,
detached_items: detached,
keep_alive_pool_len_before,
reused_from_keep_alive_items: reused_from_keep_alive,
kept_alive_items: kept_alive,
evicted_keep_alive_items: evicted_keep_alive,
keep_alive_pool_len_after,
},
);
}
}
const INVALIDATION_HIT_TEST: u8 = 1 << 0;
const INVALIDATION_LAYOUT: u8 = 1 << 1;
const INVALIDATION_PAINT: u8 = 1 << 2;
fn declarative_instance_change_mask(
previous: Option<&ElementInstance>,
next: &ElementInstance,
) -> u8 {
let Some(previous) = previous else {
return 0;
};
if std::mem::discriminant(previous) != std::mem::discriminant(next) {
return INVALIDATION_HIT_TEST | INVALIDATION_LAYOUT | INVALIDATION_PAINT;
}
let mut hit_test_changed = false;
let mut layout_changed = layout_style_for_instance(previous) != layout_style_for_instance(next);
let mut paint_changed = false;
match (previous, next) {
(ElementInstance::Container(a), ElementInstance::Container(b)) => {
if a.padding != b.padding || a.border != b.border {
layout_changed = true;
}
if a.background != b.background
|| a.background_paint != b.background_paint
|| a.shadow != b.shadow
|| a.border_color != b.border_color
|| a.border_paint != b.border_paint
|| a.border_dash != b.border_dash
|| a.focus_ring != b.focus_ring
|| a.focus_border_color != b.focus_border_color
|| a.focus_within != b.focus_within
|| a.corner_radii != b.corner_radii
|| a.snap_to_device_pixels != b.snap_to_device_pixels
{
paint_changed = true;
}
}
(ElementInstance::InteractivityGate(a), ElementInstance::InteractivityGate(b)) => {
if a.present != b.present || a.interactive != b.interactive {
layout_changed = true;
paint_changed = true;
}
}
(ElementInstance::HitTestGate(a), ElementInstance::HitTestGate(b)) => {
if a.hit_test != b.hit_test {
hit_test_changed = true;
}
}
(ElementInstance::FocusTraversalGate(a), ElementInstance::FocusTraversalGate(b)) => {
if a.traverse != b.traverse {
layout_changed = true;
paint_changed = true;
}
}
(ElementInstance::Opacity(a), ElementInstance::Opacity(b)) => {
if a.opacity != b.opacity {
paint_changed = true;
}
}
(ElementInstance::MaskLayer(a), ElementInstance::MaskLayer(b)) => {
if a.mask != b.mask {
paint_changed = true;
}
}
(ElementInstance::CompositeGroup(a), ElementInstance::CompositeGroup(b)) => {
if a.mode != b.mode || a.quality != b.quality {
paint_changed = true;
}
}
(ElementInstance::VisualTransform(a), ElementInstance::VisualTransform(b)) => {
if a.transform != b.transform {
paint_changed = true;
}
}
(ElementInstance::RenderTransform(a), ElementInstance::RenderTransform(b)) => {
if a.transform != b.transform {
layout_changed = true;
paint_changed = true;
}
}
(
ElementInstance::FractionalRenderTransform(a),
ElementInstance::FractionalRenderTransform(b),
) => {
if a.translate_x_fraction != b.translate_x_fraction
|| a.translate_y_fraction != b.translate_y_fraction
{
layout_changed = true;
paint_changed = true;
}
}
(ElementInstance::Anchored(a), ElementInstance::Anchored(b)) => {
if a.outer_margin != b.outer_margin || a.anchor != b.anchor || a.options != b.options {
layout_changed = true;
paint_changed = true;
}
}
(ElementInstance::Text(a), ElementInstance::Text(b)) => {
if a.text != b.text
|| a.style != b.style
|| a.color != b.color
|| a.wrap != b.wrap
|| a.overflow != b.overflow
{
layout_changed = true;
paint_changed = true;
}
}
(ElementInstance::StyledText(a), ElementInstance::StyledText(b)) => {
if a.rich != b.rich
|| a.style != b.style
|| a.color != b.color
|| a.wrap != b.wrap
|| a.overflow != b.overflow
{
layout_changed = true;
paint_changed = true;
}
}
(ElementInstance::SelectableText(a), ElementInstance::SelectableText(b)) => {
if a.rich != b.rich
|| a.style != b.style
|| a.color != b.color
|| a.wrap != b.wrap
|| a.overflow != b.overflow
{
layout_changed = true;
paint_changed = true;
}
}
_ => {}
}
let mut mask = 0;
if hit_test_changed {
mask |= INVALIDATION_HIT_TEST;
}
if layout_changed {
mask |= INVALIDATION_HIT_TEST | INVALIDATION_LAYOUT | INVALIDATION_PAINT;
} else if paint_changed {
mask |= INVALIDATION_PAINT;
}
mask
}
fn virtual_list_can_be_layout_barrier(props: &crate::element::VirtualListProps) -> bool {
match props.axis {
fret_core::Axis::Vertical => {
!matches!(props.layout.size.height, crate::element::Length::Auto)
}
fret_core::Axis::Horizontal => {
!matches!(props.layout.size.width, crate::element::Length::Auto)
}
}
}
fn apply_pending_invalidations<H: UiHost>(ui: &mut UiTree<H>, pending: &mut HashMap<NodeId, u8>) {
for (node, mask) in pending.drain() {
if (mask & INVALIDATION_HIT_TEST) != 0 {
ui.invalidate(node, Invalidation::HitTest);
}
if (mask & INVALIDATION_LAYOUT) != 0 {
ui.invalidate(node, Invalidation::Layout);
}
if (mask & INVALIDATION_PAINT) != 0 {
ui.invalidate(node, Invalidation::Paint);
}
}
}
fn mark_existing_declarative_subtree_seen<H: UiHost>(
ui: &UiTree<H>,
window_state: &mut crate::elements::WindowElementState,
window_frame: &WindowFrame,
root_id: GlobalElementId,
frame_id: FrameId,
root: NodeId,
) {
let mut stack: Vec<NodeId> = vec![root];
while let Some(node) = stack.pop() {
if !ui.node_exists(node) {
continue;
}
if let Some(element) = window_frame
.instances
.get(node)
.map(|r| r.element)
.or_else(|| ui.node_element(node))
.or_else(|| window_state.element_for_node(node))
{
let root = window_state
.node_entry(element)
.map(|e| e.root)
.unwrap_or(root_id);
window_state.set_node_entry(
element,
NodeEntry {
node,
last_seen_frame: frame_id,
root,
},
);
#[cfg(feature = "diagnostics")]
window_state.touch_debug_identity_for_element(frame_id, element);
}
push_existing_subtree_children(ui, window_frame, node, &mut stack);
}
}
fn touch_existing_declarative_subtree_seen<H: UiHost>(
ui: &UiTree<H>,
window_state: &mut crate::elements::WindowElementState,
window_frame: Option<&WindowFrame>,
root_id: GlobalElementId,
frame_id: FrameId,
root: NodeId,
) {
let mut stack: Vec<NodeId> = vec![root];
while let Some(node) = stack.pop() {
if !ui.node_exists(node) {
continue;
}
if let Some(element) = window_frame
.and_then(|window_frame| window_frame.instances.get(node).map(|r| r.element))
.or_else(|| ui.node_element(node))
.or_else(|| window_state.element_for_node(node))
{
let root = window_state
.node_entry(element)
.map(|e| e.root)
.unwrap_or(root_id);
window_state.set_node_entry(
element,
NodeEntry {
node,
last_seen_frame: frame_id,
root,
},
);
#[cfg(feature = "diagnostics")]
window_state.touch_debug_identity_for_element(frame_id, element);
}
if let Some(window_frame) = window_frame {
push_existing_subtree_children(ui, window_frame, node, &mut stack);
} else {
for child in ui.children(node) {
stack.push(child);
}
}
}
}
fn collect_declarative_elements_for_existing_subtree<H: UiHost>(
ui: &UiTree<H>,
window_state: &crate::elements::WindowElementState,
window_frame: &WindowFrame,
root: NodeId,
) -> Vec<GlobalElementId> {
let mut out: Vec<GlobalElementId> = Vec::new();
let mut seen: HashSet<GlobalElementId> = HashSet::new();
let mut stack: Vec<NodeId> = vec![root];
while let Some(node) = stack.pop() {
if !ui.node_exists(node) {
continue;
}
if let Some(element) = window_frame
.instances
.get(node)
.map(|r| r.element)
.or_else(|| ui.node_element(node))
.or_else(|| window_state.element_for_node(node))
&& seen.insert(element)
{
out.push(element);
}
push_existing_subtree_children(ui, window_frame, node, &mut stack);
}
out
}
fn refresh_view_cache_membership_for_ancestor_roots<H: UiHost>(
ui: &UiTree<H>,
window_state: &mut crate::elements::WindowElementState,
window_frame: &WindowFrame,
node: NodeId,
) {
let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
let mut current = Some(node);
while let Some(node) = current {
if let Some(record) = window_frame.instances.get(node)
&& matches!(record.instance, ElementInstance::ViewCache(_))
&& visited_roots.insert(record.element)
{
window_state.record_view_cache_subtree_elements(
record.element,
collect_declarative_elements_for_existing_subtree(
ui,
window_state,
window_frame,
node,
),
);
}
current = ui.node_parent(node);
}
}
#[cfg(test)]
fn collect_reachable_nodes_for_gc<H: UiHost>(
ui: &UiTree<H>,
window_frame: Option<&WindowFrame>,
roots: impl IntoIterator<Item = NodeId>,
) -> HashSet<NodeId> {
let mut out: HashSet<NodeId> = HashSet::new();
let mut stack: Vec<NodeId> = Vec::new();
collect_reachable_nodes_for_gc_in_place(ui, window_frame, roots, &mut out, &mut stack);
out
}
fn collect_reachable_nodes_for_gc_in_place<H: UiHost>(
ui: &UiTree<H>,
window_frame: Option<&WindowFrame>,
roots: impl IntoIterator<Item = NodeId>,
out: &mut HashSet<NodeId>,
stack: &mut Vec<NodeId>,
) {
out.clear();
stack.clear();
stack.extend(roots);
while let Some(node) = stack.pop() {
if !ui.node_exists(node) {
continue;
}
if !out.insert(node) {
continue;
}
if let Some(window_frame) = window_frame {
push_existing_subtree_children(ui, window_frame, node, stack);
} else {
stack.extend(ui.children(node));
}
}
}
fn collect_scroll_handle_bindings_for_existing_subtree<H: UiHost>(
ui: &UiTree<H>,
window_frame: &WindowFrame,
out: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
root: NodeId,
) {
let mut stack: Vec<NodeId> = vec![root];
while let Some(node) = stack.pop() {
if let Some(record) = window_frame.instances.get(node) {
collect_scroll_handle_bindings(record.element, &record.instance, out);
}
push_existing_subtree_children(ui, window_frame, node, &mut stack);
}
}
#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod tests {
use super::*;
use fret_core::{PathConstraints, PathMetrics, PathStyle, TextConstraints, TextMetrics};
#[derive(Default)]
struct TestWidget;
impl<H: UiHost> Widget<H> for TestWidget {
fn layout(&mut self, cx: &mut LayoutCx<'_, H>) -> Size {
for &child in cx.children {
let _ = cx.layout_in(child, cx.bounds);
}
cx.available
}
fn paint(&mut self, _cx: &mut PaintCx<'_, H>) {}
}
#[derive(Default)]
struct FakeUiServices;
impl fret_core::TextService for FakeUiServices {
fn prepare(
&mut self,
_input: &fret_core::TextInput,
_constraints: TextConstraints,
) -> (fret_core::TextBlobId, TextMetrics) {
(
fret_core::TextBlobId::default(),
TextMetrics {
size: Size::new(fret_core::Px(10.0), fret_core::Px(10.0)),
baseline: fret_core::Px(8.0),
},
)
}
fn release(&mut self, _blob: fret_core::TextBlobId) {}
}
impl fret_core::PathService for FakeUiServices {
fn prepare(
&mut self,
_commands: &[fret_core::PathCommand],
_style: PathStyle,
_constraints: PathConstraints,
) -> (fret_core::PathId, PathMetrics) {
(fret_core::PathId::default(), PathMetrics::default())
}
fn release(&mut self, _path: fret_core::PathId) {}
}
impl fret_core::SvgService for FakeUiServices {
fn register_svg(&mut self, _bytes: &[u8]) -> fret_core::SvgId {
fret_core::SvgId::default()
}
fn unregister_svg(&mut self, _svg: fret_core::SvgId) -> bool {
false
}
}
impl fret_core::MaterialService for FakeUiServices {
fn register_material(
&mut self,
_desc: fret_core::MaterialDescriptor,
) -> Result<fret_core::MaterialId, fret_core::MaterialRegistrationError> {
Err(fret_core::MaterialRegistrationError::Unsupported)
}
fn unregister_material(&mut self, _id: fret_core::MaterialId) -> bool {
false
}
}
#[test]
fn gc_reachability_unions_ui_and_window_frame_children() {
use fret_runtime::FrameId;
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
ui.set_window(AppWindowId::default());
ui.set_debug_enabled(true);
ui.begin_debug_frame_if_needed(FrameId(1));
let root = ui.create_node(TestWidget);
let ui_child = ui.create_node(TestWidget);
let frame_child = ui.create_node(TestWidget);
ui.set_root(root);
ui.set_children(root, vec![ui_child]);
let mut window_frame = WindowFrame::default();
window_frame
.children
.insert(root, Arc::<[NodeId]>::from(vec![ui_child, frame_child]));
let reachable = collect_reachable_nodes_for_gc(&ui, Some(&window_frame), [root]);
assert!(reachable.contains(&root));
assert!(reachable.contains(&ui_child));
assert!(reachable.contains(&frame_child));
}
#[test]
fn touch_existing_subtree_can_walk_window_frame_children() {
use crate::UiHost;
use crate::declarative::frame::WindowFrame;
use crate::tree::UiTree;
use crate::widget::{LayoutCx, PaintCx, Widget};
use fret_runtime::FrameId;
#[derive(Default)]
struct TestWidget;
impl<H: UiHost> Widget<H> for TestWidget {
fn layout(&mut self, cx: &mut LayoutCx<'_, H>) -> Size {
for &child in cx.children {
let _ = cx.layout_in(child, cx.bounds);
}
cx.available
}
fn paint(&mut self, _cx: &mut PaintCx<'_, H>) {}
}
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
ui.set_window(AppWindowId::default());
let root_node = ui.create_node(TestWidget);
let child_node = ui.create_node(TestWidget);
let root_element = GlobalElementId(1);
let child_element = GlobalElementId(2);
let root_id = GlobalElementId(999);
ui.set_node_element(root_node, Some(root_element));
ui.set_node_element(child_node, Some(child_element));
let mut window_frame = WindowFrame::default();
window_frame
.children
.insert(root_node, Arc::<[NodeId]>::from(vec![child_node]));
let mut window_state = crate::elements::WindowElementState::default();
touch_existing_declarative_subtree_seen(
&ui,
&mut window_state,
Some(&window_frame),
root_id,
FrameId(1),
root_node,
);
let entry = window_state
.node_entry(child_element)
.expect("child touched");
assert_eq!(entry.node, child_node);
assert_eq!(entry.last_seen_frame, FrameId(1));
assert_eq!(entry.root, root_id);
}
#[test]
fn gc_retention_ignores_stale_parent_pointer_layer_membership() {
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
ui.set_window(AppWindowId::default());
let root = ui.create_node(TestWidget);
let stale = ui.create_node(TestWidget);
let root_id = GlobalElementId(900);
let stale_id = GlobalElementId(901);
let frame_id = FrameId(2);
let cutoff = 1;
ui.set_root(root);
ui.test_set_node_parent(stale, Some(root));
assert!(
ui.node_layer(stale).is_some(),
"reproducer requires a stale parent path that still resolves to a layer"
);
let reachable = collect_reachable_nodes_for_gc(&ui, None, [root]);
assert!(
!reachable.contains(&stale),
"stale node must stay unreachable from authoritative children traversal"
);
let keep_alive_view_cache_elements: HashSet<GlobalElementId> = HashSet::new();
let reachable_from_view_cache_roots: HashSet<NodeId> = HashSet::new();
let mut last_seen_frame = FrameId(0);
assert!(matches!(
gc_node_retention_decision(
stale_id,
stale,
&mut last_seen_frame,
root_id,
root_id,
frame_id,
cutoff,
&keep_alive_view_cache_elements,
None,
false,
&reachable_from_view_cache_roots,
),
GcNodeRetentionDecision::NeedLayerReachability
));
assert!(matches!(
gc_node_retention_decision(
stale_id,
stale,
&mut last_seen_frame,
root_id,
root_id,
frame_id,
cutoff,
&keep_alive_view_cache_elements,
Some(&reachable),
false,
&reachable_from_view_cache_roots,
),
GcNodeRetentionDecision::Drop
));
}
#[test]
fn gc_prunes_removed_retained_keep_alive_roots_before_reachability() {
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
ui.set_window(AppWindowId::default());
let root = ui.create_node(TestWidget);
let stale = ui.create_node(TestWidget);
let root_id = GlobalElementId(950);
let stale_id = GlobalElementId(951);
let frame_id = FrameId(2);
let cutoff = 1;
ui.set_root(root);
let mut services = FakeUiServices;
let removed = ui.remove_subtree(&mut services, stale);
assert_eq!(
removed,
vec![stale],
"expected stale keep-alive root to be removed"
);
assert!(
!ui.node_exists(stale),
"removed keep-alive roots must not remain live in UiTree"
);
let raw_reachable = collect_reachable_nodes_for_gc(&ui, None, [stale]);
assert!(
raw_reachable.is_empty(),
"dead NodeId roots must not be treated as reachable by the GC walk"
);
let mut window_state = crate::elements::WindowElementState::default();
window_state.add_retained_virtual_list_keep_alive_root(stale);
window_state.set_node_entry(
stale_id,
NodeEntry {
node: stale,
last_seen_frame: FrameId(0),
root: root_id,
},
);
let keep_alive_roots = collect_live_retained_keep_alive_roots(&ui, &mut window_state);
assert!(
keep_alive_roots.is_empty(),
"removed keep-alive roots must be pruned before they participate in GC liveness"
);
assert!(
window_state
.retained_virtual_list_keep_alive_roots()
.next()
.is_none(),
"pruning should update the retained keep-alive root set itself"
);
let reachable = collect_reachable_nodes_for_gc(
&ui,
None,
std::iter::once(root).chain(keep_alive_roots.iter().copied()),
);
assert!(
!reachable.contains(&stale),
"a removed keep-alive root must not keep the stale node reachable"
);
let keep_alive_view_cache_elements: HashSet<GlobalElementId> = HashSet::new();
let reachable_from_view_cache_roots: HashSet<NodeId> = HashSet::new();
let mut last_seen_frame = FrameId(0);
assert!(matches!(
gc_node_retention_decision(
stale_id,
stale,
&mut last_seen_frame,
root_id,
root_id,
frame_id,
cutoff,
&keep_alive_view_cache_elements,
Some(&reachable),
false,
&reachable_from_view_cache_roots,
),
GcNodeRetentionDecision::Drop
));
}
#[test]
fn keep_alive_view_cache_membership_ignores_stale_nested_cache_roots() {
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
ui.set_window(AppWindowId::default());
let outer_node = ui.create_node(TestWidget);
let stale_inner_node = ui.create_node(TestWidget);
let stale_leaf_node = ui.create_node(TestWidget);
let outer = GlobalElementId(960);
let inner = GlobalElementId(961);
let leaf = GlobalElementId(962);
ui.set_root(outer_node);
ui.set_node_element(outer_node, Some(outer));
ui.set_node_element(stale_inner_node, Some(inner));
ui.set_node_element(stale_leaf_node, Some(leaf));
let mut window_state = crate::elements::WindowElementState::default();
window_state.set_node_entry(
outer,
NodeEntry {
node: outer_node,
last_seen_frame: FrameId(1),
root: outer,
},
);
window_state.set_node_entry(
inner,
NodeEntry {
node: stale_inner_node,
last_seen_frame: FrameId(0),
root: outer,
},
);
window_state.set_node_entry(
leaf,
NodeEntry {
node: stale_leaf_node,
last_seen_frame: FrameId(0),
root: outer,
},
);
window_state.mark_view_cache_reuse_root(outer);
window_state.record_view_cache_subtree_elements(outer, vec![outer, inner, leaf]);
window_state.record_view_cache_subtree_elements(inner, vec![inner, leaf]);
let mut keep_alive_view_cache_elements: HashSet<GlobalElementId> = HashSet::new();
let mut visited_roots: HashSet<GlobalElementId> = HashSet::new();
let mut stack: Vec<GlobalElementId> = Vec::new();
collect_keep_alive_view_cache_elements_in_place(
&ui,
&window_state,
&mut keep_alive_view_cache_elements,
&mut visited_roots,
&mut stack,
);
assert!(
keep_alive_view_cache_elements.contains(&outer),
"expected live reused cache root to stay in the keep-alive closure"
);
assert!(
!keep_alive_view_cache_elements.contains(&inner),
"stale nested cache root membership must not recurse into detached roots"
);
assert!(
!keep_alive_view_cache_elements.contains(&leaf),
"stale nested descendants must not remain in the keep-alive closure"
);
}
}
fn view_cache_root_needs_layout_for_deferred_scroll_requests<H: UiHost>(
ui: &UiTree<H>,
window_frame: &WindowFrame,
root: NodeId,
) -> bool {
let mut stack: Vec<NodeId> = vec![root];
while let Some(node) = stack.pop() {
if let Some(record) = window_frame.instances.get(node)
&& let ElementInstance::VirtualList(props) = &record.instance
&& props.scroll_handle.deferred_scroll_to_item().is_some()
{
return true;
}
push_existing_subtree_children(ui, window_frame, node, &mut stack);
}
false
}
fn push_existing_subtree_children<H: UiHost>(
ui: &UiTree<H>,
window_frame: &WindowFrame,
node: NodeId,
stack: &mut Vec<NodeId>,
) {
let ui_children = ui.children(node);
if !ui_children.is_empty() {
stack.extend(ui_children.iter().copied());
}
if let Some(frame_children) = window_frame.children.get(node) {
if ui_children.is_empty() {
stack.extend(frame_children.iter().copied());
} else {
for &child in frame_children.iter() {
if !ui_children.contains(&child) {
stack.push(child);
}
}
}
}
}
fn inherit_observations_for_existing_subtree<H: UiHost>(
ui: &UiTree<H>,
window_state: &mut crate::elements::WindowElementState,
window_frame: &WindowFrame,
root: NodeId,
) {
let mut stack: Vec<NodeId> = vec![root];
while let Some(node) = stack.pop() {
if let Some(record) = window_frame.instances.get(node) {
let element = record.element;
window_state.touch_observed_models_for_element_if_recorded(element);
window_state.touch_observed_globals_for_element_if_recorded(element);
if matches!(record.instance, ElementInstance::ViewCache(_)) {
window_state.touch_view_cache_state_keys_if_recorded(element);
}
}
push_existing_subtree_children(ui, window_frame, node, &mut stack);
}
}
fn collect_scroll_handle_bindings(
element: GlobalElementId,
instance: &ElementInstance,
out: &mut Vec<crate::declarative::frame::ScrollHandleBinding>,
) {
match instance {
ElementInstance::VirtualList(props) => {
let handle = props.scroll_handle.base_handle();
out.push(crate::declarative::frame::ScrollHandleBinding {
handle_key: handle.binding_key(),
element,
handle: handle.clone(),
});
}
ElementInstance::Scroll(props) => {
if let Some(handle) = props.scroll_handle.as_ref() {
out.push(crate::declarative::frame::ScrollHandleBinding {
handle_key: handle.binding_key(),
element,
handle: handle.clone(),
});
}
}
ElementInstance::WheelRegion(props) => {
out.push(crate::declarative::frame::ScrollHandleBinding {
handle_key: props.scroll_handle.binding_key(),
element,
handle: props.scroll_handle.clone(),
});
}
ElementInstance::Scrollbar(props) => {
out.push(crate::declarative::frame::ScrollHandleBinding {
handle_key: props.scroll_handle.binding_key(),
element,
handle: props.scroll_handle.clone(),
});
}
_ => {}
}
}