Skip to main content

fret_ui/elements/
runtime.rs

1use std::any::{Any, TypeId};
2use std::collections::{HashMap, HashSet};
3#[cfg(feature = "diagnostics")]
4use std::sync::Mutex;
5use std::sync::{
6    Arc,
7    atomic::{AtomicUsize, Ordering},
8};
9
10use fret_core::{
11    AppWindowId, Color, ColorScheme, ContrastPreference, Edges, ForcedColorsMode, NodeId,
12    PointerType, Rect,
13};
14use fret_runtime::{FrameId, ModelId, TimerToken};
15#[cfg(feature = "diagnostics")]
16use slotmap::Key as _;
17#[cfg(feature = "diagnostics")]
18use std::sync::Arc as StdArc;
19
20use crate::element::AnyElement;
21#[cfg(feature = "diagnostics")]
22use crate::overlay_placement::{AnchoredPanelLayoutTrace, Side};
23use crate::widget::Invalidation;
24
25use super::GlobalElementId;
26use super::hash::stable_hash;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub(crate) enum TimerTarget {
30    Element(GlobalElementId),
31    Node(NodeId),
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub(crate) enum EnvironmentQueryKey {
36    ViewportSize,
37    ScaleFactor,
38    ColorScheme,
39    PrefersReducedMotion,
40    TextScaleFactor,
41    PrefersReducedTransparency,
42    AccentColor,
43    PrefersContrast,
44    ForcedColorsMode,
45    PrimaryPointerType,
46    SafeAreaInsets,
47    OcclusionInsets,
48}
49
50#[cfg(feature = "diagnostics")]
51#[derive(Debug, Clone)]
52pub struct WindowElementDiagnosticsSnapshot {
53    pub focused_element: Option<GlobalElementId>,
54    pub focused_element_node: Option<NodeId>,
55    pub focused_element_bounds: Option<Rect>,
56    pub focused_element_visual_bounds: Option<Rect>,
57    pub active_text_selection: Option<(GlobalElementId, GlobalElementId)>,
58    pub hovered_pressable: Option<GlobalElementId>,
59    pub hovered_pressable_node: Option<NodeId>,
60    pub hovered_pressable_bounds: Option<Rect>,
61    pub hovered_pressable_visual_bounds: Option<Rect>,
62    pub pressed_pressable: Option<GlobalElementId>,
63    pub pressed_pressable_node: Option<NodeId>,
64    pub pressed_pressable_bounds: Option<Rect>,
65    pub pressed_pressable_visual_bounds: Option<Rect>,
66    pub hovered_hover_region: Option<GlobalElementId>,
67    pub wants_continuous_frames: bool,
68    pub continuous_frame_leases: Vec<ContinuousFrameLeaseDiagnosticsSnapshot>,
69    pub animation_frame_request_roots: Vec<AnimationFrameRequestRootDiagnosticsSnapshot>,
70    pub observed_models: Vec<(GlobalElementId, Vec<(u64, Invalidation)>)>,
71    pub observed_globals: Vec<(GlobalElementId, Vec<(String, Invalidation)>)>,
72    pub observed_layout_queries: Vec<ElementObservedLayoutQueriesDiagnosticsSnapshot>,
73    pub layout_query_regions: Vec<LayoutQueryRegionDiagnosticsSnapshot>,
74    pub environment: EnvironmentQueryDiagnosticsSnapshot,
75    pub observed_environment: Vec<ElementObservedEnvironmentDiagnosticsSnapshot>,
76    pub view_cache_reuse_roots: Vec<GlobalElementId>,
77    pub view_cache_reuse_root_element_counts: Vec<(GlobalElementId, u32)>,
78    pub view_cache_reuse_root_element_samples: Vec<ViewCacheReuseRootElementsSample>,
79    pub rendered_state_entries: u64,
80    pub next_state_entries: u64,
81    pub lag_state_frames: u64,
82    pub lag_state_entries_total: u64,
83    pub state_entries_total: u64,
84    pub nodes_count: u64,
85    pub bounds_entries_total: u64,
86    pub timer_targets_count: u64,
87    pub transient_events_count: u64,
88    pub view_cache_state_key_roots_count: u64,
89    pub view_cache_state_key_entries_total: u64,
90    pub view_cache_element_roots_count: u64,
91    pub view_cache_element_entries_total: u64,
92    pub view_cache_key_mismatch_roots_count: u64,
93    pub scratch_element_children_vec_pool_len: u64,
94    pub scratch_element_children_vec_pool_capacity_total: u64,
95    pub scratch_element_children_vec_pool_bytes_estimate_total: u64,
96    /// Detached-but-retained node roots kept alive by retained virtual surfaces (ADR 0177).
97    ///
98    /// This is part of the window's explicit liveness root set under view-cache reuse (ADR 0176).
99    pub retained_keep_alive_roots_len: u32,
100    pub retained_keep_alive_roots_head: Vec<NodeId>,
101    pub retained_keep_alive_roots_tail: Vec<NodeId>,
102    pub node_entry_root_overwrites: Vec<NodeEntryRootOverwrite>,
103    pub overlay_placement: Vec<OverlayPlacementDiagnosticsRecord>,
104}
105
106#[cfg(feature = "diagnostics")]
107#[derive(Debug, Clone)]
108pub struct ContinuousFrameLeaseDiagnosticsSnapshot {
109    pub element: GlobalElementId,
110    pub count: u32,
111    pub debug_path: Option<String>,
112}
113
114#[cfg(feature = "diagnostics")]
115#[derive(Debug, Clone)]
116pub struct AnimationFrameRequestRootDiagnosticsSnapshot {
117    pub element: GlobalElementId,
118    pub debug_path: Option<String>,
119}
120
121#[cfg(feature = "diagnostics")]
122#[allow(clippy::large_enum_variant)]
123#[derive(Debug, Clone)]
124pub enum OverlayPlacementDiagnosticsRecord {
125    /// Anchored panel placement solved via `overlay_placement` (e.g. popper-like overlays).
126    AnchoredPanel(OverlayAnchoredPanelPlacementDiagnosticsRecord),
127    /// A higher-level overlay placed via an external policy (e.g. Radix Select item-aligned mode).
128    PlacedRect(OverlayPlacedRectDiagnosticsRecord),
129}
130
131#[cfg(feature = "diagnostics")]
132#[derive(Debug, Clone)]
133pub struct OverlayAnchoredPanelPlacementDiagnosticsRecord {
134    pub frame_id: FrameId,
135    pub overlay_root_name: Option<StdArc<str>>,
136    pub anchor_element: Option<GlobalElementId>,
137    pub content_element: Option<GlobalElementId>,
138    pub trace: AnchoredPanelLayoutTrace,
139}
140
141#[cfg(feature = "diagnostics")]
142#[derive(Debug, Clone)]
143pub struct OverlayPlacedRectDiagnosticsRecord {
144    pub frame_id: FrameId,
145    pub overlay_root_name: Option<StdArc<str>>,
146    pub anchor_element: Option<GlobalElementId>,
147    pub content_element: Option<GlobalElementId>,
148    pub outer: Rect,
149    pub anchor: Rect,
150    pub placed: Rect,
151    pub side: Option<Side>,
152}
153
154#[cfg(feature = "diagnostics")]
155#[derive(Debug, Clone)]
156pub struct LayoutQueryRegionDiagnosticsSnapshot {
157    pub region: GlobalElementId,
158    pub name: Option<StdArc<str>>,
159    pub revision: u64,
160    pub changed_this_frame: bool,
161    /// Last committed bounds (i.e. what `layout_query_bounds` reads).
162    pub committed_bounds: Option<Rect>,
163    /// Best-effort current bounds observed during the current frame.
164    pub current_bounds: Option<Rect>,
165}
166
167#[cfg(feature = "diagnostics")]
168#[derive(Debug, Clone)]
169pub struct ElementObservedLayoutQueriesDiagnosticsSnapshot {
170    pub element: GlobalElementId,
171    pub deps_fingerprint: u64,
172    pub regions: Vec<ObservedLayoutQueryRegionDiagnosticsSnapshot>,
173}
174
175#[cfg(feature = "diagnostics")]
176#[derive(Debug, Clone)]
177pub struct ObservedLayoutQueryRegionDiagnosticsSnapshot {
178    pub region: GlobalElementId,
179    pub invalidation: Invalidation,
180    pub region_revision: u64,
181    pub region_changed_this_frame: bool,
182    pub region_name: Option<StdArc<str>>,
183    pub region_committed_bounds: Option<Rect>,
184}
185
186#[cfg(feature = "diagnostics")]
187#[derive(Debug, Clone)]
188pub struct EnvironmentQueryDiagnosticsSnapshot {
189    pub viewport_bounds: Rect,
190    pub scale_factor: f32,
191    pub color_scheme: Option<ColorScheme>,
192    pub prefers_reduced_motion: Option<bool>,
193    pub text_scale_factor: Option<f32>,
194    pub prefers_reduced_transparency: Option<bool>,
195    pub accent_color: Option<Color>,
196    pub contrast_preference: Option<ContrastPreference>,
197    pub forced_colors_mode: Option<ForcedColorsMode>,
198    pub primary_pointer_type: PointerType,
199    pub safe_area_insets: Option<Edges>,
200    pub occlusion_insets: Option<Edges>,
201}
202
203#[cfg(feature = "diagnostics")]
204#[derive(Debug, Clone)]
205pub struct ElementObservedEnvironmentDiagnosticsSnapshot {
206    pub element: GlobalElementId,
207    pub deps_fingerprint: u64,
208    pub keys: Vec<ObservedEnvironmentKeyDiagnosticsSnapshot>,
209}
210
211#[cfg(feature = "diagnostics")]
212#[derive(Debug, Clone)]
213pub struct ObservedEnvironmentKeyDiagnosticsSnapshot {
214    pub key: StdArc<str>,
215    pub invalidation: Invalidation,
216    pub key_revision: u64,
217    pub key_changed_this_frame: bool,
218}
219
220#[cfg(feature = "diagnostics")]
221#[derive(Debug, Clone)]
222pub struct ViewCacheReuseRootElementsSample {
223    pub root: GlobalElementId,
224    pub node: Option<NodeId>,
225    pub elements_len: u32,
226    pub elements_head: Vec<GlobalElementId>,
227    pub elements_tail: Vec<GlobalElementId>,
228}
229
230#[derive(Default)]
231pub struct ElementRuntime {
232    windows: HashMap<AppWindowId, WindowElementState>,
233    gc_lag_frames: u64,
234}
235
236impl ElementRuntime {
237    pub fn new() -> Self {
238        Self {
239            windows: HashMap::new(),
240            gc_lag_frames: 2,
241        }
242    }
243
244    pub fn gc_lag_frames(&self) -> u64 {
245        self.gc_lag_frames
246    }
247
248    pub fn set_gc_lag_frames(&mut self, frames: u64) {
249        self.gc_lag_frames = frames;
250    }
251
252    pub fn set_window_prefers_reduced_motion(
253        &mut self,
254        window: AppWindowId,
255        prefers_reduced_motion: Option<bool>,
256    ) {
257        self.for_window_mut(window)
258            .set_committed_prefers_reduced_motion(prefers_reduced_motion);
259    }
260
261    pub fn set_window_color_scheme(&mut self, window: AppWindowId, scheme: Option<ColorScheme>) {
262        self.for_window_mut(window)
263            .set_committed_color_scheme(scheme);
264    }
265
266    pub fn set_window_contrast_preference(
267        &mut self,
268        window: AppWindowId,
269        value: Option<ContrastPreference>,
270    ) {
271        self.for_window_mut(window)
272            .set_committed_contrast_preference(value);
273    }
274
275    pub fn set_window_forced_colors_mode(
276        &mut self,
277        window: AppWindowId,
278        value: Option<ForcedColorsMode>,
279    ) {
280        self.for_window_mut(window)
281            .set_committed_forced_colors_mode(value);
282    }
283
284    pub fn set_window_primary_pointer_type(
285        &mut self,
286        window: AppWindowId,
287        pointer_type: PointerType,
288    ) {
289        self.for_window_mut(window)
290            .record_committed_primary_pointer_type(pointer_type);
291    }
292
293    pub fn set_window_safe_area_insets(&mut self, window: AppWindowId, insets: Option<Edges>) {
294        self.for_window_mut(window)
295            .record_committed_safe_area_insets(insets);
296    }
297
298    pub fn set_window_occlusion_insets(&mut self, window: AppWindowId, insets: Option<Edges>) {
299        self.for_window_mut(window)
300            .record_committed_occlusion_insets(insets);
301    }
302
303    pub fn for_window_mut(&mut self, window: AppWindowId) -> &mut WindowElementState {
304        self.windows.entry(window).or_default()
305    }
306
307    pub(crate) fn for_window(&self, window: AppWindowId) -> Option<&WindowElementState> {
308        self.windows.get(&window)
309    }
310
311    pub fn prepare_window_for_frame(&mut self, window: AppWindowId, frame_id: FrameId) {
312        let lag = self.gc_lag_frames;
313        self.for_window_mut(window).prepare_for_frame(frame_id, lag);
314    }
315
316    #[cfg(feature = "diagnostics")]
317    pub fn diagnostics_snapshot(
318        &self,
319        window: AppWindowId,
320    ) -> Option<WindowElementDiagnosticsSnapshot> {
321        let state = self.windows.get(&window)?;
322        Some(state.diagnostics_snapshot())
323    }
324
325    #[cfg(feature = "diagnostics")]
326    pub fn element_for_node(&self, window: AppWindowId, node: NodeId) -> Option<GlobalElementId> {
327        let state = self.windows.get(&window)?;
328        state.element_for_node(node)
329    }
330
331    #[cfg(feature = "diagnostics")]
332    pub fn node_for_element(
333        &self,
334        window: AppWindowId,
335        element: GlobalElementId,
336    ) -> Option<NodeId> {
337        let state = self.windows.get(&window)?;
338        state.node_entry(element).map(|e| e.node)
339    }
340
341    #[cfg(any(test, feature = "diagnostics"))]
342    pub fn selectable_text_interactive_span_bounds_for_node(
343        &self,
344        window: AppWindowId,
345        node: NodeId,
346    ) -> Option<Vec<crate::element::SelectableTextInteractiveSpanBounds>> {
347        let state = self.windows.get(&window)?;
348        let element = state.element_for_node(node)?;
349        self.selectable_text_interactive_span_bounds_for_element(window, element)
350    }
351
352    #[cfg(any(test, feature = "diagnostics"))]
353    pub fn selectable_text_interactive_span_bounds_for_element(
354        &self,
355        window: AppWindowId,
356        element: GlobalElementId,
357    ) -> Option<Vec<crate::element::SelectableTextInteractiveSpanBounds>> {
358        let state = self.windows.get(&window)?;
359        let key = (element, TypeId::of::<crate::element::SelectableTextState>());
360
361        let boxed = state
362            .next_state
363            .get(&key)
364            .or_else(|| state.rendered_state.get(&key))
365            .or_else(|| state.lag_state.iter().rev().find_map(|m| m.get(&key)))?;
366
367        let state = boxed.downcast_ref::<crate::element::SelectableTextState>()?;
368        Some(state.interactive_span_bounds.clone())
369    }
370
371    #[cfg(feature = "diagnostics")]
372    pub fn debug_path_for_element(
373        &self,
374        window: AppWindowId,
375        element: GlobalElementId,
376    ) -> Option<String> {
377        let state = self.windows.get(&window)?;
378        state.debug_path_for_element(element)
379    }
380}
381
382#[derive(Default)]
383pub struct WindowElementState {
384    pub(super) rendered_state: HashMap<(GlobalElementId, TypeId), Box<dyn Any>>,
385    pub(super) next_state: HashMap<(GlobalElementId, TypeId), Box<dyn Any>>,
386    pub(super) lag_state: Vec<HashMap<(GlobalElementId, TypeId), Box<dyn Any>>>,
387    pub(super) view_cache_state_keys_rendered:
388        HashMap<GlobalElementId, Vec<(GlobalElementId, TypeId)>>,
389    pub(super) view_cache_state_keys_next: HashMap<GlobalElementId, Vec<(GlobalElementId, TypeId)>>,
390    pub(super) view_cache_authoring_identities_rendered:
391        HashMap<GlobalElementId, Vec<GlobalElementId>>,
392    pub(super) view_cache_authoring_identities_next: HashMap<GlobalElementId, Vec<GlobalElementId>>,
393    view_cache_keys_rendered: HashMap<GlobalElementId, u64>,
394    view_cache_keys_next: HashMap<GlobalElementId, u64>,
395    view_cache_key_mismatch_roots: HashSet<GlobalElementId>,
396    pub(super) view_cache_elements_rendered: HashMap<GlobalElementId, Vec<GlobalElementId>>,
397    pub(super) view_cache_elements_next: HashMap<GlobalElementId, Vec<GlobalElementId>>,
398    pub(super) view_cache_reuse_roots: HashSet<GlobalElementId>,
399    view_cache_last_reused_frame: HashMap<GlobalElementId, FrameId>,
400    view_cache_transitioned_reuse_roots: HashSet<GlobalElementId>,
401    view_cache_stack: Vec<GlobalElementId>,
402    raf_notify_roots: HashSet<GlobalElementId>,
403    #[cfg(feature = "diagnostics")]
404    raf_notify_roots_debug: HashSet<GlobalElementId>,
405    pub(super) pending_retained_virtual_list_reconciles:
406        HashMap<GlobalElementId, crate::tree::UiDebugRetainedVirtualListReconcileKind>,
407    retained_virtual_list_keep_alive_roots: HashSet<NodeId>,
408    prepared_frame: FrameId,
409    #[cfg(any(test, feature = "diagnostics"))]
410    strict_ownership: bool,
411    pub(super) prev_unkeyed_fingerprints: HashMap<u64, Vec<u64>>,
412    pub(super) cur_unkeyed_fingerprints: HashMap<u64, Vec<u64>>,
413    pub(super) observed_models_rendered: HashMap<GlobalElementId, Vec<(ModelId, Invalidation)>>,
414    pub(super) observed_models_next: HashMap<GlobalElementId, Vec<(ModelId, Invalidation)>>,
415    pub(super) observed_globals_rendered: HashMap<GlobalElementId, Vec<(TypeId, Invalidation)>>,
416    pub(super) observed_globals_next: HashMap<GlobalElementId, Vec<(TypeId, Invalidation)>>,
417    pub(super) observed_layout_queries_rendered:
418        HashMap<GlobalElementId, Vec<(GlobalElementId, Invalidation)>>,
419    pub(super) observed_layout_queries_next:
420        HashMap<GlobalElementId, Vec<(GlobalElementId, Invalidation)>>,
421    pub(super) observed_environment_rendered:
422        HashMap<GlobalElementId, Vec<(EnvironmentQueryKey, Invalidation)>>,
423    pub(super) observed_environment_next:
424        HashMap<GlobalElementId, Vec<(EnvironmentQueryKey, Invalidation)>>,
425    layout_query_region_revisions: HashMap<GlobalElementId, u64>,
426    layout_query_regions_changed_this_frame: HashSet<GlobalElementId>,
427    environment_revisions: HashMap<EnvironmentQueryKey, u64>,
428    environment_changed_this_frame: HashSet<EnvironmentQueryKey>,
429    pub(super) timer_targets: HashMap<TimerToken, TimerTarget>,
430    scratch_view_cache_keep_alive_elements: HashSet<GlobalElementId>,
431    scratch_view_cache_keep_alive_visited_roots: HashSet<GlobalElementId>,
432    scratch_view_cache_keep_alive_stack: Vec<GlobalElementId>,
433    scratch_element_children_vec_pool: Vec<Vec<AnyElement>>,
434    element_children_vec_pool_reuses: u32,
435    element_children_vec_pool_misses: u32,
436    transient_events: HashMap<(GlobalElementId, u64), FrameId>,
437    authoring_identities_current_frame: HashSet<GlobalElementId>,
438    nodes: HashMap<GlobalElementId, NodeEntry>,
439    root_bounds: HashMap<GlobalElementId, Rect>,
440    prev_bounds: HashMap<GlobalElementId, Rect>,
441    cur_bounds: HashMap<GlobalElementId, Rect>,
442    prev_visual_bounds: HashMap<GlobalElementId, Rect>,
443    cur_visual_bounds: HashMap<GlobalElementId, Rect>,
444    committed_viewport_bounds: Rect,
445    committed_scale_factor: f32,
446    committed_color_scheme: Option<ColorScheme>,
447    committed_prefers_reduced_motion: Option<bool>,
448    committed_text_scale_factor: Option<f32>,
449    committed_prefers_reduced_transparency: Option<bool>,
450    committed_accent_color: Option<Color>,
451    committed_contrast_preference: Option<ContrastPreference>,
452    committed_forced_colors_mode: Option<ForcedColorsMode>,
453    committed_primary_pointer_type: Option<PointerType>,
454    committed_safe_area_insets: Option<Edges>,
455    committed_occlusion_insets: Option<Edges>,
456    pub(super) focused_element: Option<GlobalElementId>,
457    pub(super) active_text_selection: Option<ActiveTextSelection>,
458    pub(super) hovered_pressable: Option<GlobalElementId>,
459    pub(super) hovered_pressable_node: Option<NodeId>,
460    pub(super) hovered_pressable_raw: Option<GlobalElementId>,
461    pub(super) hovered_pressable_raw_node: Option<NodeId>,
462    pub(super) hovered_pressable_raw_below_barrier: Option<GlobalElementId>,
463    pub(super) hovered_pressable_raw_below_barrier_node: Option<NodeId>,
464    pub(super) pressed_pressable: Option<GlobalElementId>,
465    pub(super) pressed_pressable_node: Option<NodeId>,
466    pub(super) hovered_hover_region: Option<GlobalElementId>,
467    pub(super) hovered_hover_region_node: Option<NodeId>,
468    continuous_frames: Arc<AtomicUsize>,
469    #[cfg(feature = "diagnostics")]
470    continuous_frame_owners: Arc<Mutex<HashMap<GlobalElementId, u32>>>,
471    #[cfg(feature = "diagnostics")]
472    debug_identity: DebugIdentityRegistry,
473    #[cfg(feature = "diagnostics")]
474    debug_node_entry_root_overwrites: Vec<NodeEntryRootOverwrite>,
475    #[cfg(feature = "diagnostics")]
476    overlay_placement: Vec<OverlayPlacementDiagnosticsRecord>,
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub(crate) struct ActiveTextSelection {
481    pub root: GlobalElementId,
482    pub element: GlobalElementId,
483    pub node: NodeId,
484}
485
486#[derive(Debug, Clone, Copy)]
487pub(crate) struct NodeEntry {
488    pub node: NodeId,
489    pub last_seen_frame: FrameId,
490    pub root: GlobalElementId,
491}
492
493#[cfg(feature = "diagnostics")]
494#[derive(Debug, Clone, Copy)]
495pub struct NodeEntryRootOverwrite {
496    pub frame_id: FrameId,
497    pub element: GlobalElementId,
498    pub old_root: GlobalElementId,
499    pub new_root: GlobalElementId,
500    pub old_node: NodeId,
501    pub new_node: NodeId,
502    pub file: &'static str,
503    pub line: u32,
504    pub column: u32,
505}
506
507#[derive(Debug, Default, Clone)]
508pub(crate) struct LayoutQueryRegionMarker {
509    pub name: Option<Arc<str>>,
510}
511
512impl WindowElementState {
513    pub(crate) fn element_children_vec_pool_reuses(&self) -> u32 {
514        self.element_children_vec_pool_reuses
515    }
516
517    pub(crate) fn element_children_vec_pool_misses(&self) -> u32 {
518        self.element_children_vec_pool_misses
519    }
520
521    pub(crate) fn take_scratch_element_children_vec(
522        &mut self,
523        min_capacity: usize,
524    ) -> Vec<AnyElement> {
525        if let Some(mut out) = self.scratch_element_children_vec_pool.pop() {
526            self.element_children_vec_pool_reuses =
527                self.element_children_vec_pool_reuses.saturating_add(1);
528            out.clear();
529            if out.capacity() < min_capacity {
530                out.reserve(min_capacity - out.capacity());
531            }
532            return out;
533        }
534        self.element_children_vec_pool_misses =
535            self.element_children_vec_pool_misses.saturating_add(1);
536        Vec::with_capacity(min_capacity)
537    }
538
539    pub(crate) fn restore_scratch_element_children_vec(&mut self, mut scratch: Vec<AnyElement>) {
540        const POOL_MAX: usize = 2048;
541        const MAX_RETAIN_BYTES: usize = 64 * 1024;
542        if self.scratch_element_children_vec_pool.len() >= POOL_MAX {
543            return;
544        }
545        let bytes = scratch
546            .capacity()
547            .saturating_mul(std::mem::size_of::<AnyElement>());
548        if bytes > MAX_RETAIN_BYTES {
549            return;
550        }
551        scratch.clear();
552        self.scratch_element_children_vec_pool.push(scratch);
553    }
554
555    pub(crate) fn take_scratch_view_cache_keep_alive_elements(
556        &mut self,
557    ) -> HashSet<GlobalElementId> {
558        std::mem::take(&mut self.scratch_view_cache_keep_alive_elements)
559    }
560
561    pub(crate) fn restore_scratch_view_cache_keep_alive_elements(
562        &mut self,
563        scratch: HashSet<GlobalElementId>,
564    ) {
565        self.scratch_view_cache_keep_alive_elements = scratch;
566    }
567
568    pub(crate) fn take_scratch_view_cache_keep_alive_visited_roots(
569        &mut self,
570    ) -> HashSet<GlobalElementId> {
571        std::mem::take(&mut self.scratch_view_cache_keep_alive_visited_roots)
572    }
573
574    pub(crate) fn restore_scratch_view_cache_keep_alive_visited_roots(
575        &mut self,
576        scratch: HashSet<GlobalElementId>,
577    ) {
578        self.scratch_view_cache_keep_alive_visited_roots = scratch;
579    }
580
581    pub(crate) fn take_scratch_view_cache_keep_alive_stack(&mut self) -> Vec<GlobalElementId> {
582        std::mem::take(&mut self.scratch_view_cache_keep_alive_stack)
583    }
584
585    pub(crate) fn restore_scratch_view_cache_keep_alive_stack(
586        &mut self,
587        scratch: Vec<GlobalElementId>,
588    ) {
589        self.scratch_view_cache_keep_alive_stack = scratch;
590    }
591
592    #[cfg(any(test, feature = "diagnostics"))]
593    #[allow(dead_code)]
594    pub(crate) fn set_strict_ownership(&mut self, strict: bool) {
595        self.strict_ownership = strict;
596    }
597
598    fn prepare_for_frame(&mut self, frame_id: FrameId, lag_frames: u64) {
599        if self.prepared_frame == frame_id {
600            return;
601        }
602        self.prepared_frame = frame_id;
603
604        self.advance_element_state_buffers(lag_frames);
605
606        let cutoff = frame_id.0.saturating_sub(1);
607        self.transient_events
608            .retain(|_, recorded| recorded.0 >= cutoff);
609        self.raf_notify_roots.clear();
610        #[cfg(feature = "diagnostics")]
611        self.raf_notify_roots_debug.clear();
612        self.view_cache_key_mismatch_roots.clear();
613        self.element_children_vec_pool_reuses = 0;
614        self.element_children_vec_pool_misses = 0;
615
616        std::mem::swap(
617            &mut self.view_cache_keys_rendered,
618            &mut self.view_cache_keys_next,
619        );
620        self.view_cache_keys_next.clear();
621
622        std::mem::swap(
623            &mut self.view_cache_state_keys_rendered,
624            &mut self.view_cache_state_keys_next,
625        );
626        self.view_cache_state_keys_next.clear();
627
628        std::mem::swap(
629            &mut self.view_cache_authoring_identities_rendered,
630            &mut self.view_cache_authoring_identities_next,
631        );
632        self.view_cache_authoring_identities_next.clear();
633
634        std::mem::swap(
635            &mut self.view_cache_elements_rendered,
636            &mut self.view_cache_elements_next,
637        );
638        self.view_cache_elements_next.clear();
639
640        self.view_cache_reuse_roots.clear();
641        self.view_cache_transitioned_reuse_roots.clear();
642        self.view_cache_stack.clear();
643
644        std::mem::swap(
645            &mut self.prev_unkeyed_fingerprints,
646            &mut self.cur_unkeyed_fingerprints,
647        );
648        self.cur_unkeyed_fingerprints.clear();
649
650        std::mem::swap(
651            &mut self.observed_models_rendered,
652            &mut self.observed_models_next,
653        );
654        self.observed_models_next.clear();
655        std::mem::swap(
656            &mut self.observed_globals_rendered,
657            &mut self.observed_globals_next,
658        );
659        self.observed_globals_next.clear();
660        std::mem::swap(
661            &mut self.observed_layout_queries_rendered,
662            &mut self.observed_layout_queries_next,
663        );
664        self.observed_layout_queries_next.clear();
665        self.layout_query_regions_changed_this_frame.clear();
666
667        std::mem::swap(
668            &mut self.observed_environment_rendered,
669            &mut self.observed_environment_next,
670        );
671        self.observed_environment_next.clear();
672        self.environment_changed_this_frame.clear();
673
674        // Keep cross-frame geometry queries stable even when layout/paint skips subtrees due to
675        // caching:
676        // - `prev_*` stores the last committed snapshot (used by cross-frame queries).
677        // - `cur_*` stores only the current frame's recorded deltas.
678        //
679        // Committing deltas at frame boundaries avoids cloning full maps on cache-hit frames.
680        if !self.cur_bounds.is_empty() {
681            self.prev_bounds.reserve(self.cur_bounds.len());
682            self.prev_bounds.extend(self.cur_bounds.drain());
683        }
684        self.cur_bounds.clear();
685
686        if !self.cur_visual_bounds.is_empty() {
687            self.prev_visual_bounds
688                .reserve(self.cur_visual_bounds.len());
689            self.prev_visual_bounds
690                .extend(self.cur_visual_bounds.drain());
691        }
692        self.cur_visual_bounds.clear();
693
694        self.focused_element = None;
695        self.authoring_identities_current_frame.clear();
696
697        #[cfg(feature = "diagnostics")]
698        {
699            let cutoff = frame_id.0.saturating_sub(lag_frames);
700            self.debug_identity
701                .entries
702                .retain(|_, v| v.last_seen_frame.0 >= cutoff);
703            self.debug_node_entry_root_overwrites
704                .retain(|r| r.frame_id.0 >= cutoff);
705
706            // Keep a small rolling window of placement records so scripted diagnostics can snapshot
707            // placement decisions even when scripts are processed earlier in the frame.
708            const KEEP_FRAMES: u64 = 120;
709            let cutoff = frame_id.0.saturating_sub(KEEP_FRAMES);
710            self.overlay_placement.retain(|rec| match rec {
711                OverlayPlacementDiagnosticsRecord::AnchoredPanel(r) => r.frame_id.0 >= cutoff,
712                OverlayPlacementDiagnosticsRecord::PlacedRect(r) => r.frame_id.0 >= cutoff,
713            });
714            const MAX_RECORDS: usize = 512;
715            if self.overlay_placement.len() > MAX_RECORDS {
716                let extra = self.overlay_placement.len() - MAX_RECORDS;
717                self.overlay_placement.drain(0..extra);
718            }
719        }
720    }
721
722    pub(crate) fn retained_virtual_list_keep_alive_roots(
723        &self,
724    ) -> impl Iterator<Item = NodeId> + '_ {
725        self.retained_virtual_list_keep_alive_roots.iter().copied()
726    }
727
728    pub(crate) fn add_retained_virtual_list_keep_alive_root(&mut self, node: NodeId) {
729        self.retained_virtual_list_keep_alive_roots.insert(node);
730    }
731
732    pub(crate) fn remove_retained_virtual_list_keep_alive_root(&mut self, node: NodeId) {
733        self.retained_virtual_list_keep_alive_roots.remove(&node);
734    }
735
736    pub(crate) fn retain_retained_virtual_list_keep_alive_roots(
737        &mut self,
738        mut f: impl FnMut(NodeId) -> bool,
739    ) {
740        self.retained_virtual_list_keep_alive_roots
741            .retain(|node| f(*node));
742    }
743
744    pub(crate) fn record_transient_event(&mut self, element: GlobalElementId, key: u64) {
745        self.transient_events
746            .insert((element, key), self.prepared_frame);
747    }
748
749    pub(crate) fn mark_authoring_identity_seen(&mut self, element: GlobalElementId) {
750        self.authoring_identities_current_frame.insert(element);
751        for &root in &self.view_cache_stack {
752            self.view_cache_authoring_identities_next
753                .entry(root)
754                .or_default()
755                .push(element);
756        }
757        #[cfg(feature = "diagnostics")]
758        self.touch_debug_identity_for_element(self.prepared_frame, element);
759    }
760
761    pub(crate) fn element_identity_is_live_in_current_frame(
762        &self,
763        element: GlobalElementId,
764    ) -> bool {
765        self.authoring_identities_current_frame.contains(&element)
766            || self
767                .node_entry(element)
768                .is_some_and(|entry| entry.last_seen_frame == self.prepared_frame)
769    }
770
771    pub(crate) fn take_transient_event(&mut self, element: GlobalElementId, key: u64) -> bool {
772        self.transient_events.remove(&(element, key)).is_some()
773    }
774
775    fn advance_element_state_buffers(&mut self, lag_frames: u64) {
776        if lag_frames == 0 {
777            self.lag_state.clear();
778        } else {
779            self.lag_state
780                .push(std::mem::take(&mut self.rendered_state));
781            let max = lag_frames as usize;
782            if self.lag_state.len() > max {
783                let drain = self.lag_state.len() - max;
784                self.lag_state.drain(0..drain);
785            }
786        }
787
788        self.rendered_state = std::mem::take(&mut self.next_state);
789        self.next_state.clear();
790    }
791
792    pub(super) fn state_any_ref(&self, key: &(GlobalElementId, TypeId)) -> Option<&dyn Any> {
793        if let Some(v) = self.next_state.get(key) {
794            return Some(&**v);
795        }
796        if let Some(v) = self.rendered_state.get(key) {
797            return Some(&**v);
798        }
799        for map in self.lag_state.iter().rev() {
800            if let Some(v) = map.get(key) {
801                return Some(&**v);
802            }
803        }
804        None
805    }
806
807    pub(crate) fn has_state<S: Any>(&self, element: GlobalElementId) -> bool {
808        let key = (element, TypeId::of::<S>());
809        self.state_any_ref(&key).is_some()
810    }
811
812    pub(crate) fn layout_query_region_revision(&self, element: GlobalElementId) -> u64 {
813        self.layout_query_region_revisions
814            .get(&element)
815            .copied()
816            .unwrap_or(0)
817    }
818
819    pub(crate) fn layout_query_deps_fingerprint_rendered(&self, root: GlobalElementId) -> u64 {
820        self.layout_query_deps_fingerprint_from_list(
821            self.observed_layout_queries_rendered.get(&root),
822        )
823    }
824
825    pub(crate) fn layout_query_deps_fingerprint_next(&self, root: GlobalElementId) -> u64 {
826        self.layout_query_deps_fingerprint_from_list(self.observed_layout_queries_next.get(&root))
827    }
828
829    fn layout_query_deps_fingerprint_from_list(
830        &self,
831        deps: Option<&Vec<(GlobalElementId, Invalidation)>>,
832    ) -> u64 {
833        let Some(deps) = deps else {
834            return 0;
835        };
836        if deps.is_empty() {
837            return 0;
838        }
839
840        let mut entries: Vec<(u64, u64, u8)> = deps
841            .iter()
842            .map(|(region, inv)| {
843                (
844                    region.0,
845                    self.layout_query_region_revision(*region),
846                    *inv as u8,
847                )
848            })
849            .collect();
850        entries.sort_by(|a, b| a.0.cmp(&b.0).then(a.2.cmp(&b.2)));
851        stable_hash(&entries)
852    }
853
854    pub(crate) fn environment_deps_fingerprint_rendered(&self, root: GlobalElementId) -> u64 {
855        self.environment_deps_fingerprint_from_list(self.observed_environment_rendered.get(&root))
856    }
857
858    pub(crate) fn environment_deps_fingerprint_next(&self, root: GlobalElementId) -> u64 {
859        self.environment_deps_fingerprint_from_list(self.observed_environment_next.get(&root))
860    }
861
862    fn environment_revision(&self, key: EnvironmentQueryKey) -> u64 {
863        self.environment_revisions.get(&key).copied().unwrap_or(0)
864    }
865
866    fn environment_deps_fingerprint_from_list(
867        &self,
868        deps: Option<&Vec<(EnvironmentQueryKey, Invalidation)>>,
869    ) -> u64 {
870        let Some(deps) = deps else {
871            return 0;
872        };
873        if deps.is_empty() {
874            return 0;
875        }
876
877        let key_id = |k: EnvironmentQueryKey| match k {
878            EnvironmentQueryKey::ViewportSize => 0u8,
879            EnvironmentQueryKey::ScaleFactor => 1u8,
880            EnvironmentQueryKey::ColorScheme => 2u8,
881            EnvironmentQueryKey::PrefersReducedMotion => 3u8,
882            EnvironmentQueryKey::PrimaryPointerType => 4u8,
883            EnvironmentQueryKey::SafeAreaInsets => 5u8,
884            EnvironmentQueryKey::OcclusionInsets => 6u8,
885            EnvironmentQueryKey::PrefersContrast => 7u8,
886            EnvironmentQueryKey::ForcedColorsMode => 8u8,
887            EnvironmentQueryKey::TextScaleFactor => 9u8,
888            EnvironmentQueryKey::PrefersReducedTransparency => 10u8,
889            EnvironmentQueryKey::AccentColor => 11u8,
890        };
891
892        let mut entries: Vec<(u8, u64, u8)> = deps
893            .iter()
894            .map(|(key, inv)| (key_id(*key), self.environment_revision(*key), *inv as u8))
895            .collect();
896        entries.sort_by(|a, b| a.0.cmp(&b.0).then(a.2.cmp(&b.2)));
897        stable_hash(&entries)
898    }
899
900    pub(super) fn take_state_box(
901        &mut self,
902        key: &(GlobalElementId, TypeId),
903    ) -> Option<Box<dyn Any>> {
904        if let Some(v) = self.next_state.remove(key) {
905            return Some(v);
906        }
907        if let Some(v) = self.rendered_state.remove(key) {
908            return Some(v);
909        }
910        for map in self.lag_state.iter_mut().rev() {
911            if let Some(v) = map.remove(key) {
912                return Some(v);
913            }
914        }
915        None
916    }
917
918    pub(super) fn insert_state_box(&mut self, key: (GlobalElementId, TypeId), value: Box<dyn Any>) {
919        self.next_state.insert(key, value);
920    }
921
922    pub(crate) fn with_state_mut<S: Any, R>(
923        &mut self,
924        element: GlobalElementId,
925        init: impl FnOnce() -> S,
926        f: impl FnOnce(&mut S) -> R,
927    ) -> R {
928        struct StateBoxGuard<'a> {
929            window_state: &'a mut WindowElementState,
930            key: (GlobalElementId, TypeId),
931            value: Option<Box<dyn Any>>,
932        }
933
934        impl Drop for StateBoxGuard<'_> {
935            fn drop(&mut self) {
936                let Some(value) = self.value.take() else {
937                    return;
938                };
939                self.window_state.insert_state_box(self.key, value);
940            }
941        }
942
943        let key = (element, TypeId::of::<S>());
944        self.record_state_key_access(key);
945
946        let mut init = Some(init);
947        let value = self.take_state_box(&key).unwrap_or_else(|| {
948            let init = init.take().expect("init is available");
949            Box::new(init())
950        });
951        let mut guard = StateBoxGuard {
952            window_state: self,
953            key,
954            value: Some(value),
955        };
956
957        let state = match guard
958            .value
959            .as_deref_mut()
960            .expect("guard has a value")
961            .downcast_mut::<S>()
962        {
963            Some(state) => state,
964            None => {
965                if crate::strict_runtime::strict_runtime_enabled() {
966                    panic!(
967                        "element state type mismatch: element={:?}, expected={}",
968                        element,
969                        std::any::type_name::<S>()
970                    );
971                }
972
973                #[cfg(debug_assertions)]
974                {
975                    eprintln!(
976                        "element state type mismatch: element={:?}, expected={}; dropping corrupt state and re-initializing",
977                        element,
978                        std::any::type_name::<S>()
979                    );
980                }
981
982                let init = init.take().expect("init is available");
983                guard.value = Some(Box::new(init()));
984                guard
985                    .value
986                    .as_deref_mut()
987                    .expect("guard has a value")
988                    .downcast_mut::<S>()
989                    .expect("re-initialized state has the expected type")
990            }
991        };
992
993        f(state)
994    }
995
996    pub(crate) fn try_with_state_mut<S: Any, R>(
997        &mut self,
998        element: GlobalElementId,
999        f: impl FnOnce(&mut S) -> R,
1000    ) -> Option<R> {
1001        struct StateBoxGuard<'a> {
1002            window_state: &'a mut WindowElementState,
1003            key: (GlobalElementId, TypeId),
1004            value: Option<Box<dyn Any>>,
1005        }
1006
1007        impl Drop for StateBoxGuard<'_> {
1008            fn drop(&mut self) {
1009                let Some(value) = self.value.take() else {
1010                    return;
1011                };
1012                self.window_state.insert_state_box(self.key, value);
1013            }
1014        }
1015
1016        let key = (element, TypeId::of::<S>());
1017        self.record_state_key_access(key);
1018
1019        let value = self.take_state_box(&key)?;
1020        let mut guard = StateBoxGuard {
1021            window_state: self,
1022            key,
1023            value: Some(value),
1024        };
1025
1026        let state = match guard
1027            .value
1028            .as_deref_mut()
1029            .expect("guard has a value")
1030            .downcast_mut::<S>()
1031        {
1032            Some(state) => state,
1033            None => {
1034                if crate::strict_runtime::strict_runtime_enabled() {
1035                    panic!(
1036                        "element state type mismatch: element={:?}, expected={}",
1037                        element,
1038                        std::any::type_name::<S>()
1039                    );
1040                }
1041
1042                #[cfg(debug_assertions)]
1043                {
1044                    eprintln!(
1045                        "element state type mismatch: element={:?}, expected={}; dropping corrupt state",
1046                        element,
1047                        std::any::type_name::<S>()
1048                    );
1049                }
1050
1051                guard.value = None;
1052                return None;
1053            }
1054        };
1055
1056        Some(f(state))
1057    }
1058
1059    pub(crate) fn mark_retained_virtual_list_needs_reconcile(
1060        &mut self,
1061        element: GlobalElementId,
1062        kind: crate::tree::UiDebugRetainedVirtualListReconcileKind,
1063    ) {
1064        self.pending_retained_virtual_list_reconciles
1065            .entry(element)
1066            .and_modify(|existing| {
1067                // "Escape" is correctness-critical; if both are scheduled in the same tick, keep the
1068                // stronger reason so downstream diagnostics/gates remain explainable.
1069                if *existing == crate::tree::UiDebugRetainedVirtualListReconcileKind::Prefetch
1070                    && kind == crate::tree::UiDebugRetainedVirtualListReconcileKind::Escape
1071                {
1072                    *existing = kind;
1073                }
1074            })
1075            .or_insert(kind);
1076    }
1077
1078    pub(crate) fn take_retained_virtual_list_reconciles(
1079        &mut self,
1080    ) -> Vec<(
1081        GlobalElementId,
1082        crate::tree::UiDebugRetainedVirtualListReconcileKind,
1083    )> {
1084        self.pending_retained_virtual_list_reconciles
1085            .drain()
1086            .collect()
1087    }
1088
1089    pub(super) fn record_state_key_access(&mut self, key: (GlobalElementId, TypeId)) {
1090        if self.view_cache_stack.is_empty() {
1091            return;
1092        };
1093        // Nested view-cache correctness: when entering a child view-cache scope, parent cache
1094        // roots still need to keep the child's state alive if the parent reuses without
1095        // re-rendering that subtree.
1096        for &root in &self.view_cache_stack {
1097            self.view_cache_state_keys_next
1098                .entry(root)
1099                .or_default()
1100                .push(key);
1101        }
1102    }
1103
1104    pub(super) fn begin_view_cache_scope(&mut self, root: GlobalElementId) {
1105        self.view_cache_stack.push(root);
1106        self.view_cache_state_keys_next.remove(&root);
1107        self.view_cache_authoring_identities_next.remove(&root);
1108        self.view_cache_elements_next.remove(&root);
1109    }
1110
1111    pub(super) fn end_view_cache_scope(&mut self, root: GlobalElementId) {
1112        let popped = self.view_cache_stack.pop();
1113        debug_assert_eq!(popped, Some(root));
1114        if let Some(keys) = self.view_cache_state_keys_next.get_mut(&root) {
1115            let mut seen: HashSet<(GlobalElementId, TypeId)> = HashSet::with_capacity(keys.len());
1116            keys.retain(|&key| seen.insert(key));
1117        }
1118        if let Some(identities) = self.view_cache_authoring_identities_next.get_mut(&root) {
1119            let mut seen: HashSet<GlobalElementId> = HashSet::with_capacity(identities.len());
1120            identities.retain(|&id| seen.insert(id));
1121        }
1122        if let Some(elements) = self.view_cache_elements_next.get_mut(&root) {
1123            let mut seen: HashSet<GlobalElementId> = HashSet::with_capacity(elements.len());
1124            elements.retain(|&id| seen.insert(id));
1125        }
1126    }
1127
1128    pub(crate) fn touch_observed_models_for_element_if_recorded(
1129        &mut self,
1130        element: GlobalElementId,
1131    ) {
1132        if self.observed_models_next.contains_key(&element) {
1133            return;
1134        }
1135        let Some(list) = self.observed_models_rendered.get(&element) else {
1136            return;
1137        };
1138        self.observed_models_next.insert(element, list.clone());
1139    }
1140
1141    pub(crate) fn touch_observed_globals_for_element_if_recorded(
1142        &mut self,
1143        element: GlobalElementId,
1144    ) {
1145        if self.observed_globals_next.contains_key(&element) {
1146            return;
1147        }
1148        let Some(list) = self.observed_globals_rendered.get(&element) else {
1149            return;
1150        };
1151        self.observed_globals_next.insert(element, list.clone());
1152    }
1153
1154    pub(crate) fn touch_observed_layout_queries_for_element_if_recorded(
1155        &mut self,
1156        element: GlobalElementId,
1157    ) {
1158        if self.observed_layout_queries_next.contains_key(&element) {
1159            return;
1160        }
1161        let Some(list) = self.observed_layout_queries_rendered.get(&element) else {
1162            return;
1163        };
1164        self.observed_layout_queries_next
1165            .insert(element, list.clone());
1166    }
1167
1168    pub(crate) fn touch_observed_environment_for_element_if_recorded(
1169        &mut self,
1170        element: GlobalElementId,
1171    ) {
1172        if self.observed_environment_next.contains_key(&element) {
1173            return;
1174        }
1175        let Some(list) = self.observed_environment_rendered.get(&element) else {
1176            return;
1177        };
1178        self.observed_environment_next.insert(element, list.clone());
1179    }
1180
1181    pub(crate) fn mark_view_cache_reuse_root(&mut self, root: GlobalElementId) {
1182        self.view_cache_reuse_roots.insert(root);
1183    }
1184
1185    /// Returns `true` if this cache root was *not* reused in the immediately-previous frame.
1186    ///
1187    /// This is used to refresh liveness/recording when a view-cache root transitions into reuse,
1188    /// avoiding GC sweeping stale-but-live subtrees on the first cache-hit frame.
1189    pub(crate) fn record_view_cache_reuse_frame(
1190        &mut self,
1191        root: GlobalElementId,
1192        frame_id: FrameId,
1193    ) -> bool {
1194        let transitioned = self
1195            .view_cache_last_reused_frame
1196            .get(&root)
1197            .is_none_or(|last| last.0.saturating_add(1) < frame_id.0);
1198        self.view_cache_last_reused_frame.insert(root, frame_id);
1199        if transitioned {
1200            self.view_cache_transitioned_reuse_roots.insert(root);
1201        }
1202        transitioned
1203    }
1204
1205    pub(crate) fn view_cache_transitioned_reuse_roots(
1206        &self,
1207    ) -> impl Iterator<Item = GlobalElementId> + '_ {
1208        self.view_cache_transitioned_reuse_roots.iter().copied()
1209    }
1210
1211    pub(crate) fn should_reuse_view_cache_root(&self, root: GlobalElementId) -> bool {
1212        self.view_cache_reuse_roots.contains(&root)
1213    }
1214
1215    pub(crate) fn view_cache_reuse_roots(&self) -> impl Iterator<Item = GlobalElementId> + '_ {
1216        self.view_cache_reuse_roots.iter().copied()
1217    }
1218
1219    pub(crate) fn current_view_cache_root(&self) -> Option<GlobalElementId> {
1220        self.view_cache_stack.last().copied()
1221    }
1222
1223    pub(crate) fn request_notify_for_animation_frame(&mut self, root: GlobalElementId) {
1224        self.raf_notify_roots.insert(root);
1225        #[cfg(feature = "diagnostics")]
1226        self.raf_notify_roots_debug.insert(root);
1227    }
1228
1229    pub(crate) fn take_notify_for_animation_frame(&mut self) -> Vec<GlobalElementId> {
1230        if self.raf_notify_roots.is_empty() {
1231            return Vec::new();
1232        }
1233        let out: Vec<GlobalElementId> = self.raf_notify_roots.iter().copied().collect();
1234        self.raf_notify_roots.clear();
1235        out
1236    }
1237
1238    pub(crate) fn view_cache_key_matches_and_touch(
1239        &mut self,
1240        root: GlobalElementId,
1241        key: u64,
1242    ) -> bool {
1243        if self.view_cache_keys_next.contains_key(&root) {
1244            return self.view_cache_keys_next.get(&root).copied() == Some(key);
1245        }
1246        let Some(prev) = self.view_cache_keys_rendered.get(&root).copied() else {
1247            return false;
1248        };
1249        if prev != key {
1250            return false;
1251        }
1252        self.view_cache_keys_next.insert(root, key);
1253        true
1254    }
1255
1256    pub(crate) fn set_view_cache_key(&mut self, root: GlobalElementId, key: u64) {
1257        self.view_cache_keys_next.insert(root, key);
1258    }
1259
1260    pub(crate) fn record_view_cache_key_mismatch(&mut self, root: GlobalElementId) {
1261        self.view_cache_key_mismatch_roots.insert(root);
1262    }
1263
1264    pub(crate) fn view_cache_key_mismatch(&self, root: GlobalElementId) -> bool {
1265        self.view_cache_key_mismatch_roots.contains(&root)
1266    }
1267
1268    pub(super) fn touch_state_key(&mut self, key: (GlobalElementId, TypeId)) {
1269        let Some(value) = self.take_state_box(&key) else {
1270            return;
1271        };
1272        self.insert_state_box(key, value);
1273    }
1274
1275    pub(crate) fn touch_view_cache_state_keys_if_recorded(&mut self, root: GlobalElementId) {
1276        let Some(keys) = self.view_cache_state_keys_rendered.get(&root).cloned() else {
1277            return;
1278        };
1279        for &key in &keys {
1280            self.touch_state_key(key);
1281        }
1282        self.view_cache_state_keys_next.insert(root, keys);
1283    }
1284
1285    pub(crate) fn touch_view_cache_authoring_identities_if_recorded(
1286        &mut self,
1287        root: GlobalElementId,
1288    ) {
1289        let Some(identities) = self
1290            .view_cache_authoring_identities_rendered
1291            .get(&root)
1292            .cloned()
1293        else {
1294            return;
1295        };
1296        for &identity in &identities {
1297            self.authoring_identities_current_frame.insert(identity);
1298            #[cfg(feature = "diagnostics")]
1299            self.touch_debug_identity_for_element(self.prepared_frame, identity);
1300        }
1301        self.view_cache_authoring_identities_next
1302            .insert(root, identities);
1303    }
1304
1305    /// Keep action-hook state alive for a cached subtree, even when view-cache reuse skips the
1306    /// declarative closures that normally re-register those hooks.
1307    ///
1308    /// `touch_view_cache_state_keys_if_recorded` is the primary mechanism, but action hooks can be
1309    /// missed if they are installed outside the view-cache scope (or if a refactor temporarily
1310    /// disrupts state-key recording). Touching known hook state types for all recorded subtree
1311    /// elements provides a conservative fallback.
1312    pub(crate) fn touch_view_cache_action_hook_state_for_subtree_elements(
1313        &mut self,
1314        root: GlobalElementId,
1315    ) {
1316        let Some(elements) = self.view_cache_elements_for_root(root) else {
1317            return;
1318        };
1319        if elements.is_empty() {
1320            return;
1321        }
1322
1323        let elements = elements.to_vec();
1324        for element in elements {
1325            self.touch_state_key((element, TypeId::of::<crate::action::PressableActionHooks>()));
1326            self.touch_state_key((
1327                element,
1328                TypeId::of::<crate::action::PressableHoverActionHooks>(),
1329            ));
1330            self.touch_state_key((
1331                element,
1332                TypeId::of::<crate::action::DismissibleActionHooks>(),
1333            ));
1334            self.touch_state_key((element, TypeId::of::<crate::action::PointerActionHooks>()));
1335            self.touch_state_key((element, TypeId::of::<crate::action::KeyActionHooks>()));
1336            self.touch_state_key((element, TypeId::of::<crate::action::ActionRouteHooks>()));
1337            self.touch_state_key((element, TypeId::of::<crate::action::CommandActionHooks>()));
1338            self.touch_state_key((
1339                element,
1340                TypeId::of::<crate::action::CommandAvailabilityActionHooks>(),
1341            ));
1342            self.touch_state_key((element, TypeId::of::<crate::action::TimerActionHooks>()));
1343            self.touch_state_key((element, TypeId::of::<crate::action::RovingActionHooks>()));
1344            self.touch_state_key((element, TypeId::of::<crate::element::SelectableTextState>()));
1345            self.touch_state_key((
1346                element,
1347                TypeId::of::<crate::action::SelectableTextActionHooks>(),
1348            ));
1349            self.touch_state_key((
1350                element,
1351                TypeId::of::<crate::action::TextInputRegionActionHooks>(),
1352            ));
1353            self.touch_state_key((
1354                element,
1355                TypeId::of::<crate::action::InternalDragActionHooks>(),
1356            ));
1357            self.touch_state_key((
1358                element,
1359                TypeId::of::<crate::action::ExternalDragActionHooks>(),
1360            ));
1361        }
1362    }
1363
1364    pub(crate) fn record_view_cache_subtree_elements(
1365        &mut self,
1366        root: GlobalElementId,
1367        elements: Vec<GlobalElementId>,
1368    ) {
1369        self.view_cache_elements_next.insert(root, elements);
1370    }
1371
1372    pub(crate) fn forget_view_cache_subtree_elements(&mut self, root: GlobalElementId) {
1373        self.view_cache_authoring_identities_rendered.remove(&root);
1374        self.view_cache_authoring_identities_next.remove(&root);
1375        self.view_cache_elements_rendered.remove(&root);
1376        self.view_cache_elements_next.remove(&root);
1377    }
1378
1379    pub(crate) fn touch_view_cache_subtree_elements_if_recorded(
1380        &mut self,
1381        root: GlobalElementId,
1382        frame_id: FrameId,
1383        root_id: GlobalElementId,
1384        mut resolve_live_attached_node: impl FnMut(GlobalElementId, Option<NodeId>) -> Option<NodeId>,
1385    ) -> bool {
1386        if self.view_cache_elements_next.contains_key(&root) {
1387            return true;
1388        }
1389
1390        let Some(elements) = self.view_cache_elements_rendered.get(&root).cloned() else {
1391            return false;
1392        };
1393
1394        let mut authoritative_nodes: Vec<(GlobalElementId, NodeId, GlobalElementId)> =
1395            Vec::with_capacity(elements.len());
1396        for &element in &elements {
1397            let seeded_entry = self.nodes.get(&element).copied();
1398            let Some(node) =
1399                resolve_live_attached_node(element, seeded_entry.map(|entry| entry.node))
1400            else {
1401                return false;
1402            };
1403            let owner_root = seeded_entry.map(|entry| entry.root).unwrap_or(root_id);
1404            authoritative_nodes.push((element, node, owner_root));
1405        }
1406
1407        self.view_cache_elements_next.insert(root, elements);
1408
1409        for (element, node, owner_root) in authoritative_nodes {
1410            // Touching a retained subtree must not reassign cross-root ownership (ADR 0176).
1411            // If the element is already owned by a different root, keep the original owner and
1412            // rely on diagnostics to flag the mismatch rather than "repairing" it implicitly.
1413            if owner_root == root_id {
1414                // Fast path: expected owner.
1415            }
1416            self.nodes.insert(
1417                element,
1418                NodeEntry {
1419                    node,
1420                    last_seen_frame: frame_id,
1421                    root: owner_root,
1422                },
1423            );
1424
1425            #[cfg(feature = "diagnostics")]
1426            self.touch_debug_identity_for_element(frame_id, element);
1427        }
1428
1429        true
1430    }
1431
1432    pub(crate) fn view_cache_elements_for_root(
1433        &self,
1434        root: GlobalElementId,
1435    ) -> Option<&[GlobalElementId]> {
1436        if let Some(v) = self.view_cache_elements_next.get(&root) {
1437            return Some(v.as_slice());
1438        }
1439        self.view_cache_elements_rendered
1440            .get(&root)
1441            .map(|v| v.as_slice())
1442    }
1443
1444    pub(crate) fn active_text_selection(&self) -> Option<ActiveTextSelection> {
1445        self.active_text_selection
1446    }
1447
1448    pub(crate) fn set_active_text_selection(&mut self, selection: Option<ActiveTextSelection>) {
1449        self.active_text_selection = selection;
1450    }
1451
1452    pub(crate) fn node_entry(&self, id: GlobalElementId) -> Option<NodeEntry> {
1453        self.nodes.get(&id).copied()
1454    }
1455
1456    pub(crate) fn for_each_observed_model_for_invalidation(
1457        &self,
1458        frame_id: FrameId,
1459        mut f: impl FnMut(GlobalElementId, &Vec<(ModelId, Invalidation)>),
1460    ) {
1461        if self.prepared_frame != frame_id {
1462            for (&element, observations) in &self.observed_models_next {
1463                f(element, observations);
1464            }
1465            return;
1466        }
1467
1468        for (&element, observations) in &self.observed_models_rendered {
1469            if self.observed_models_next.contains_key(&element) {
1470                continue;
1471            }
1472            f(element, observations);
1473        }
1474        for (&element, observations) in &self.observed_models_next {
1475            f(element, observations);
1476        }
1477    }
1478
1479    pub(crate) fn for_each_observed_global_for_invalidation(
1480        &self,
1481        frame_id: FrameId,
1482        mut f: impl FnMut(GlobalElementId, &Vec<(TypeId, Invalidation)>),
1483    ) {
1484        if self.prepared_frame != frame_id {
1485            for (&element, observations) in &self.observed_globals_next {
1486                f(element, observations);
1487            }
1488            return;
1489        }
1490
1491        for (&element, observations) in &self.observed_globals_rendered {
1492            if self.observed_globals_next.contains_key(&element) {
1493                continue;
1494            }
1495            f(element, observations);
1496        }
1497        for (&element, observations) in &self.observed_globals_next {
1498            f(element, observations);
1499        }
1500    }
1501
1502    pub(crate) fn element_for_node(&self, node: NodeId) -> Option<GlobalElementId> {
1503        self.nodes
1504            .iter()
1505            .find_map(|(&element, entry)| (entry.node == node).then_some(element))
1506    }
1507
1508    #[track_caller]
1509    pub(crate) fn set_node_entry(&mut self, id: GlobalElementId, entry: NodeEntry) {
1510        #[cfg(feature = "diagnostics")]
1511        if let Some(prev) = self.nodes.get(&id)
1512            && prev.root != entry.root
1513        {
1514            let location = std::panic::Location::caller();
1515            self.debug_node_entry_root_overwrites
1516                .push(NodeEntryRootOverwrite {
1517                    frame_id: entry.last_seen_frame,
1518                    element: id,
1519                    old_root: prev.root,
1520                    new_root: entry.root,
1521                    old_node: prev.node,
1522                    new_node: entry.node,
1523                    file: location.file(),
1524                    line: location.line(),
1525                    column: location.column(),
1526                });
1527            const MAX_RECORDS: usize = 256;
1528            if self.debug_node_entry_root_overwrites.len() > MAX_RECORDS {
1529                let drain = self.debug_node_entry_root_overwrites.len() - MAX_RECORDS;
1530                self.debug_node_entry_root_overwrites.drain(0..drain);
1531            }
1532        }
1533        #[cfg(any(test, feature = "diagnostics"))]
1534        if self.strict_ownership
1535            && let Some(prev) = self.nodes.get(&id)
1536        {
1537            assert_eq!(
1538                prev.root, entry.root,
1539                "ownership root overwrite detected for element {id:?}: old_root={:?} new_root={:?} (cross-root reparenting must be explicit; see ADR 0176)",
1540                prev.root, entry.root
1541            );
1542        }
1543        self.nodes.insert(id, entry);
1544    }
1545
1546    pub(crate) fn retain_nodes(&mut self, f: impl FnMut(&GlobalElementId, &mut NodeEntry) -> bool) {
1547        self.nodes.retain(f);
1548        if let Some(selection) = self.active_text_selection
1549            && !self.nodes.contains_key(&selection.element)
1550        {
1551            self.active_text_selection = None;
1552        }
1553    }
1554
1555    pub(crate) fn clear_stale_interaction_targets_for_frame(&mut self, frame_id: FrameId) {
1556        let is_live_this_frame = |id: GlobalElementId| {
1557            self.nodes
1558                .get(&id)
1559                .is_some_and(|entry| entry.last_seen_frame == frame_id)
1560        };
1561
1562        if let Some(id) = self.hovered_pressable
1563            && !is_live_this_frame(id)
1564        {
1565            self.hovered_pressable = None;
1566            self.hovered_pressable_node = None;
1567        }
1568        if let Some(id) = self.hovered_pressable_raw
1569            && !is_live_this_frame(id)
1570        {
1571            self.hovered_pressable_raw = None;
1572            self.hovered_pressable_raw_node = None;
1573        }
1574        if let Some(id) = self.hovered_pressable_raw_below_barrier
1575            && !is_live_this_frame(id)
1576        {
1577            self.hovered_pressable_raw_below_barrier = None;
1578            self.hovered_pressable_raw_below_barrier_node = None;
1579        }
1580        if let Some(id) = self.pressed_pressable
1581            && !is_live_this_frame(id)
1582        {
1583            self.pressed_pressable = None;
1584            self.pressed_pressable_node = None;
1585        }
1586        if let Some(id) = self.hovered_hover_region
1587            && !is_live_this_frame(id)
1588        {
1589            self.hovered_hover_region = None;
1590            self.hovered_hover_region_node = None;
1591        }
1592    }
1593
1594    pub(crate) fn sync_interaction_target_nodes(
1595        &mut self,
1596        mut resolve: impl FnMut(GlobalElementId, Option<NodeId>) -> Option<NodeId>,
1597    ) {
1598        let hovered_pressable = self.hovered_pressable;
1599        self.hovered_pressable_node = hovered_pressable.and_then(|element| {
1600            resolve(
1601                element,
1602                self.hovered_pressable_node
1603                    .or_else(|| self.node_entry(element).map(|entry| entry.node)),
1604            )
1605        });
1606
1607        let hovered_pressable_raw = self.hovered_pressable_raw;
1608        self.hovered_pressable_raw_node = hovered_pressable_raw.and_then(|element| {
1609            resolve(
1610                element,
1611                self.hovered_pressable_raw_node
1612                    .or_else(|| self.node_entry(element).map(|entry| entry.node)),
1613            )
1614        });
1615
1616        let hovered_pressable_raw_below_barrier = self.hovered_pressable_raw_below_barrier;
1617        self.hovered_pressable_raw_below_barrier_node = hovered_pressable_raw_below_barrier
1618            .and_then(|element| {
1619                resolve(
1620                    element,
1621                    self.hovered_pressable_raw_below_barrier_node
1622                        .or_else(|| self.node_entry(element).map(|entry| entry.node)),
1623                )
1624            });
1625
1626        let pressed_pressable = self.pressed_pressable;
1627        self.pressed_pressable_node = pressed_pressable.and_then(|element| {
1628            resolve(
1629                element,
1630                self.pressed_pressable_node
1631                    .or_else(|| self.node_entry(element).map(|entry| entry.node)),
1632            )
1633        });
1634
1635        let hovered_hover_region = self.hovered_hover_region;
1636        self.hovered_hover_region_node = hovered_hover_region.and_then(|element| {
1637            resolve(
1638                element,
1639                self.hovered_hover_region_node
1640                    .or_else(|| self.node_entry(element).map(|entry| entry.node)),
1641            )
1642        });
1643    }
1644
1645    pub(crate) fn sync_active_text_selection_node(
1646        &mut self,
1647        mut resolve: impl FnMut(GlobalElementId, Option<NodeId>) -> Option<NodeId>,
1648    ) {
1649        let Some(mut selection) = self.active_text_selection else {
1650            return;
1651        };
1652
1653        let Some(node) = resolve(
1654            selection.element,
1655            Some(selection.node)
1656                .or_else(|| self.node_entry(selection.element).map(|entry| entry.node)),
1657        ) else {
1658            self.active_text_selection = None;
1659            return;
1660        };
1661
1662        selection.node = node;
1663        if let Some(root) = self.node_entry(selection.element).map(|entry| entry.root) {
1664            selection.root = root;
1665        }
1666        self.active_text_selection = Some(selection);
1667    }
1668
1669    pub(crate) fn set_root_bounds(&mut self, root: GlobalElementId, bounds: Rect) {
1670        self.root_bounds.insert(root, bounds);
1671    }
1672
1673    pub(crate) fn root_bounds(&self, root: GlobalElementId) -> Option<Rect> {
1674        self.root_bounds.get(&root).copied()
1675    }
1676
1677    pub(crate) fn record_bounds(&mut self, element: GlobalElementId, bounds: Rect) {
1678        self.cur_bounds.insert(element, bounds);
1679
1680        if !self.has_state::<LayoutQueryRegionMarker>(element) {
1681            return;
1682        }
1683        if self
1684            .layout_query_regions_changed_this_frame
1685            .contains(&element)
1686        {
1687            return;
1688        }
1689
1690        // Container queries are primarily sensitive to container size. Ignore origin changes to
1691        // avoid rebuild storms when regions move due to reflow or scrolling.
1692        const EPS_PX: f32 = 0.5;
1693        let changed = match self.prev_bounds.get(&element).copied() {
1694            None => true,
1695            Some(prev) => {
1696                (prev.size.width.0 - bounds.size.width.0).abs() > EPS_PX
1697                    || (prev.size.height.0 - bounds.size.height.0).abs() > EPS_PX
1698            }
1699        };
1700
1701        if changed {
1702            self.layout_query_region_revisions
1703                .entry(element)
1704                .and_modify(|v| *v = v.saturating_add(1))
1705                .or_insert(1);
1706            self.layout_query_regions_changed_this_frame.insert(element);
1707        }
1708    }
1709
1710    pub(crate) fn element_nodes_copy_into(&self, out: &mut Vec<(GlobalElementId, NodeId)>) {
1711        out.clear();
1712        if out.capacity() < self.nodes.len() {
1713            out.reserve(self.nodes.len() - out.capacity());
1714        }
1715        out.extend(
1716            self.nodes
1717                .iter()
1718                .map(|(&element, entry)| (element, entry.node)),
1719        );
1720    }
1721
1722    pub(crate) fn last_bounds(&self, element: GlobalElementId) -> Option<Rect> {
1723        self.prev_bounds.get(&element).copied()
1724    }
1725
1726    pub(crate) fn current_bounds(&self, element: GlobalElementId) -> Option<Rect> {
1727        self.cur_bounds
1728            .get(&element)
1729            .copied()
1730            .or_else(|| self.prev_bounds.get(&element).copied())
1731    }
1732
1733    pub(crate) fn record_visual_bounds(&mut self, element: GlobalElementId, bounds: Rect) {
1734        self.cur_visual_bounds.insert(element, bounds);
1735    }
1736
1737    pub(crate) fn last_visual_bounds(&self, element: GlobalElementId) -> Option<Rect> {
1738        self.prev_visual_bounds.get(&element).copied()
1739    }
1740
1741    pub(crate) fn current_visual_bounds(&self, element: GlobalElementId) -> Option<Rect> {
1742        self.cur_visual_bounds
1743            .get(&element)
1744            .copied()
1745            .or_else(|| self.prev_visual_bounds.get(&element).copied())
1746    }
1747
1748    pub(crate) fn committed_viewport_bounds(&self) -> Rect {
1749        self.committed_viewport_bounds
1750    }
1751
1752    pub(crate) fn committed_scale_factor(&self) -> f32 {
1753        self.committed_scale_factor
1754    }
1755
1756    pub(crate) fn committed_color_scheme(&self) -> Option<ColorScheme> {
1757        self.committed_color_scheme
1758    }
1759
1760    pub(crate) fn committed_prefers_reduced_motion(&self) -> Option<bool> {
1761        self.committed_prefers_reduced_motion
1762    }
1763
1764    pub(crate) fn committed_text_scale_factor(&self) -> Option<f32> {
1765        self.committed_text_scale_factor
1766    }
1767
1768    pub(crate) fn committed_prefers_reduced_transparency(&self) -> Option<bool> {
1769        self.committed_prefers_reduced_transparency
1770    }
1771
1772    pub(crate) fn committed_accent_color(&self) -> Option<Color> {
1773        self.committed_accent_color
1774    }
1775
1776    pub(crate) fn committed_contrast_preference(&self) -> Option<ContrastPreference> {
1777        self.committed_contrast_preference
1778    }
1779
1780    pub(crate) fn committed_forced_colors_mode(&self) -> Option<ForcedColorsMode> {
1781        self.committed_forced_colors_mode
1782    }
1783
1784    pub(crate) fn committed_primary_pointer_type(&self) -> PointerType {
1785        self.committed_primary_pointer_type
1786            .unwrap_or(PointerType::Unknown)
1787    }
1788
1789    pub(crate) fn committed_safe_area_insets(&self) -> Option<Edges> {
1790        self.committed_safe_area_insets
1791    }
1792
1793    pub(crate) fn committed_occlusion_insets(&self) -> Option<Edges> {
1794        self.committed_occlusion_insets
1795    }
1796
1797    pub(crate) fn occlusion_insets_changed_this_frame(&self) -> bool {
1798        self.environment_changed_this_frame
1799            .contains(&EnvironmentQueryKey::OcclusionInsets)
1800    }
1801
1802    pub(crate) fn set_committed_prefers_reduced_motion(&mut self, value: Option<bool>) {
1803        if self.committed_prefers_reduced_motion == value {
1804            return;
1805        }
1806        self.committed_prefers_reduced_motion = value;
1807        self.environment_revisions
1808            .entry(EnvironmentQueryKey::PrefersReducedMotion)
1809            .and_modify(|v| *v = v.saturating_add(1))
1810            .or_insert(1);
1811        self.environment_changed_this_frame
1812            .insert(EnvironmentQueryKey::PrefersReducedMotion);
1813    }
1814
1815    #[allow(dead_code)]
1816    pub(crate) fn set_committed_text_scale_factor(&mut self, value: Option<f32>) {
1817        let changed = match (self.committed_text_scale_factor, value) {
1818            (Some(a), Some(b)) => (a - b).abs() > 0.0001,
1819            (None, None) => false,
1820            _ => true,
1821        };
1822        if !changed {
1823            return;
1824        }
1825
1826        self.committed_text_scale_factor = value;
1827        self.environment_revisions
1828            .entry(EnvironmentQueryKey::TextScaleFactor)
1829            .and_modify(|v| *v = v.saturating_add(1))
1830            .or_insert(1);
1831        self.environment_changed_this_frame
1832            .insert(EnvironmentQueryKey::TextScaleFactor);
1833    }
1834
1835    #[allow(dead_code)]
1836    pub(crate) fn set_committed_prefers_reduced_transparency(&mut self, value: Option<bool>) {
1837        if self.committed_prefers_reduced_transparency == value {
1838            return;
1839        }
1840        self.committed_prefers_reduced_transparency = value;
1841        self.environment_revisions
1842            .entry(EnvironmentQueryKey::PrefersReducedTransparency)
1843            .and_modify(|v| *v = v.saturating_add(1))
1844            .or_insert(1);
1845        self.environment_changed_this_frame
1846            .insert(EnvironmentQueryKey::PrefersReducedTransparency);
1847    }
1848
1849    #[allow(dead_code)]
1850    pub(crate) fn set_committed_accent_color(&mut self, value: Option<Color>) {
1851        if self.committed_accent_color == value {
1852            return;
1853        }
1854        self.committed_accent_color = value;
1855        self.environment_revisions
1856            .entry(EnvironmentQueryKey::AccentColor)
1857            .and_modify(|v| *v = v.saturating_add(1))
1858            .or_insert(1);
1859        self.environment_changed_this_frame
1860            .insert(EnvironmentQueryKey::AccentColor);
1861    }
1862
1863    pub(crate) fn set_committed_color_scheme(&mut self, value: Option<ColorScheme>) {
1864        if self.committed_color_scheme == value {
1865            return;
1866        }
1867        self.committed_color_scheme = value;
1868        self.environment_revisions
1869            .entry(EnvironmentQueryKey::ColorScheme)
1870            .and_modify(|v| *v = v.saturating_add(1))
1871            .or_insert(1);
1872        self.environment_changed_this_frame
1873            .insert(EnvironmentQueryKey::ColorScheme);
1874    }
1875
1876    pub(crate) fn set_committed_contrast_preference(&mut self, value: Option<ContrastPreference>) {
1877        if self.committed_contrast_preference == value {
1878            return;
1879        }
1880        self.committed_contrast_preference = value;
1881        self.environment_revisions
1882            .entry(EnvironmentQueryKey::PrefersContrast)
1883            .and_modify(|v| *v = v.saturating_add(1))
1884            .or_insert(1);
1885        self.environment_changed_this_frame
1886            .insert(EnvironmentQueryKey::PrefersContrast);
1887    }
1888
1889    pub(crate) fn set_committed_forced_colors_mode(&mut self, value: Option<ForcedColorsMode>) {
1890        if self.committed_forced_colors_mode == value {
1891            return;
1892        }
1893        self.committed_forced_colors_mode = value;
1894        self.environment_revisions
1895            .entry(EnvironmentQueryKey::ForcedColorsMode)
1896            .and_modify(|v| *v = v.saturating_add(1))
1897            .or_insert(1);
1898        self.environment_changed_this_frame
1899            .insert(EnvironmentQueryKey::ForcedColorsMode);
1900    }
1901
1902    pub(crate) fn record_committed_primary_pointer_type(&mut self, pointer_type: PointerType) {
1903        if self.committed_primary_pointer_type == Some(pointer_type) {
1904            return;
1905        }
1906        self.committed_primary_pointer_type = Some(pointer_type);
1907        self.environment_revisions
1908            .entry(EnvironmentQueryKey::PrimaryPointerType)
1909            .and_modify(|v| *v = v.saturating_add(1))
1910            .or_insert(1);
1911        self.environment_changed_this_frame
1912            .insert(EnvironmentQueryKey::PrimaryPointerType);
1913    }
1914
1915    pub(crate) fn record_committed_safe_area_insets(&mut self, insets: Option<Edges>) {
1916        if self.committed_safe_area_insets == insets {
1917            return;
1918        }
1919        self.committed_safe_area_insets = insets;
1920        self.environment_revisions
1921            .entry(EnvironmentQueryKey::SafeAreaInsets)
1922            .and_modify(|v| *v = v.saturating_add(1))
1923            .or_insert(1);
1924        self.environment_changed_this_frame
1925            .insert(EnvironmentQueryKey::SafeAreaInsets);
1926    }
1927
1928    pub(crate) fn record_committed_occlusion_insets(&mut self, insets: Option<Edges>) {
1929        if self.committed_occlusion_insets == insets {
1930            return;
1931        }
1932        self.committed_occlusion_insets = insets;
1933        self.environment_revisions
1934            .entry(EnvironmentQueryKey::OcclusionInsets)
1935            .and_modify(|v| *v = v.saturating_add(1))
1936            .or_insert(1);
1937        self.environment_changed_this_frame
1938            .insert(EnvironmentQueryKey::OcclusionInsets);
1939    }
1940
1941    pub(crate) fn record_committed_viewport_bounds(&mut self, bounds: Rect) {
1942        // Environment-driven responsiveness is typically sensitive to viewport size. Ignore origin
1943        // changes so panning/multi-monitor movement cannot trigger rebuild storms.
1944        const EPS_PX: f32 = 0.5;
1945        let prev = self.committed_viewport_bounds.size;
1946        let next = bounds.size;
1947        let changed = (prev.width.0 - next.width.0).abs() > EPS_PX
1948            || (prev.height.0 - next.height.0).abs() > EPS_PX;
1949
1950        self.committed_viewport_bounds = bounds;
1951        if !changed {
1952            return;
1953        }
1954
1955        self.environment_revisions
1956            .entry(EnvironmentQueryKey::ViewportSize)
1957            .and_modify(|v| *v = v.saturating_add(1))
1958            .or_insert(1);
1959        self.environment_changed_this_frame
1960            .insert(EnvironmentQueryKey::ViewportSize);
1961    }
1962
1963    pub(crate) fn record_committed_scale_factor(&mut self, scale_factor: f32) {
1964        if (self.committed_scale_factor - scale_factor).abs() < 0.0001 {
1965            return;
1966        }
1967        self.committed_scale_factor = scale_factor;
1968        self.environment_revisions
1969            .entry(EnvironmentQueryKey::ScaleFactor)
1970            .and_modify(|v| *v = v.saturating_add(1))
1971            .or_insert(1);
1972        self.environment_changed_this_frame
1973            .insert(EnvironmentQueryKey::ScaleFactor);
1974    }
1975
1976    pub(crate) fn wants_continuous_frames(&self) -> bool {
1977        self.continuous_frames.load(Ordering::Relaxed) > 0
1978    }
1979
1980    pub(crate) fn begin_continuous_frames(
1981        &self,
1982        owner: Option<GlobalElementId>,
1983    ) -> ContinuousFrames {
1984        self.continuous_frames.fetch_add(1, Ordering::Relaxed);
1985        #[cfg(not(feature = "diagnostics"))]
1986        let _ = owner;
1987        #[cfg(feature = "diagnostics")]
1988        if let Some(owner) = owner {
1989            let mut owners = self
1990                .continuous_frame_owners
1991                .lock()
1992                .unwrap_or_else(|err| err.into_inner());
1993            owners
1994                .entry(owner)
1995                .and_modify(|count| *count = count.saturating_add(1))
1996                .or_insert(1);
1997        }
1998        ContinuousFrames {
1999            leases: self.continuous_frames.clone(),
2000            #[cfg(feature = "diagnostics")]
2001            owners: owner.map(|owner| (self.continuous_frame_owners.clone(), owner)),
2002        }
2003    }
2004
2005    #[cfg(feature = "diagnostics")]
2006    fn diagnostics_snapshot(&self) -> WindowElementDiagnosticsSnapshot {
2007        let invalidation_sort_key = |inv: Invalidation| match inv {
2008            Invalidation::Layout => 0u8,
2009            Invalidation::Paint => 1u8,
2010            Invalidation::HitTest => 2u8,
2011            Invalidation::HitTestOnly => 3u8,
2012        };
2013
2014        let env_key_label = |key: EnvironmentQueryKey| match key {
2015            EnvironmentQueryKey::ViewportSize => "viewport_size",
2016            EnvironmentQueryKey::ScaleFactor => "scale_factor",
2017            EnvironmentQueryKey::ColorScheme => "color_scheme",
2018            EnvironmentQueryKey::PrefersReducedMotion => "prefers_reduced_motion",
2019            EnvironmentQueryKey::TextScaleFactor => "text_scale_factor",
2020            EnvironmentQueryKey::PrefersReducedTransparency => "prefers_reduced_transparency",
2021            EnvironmentQueryKey::AccentColor => "accent_color",
2022            EnvironmentQueryKey::PrefersContrast => "prefers_contrast",
2023            EnvironmentQueryKey::ForcedColorsMode => "forced_colors_mode",
2024            EnvironmentQueryKey::PrimaryPointerType => "primary_pointer_type",
2025            EnvironmentQueryKey::SafeAreaInsets => "safe_area_insets",
2026            EnvironmentQueryKey::OcclusionInsets => "occlusion_insets",
2027        };
2028
2029        let bounds_for = |element: Option<GlobalElementId>| {
2030            element.and_then(|id| {
2031                self.prev_bounds
2032                    .get(&id)
2033                    .copied()
2034                    .or_else(|| self.cur_bounds.get(&id).copied())
2035            })
2036        };
2037        let visual_bounds_for = |element: Option<GlobalElementId>| {
2038            element.and_then(|id| {
2039                self.prev_visual_bounds
2040                    .get(&id)
2041                    .copied()
2042                    .or_else(|| self.cur_visual_bounds.get(&id).copied())
2043                    .or_else(|| {
2044                        self.prev_bounds
2045                            .get(&id)
2046                            .copied()
2047                            .or_else(|| self.cur_bounds.get(&id).copied())
2048                    })
2049            })
2050        };
2051        let node_for = |element: Option<GlobalElementId>| {
2052            element.and_then(|id| self.node_entry(id).map(|e| e.node))
2053        };
2054
2055        let layout_query_region_name_for = |element: GlobalElementId| -> Option<StdArc<str>> {
2056            let key = (element, TypeId::of::<LayoutQueryRegionMarker>());
2057            self.state_any_ref(&key)
2058                .and_then(|any| any.downcast_ref::<LayoutQueryRegionMarker>())
2059                .and_then(|marker| marker.name.clone())
2060        };
2061
2062        let mut layout_query_regions: Vec<LayoutQueryRegionDiagnosticsSnapshot> = self
2063            .nodes
2064            .iter()
2065            .filter(|(_, entry)| entry.last_seen_frame == self.prepared_frame)
2066            .map(|(&element, _)| element)
2067            .filter(|&element| self.has_state::<LayoutQueryRegionMarker>(element))
2068            .map(|element| {
2069                let committed_bounds = self.prev_bounds.get(&element).copied();
2070                let current_bounds = self.cur_bounds.get(&element).copied().or(committed_bounds);
2071                LayoutQueryRegionDiagnosticsSnapshot {
2072                    region: element,
2073                    name: layout_query_region_name_for(element),
2074                    revision: self.layout_query_region_revision(element),
2075                    changed_this_frame: self
2076                        .layout_query_regions_changed_this_frame
2077                        .contains(&element),
2078                    committed_bounds,
2079                    current_bounds,
2080                }
2081            })
2082            .collect();
2083        layout_query_regions.sort_by_key(|r| r.region.0);
2084
2085        let mut observed_layout_queries: Vec<ElementObservedLayoutQueriesDiagnosticsSnapshot> =
2086            self.observed_layout_queries_next
2087                .iter()
2088                .map(|(element, list)| {
2089                    let deps_fingerprint = self.layout_query_deps_fingerprint_from_list(Some(list));
2090                    let mut regions: Vec<ObservedLayoutQueryRegionDiagnosticsSnapshot> = list
2091                        .iter()
2092                        .map(
2093                            |(region, inv)| ObservedLayoutQueryRegionDiagnosticsSnapshot {
2094                                region: *region,
2095                                invalidation: *inv,
2096                                region_revision: self.layout_query_region_revision(*region),
2097                                region_changed_this_frame: self
2098                                    .layout_query_regions_changed_this_frame
2099                                    .contains(region),
2100                                region_name: layout_query_region_name_for(*region),
2101                                region_committed_bounds: self.prev_bounds.get(region).copied(),
2102                            },
2103                        )
2104                        .collect();
2105                    regions.sort_by(|a, b| {
2106                        a.region.0.cmp(&b.region.0).then_with(|| {
2107                            let key = |inv: Invalidation| match inv {
2108                                Invalidation::Layout => 0u8,
2109                                Invalidation::Paint => 1u8,
2110                                Invalidation::HitTest => 2u8,
2111                                Invalidation::HitTestOnly => 3u8,
2112                            };
2113                            key(a.invalidation).cmp(&key(b.invalidation))
2114                        })
2115                    });
2116
2117                    ElementObservedLayoutQueriesDiagnosticsSnapshot {
2118                        element: *element,
2119                        deps_fingerprint,
2120                        regions,
2121                    }
2122                })
2123                .collect();
2124        observed_layout_queries.sort_by_key(|e| e.element.0);
2125
2126        let mut observed_environment: Vec<ElementObservedEnvironmentDiagnosticsSnapshot> = self
2127            .observed_environment_next
2128            .iter()
2129            .map(|(element, list)| {
2130                let deps_fingerprint = self.environment_deps_fingerprint_from_list(Some(list));
2131                let mut keys: Vec<ObservedEnvironmentKeyDiagnosticsSnapshot> = list
2132                    .iter()
2133                    .map(|(key, inv)| ObservedEnvironmentKeyDiagnosticsSnapshot {
2134                        key: StdArc::<str>::from(env_key_label(*key)),
2135                        invalidation: *inv,
2136                        key_revision: self.environment_revision(*key),
2137                        key_changed_this_frame: self.environment_changed_this_frame.contains(key),
2138                    })
2139                    .collect();
2140                keys.sort_by(|a, b| {
2141                    a.key.as_ref().cmp(b.key.as_ref()).then_with(|| {
2142                        invalidation_sort_key(a.invalidation)
2143                            .cmp(&invalidation_sort_key(b.invalidation))
2144                    })
2145                });
2146
2147                ElementObservedEnvironmentDiagnosticsSnapshot {
2148                    element: *element,
2149                    deps_fingerprint,
2150                    keys,
2151                }
2152            })
2153            .collect();
2154        observed_environment.sort_by_key(|e| e.element.0);
2155
2156        let environment = EnvironmentQueryDiagnosticsSnapshot {
2157            viewport_bounds: self.committed_viewport_bounds,
2158            scale_factor: self.committed_scale_factor,
2159            color_scheme: self.committed_color_scheme,
2160            prefers_reduced_motion: self.committed_prefers_reduced_motion,
2161            text_scale_factor: self.committed_text_scale_factor,
2162            prefers_reduced_transparency: self.committed_prefers_reduced_transparency,
2163            accent_color: self.committed_accent_color,
2164            contrast_preference: self.committed_contrast_preference,
2165            forced_colors_mode: self.committed_forced_colors_mode,
2166            primary_pointer_type: self.committed_primary_pointer_type(),
2167            safe_area_insets: self.committed_safe_area_insets,
2168            occlusion_insets: self.committed_occlusion_insets,
2169        };
2170
2171        let mut view_cache_reuse_roots: Vec<GlobalElementId> =
2172            self.view_cache_reuse_roots.iter().copied().collect();
2173        view_cache_reuse_roots.sort_by_key(|id| id.0);
2174
2175        let view_cache_reuse_root_element_counts: Vec<(GlobalElementId, u32)> =
2176            view_cache_reuse_roots
2177                .iter()
2178                .map(|root| {
2179                    let count = self
2180                        .view_cache_elements_rendered
2181                        .get(root)
2182                        .map(|v| v.len())
2183                        .unwrap_or(0);
2184                    (*root, count.min(u32::MAX as usize) as u32)
2185                })
2186                .collect();
2187
2188        const ELEMENT_SAMPLE: usize = 16;
2189        let view_cache_reuse_root_element_samples: Vec<ViewCacheReuseRootElementsSample> =
2190            view_cache_reuse_roots
2191                .iter()
2192                .map(|&root| {
2193                    let elements = self.view_cache_elements_for_root(root).unwrap_or(&[]);
2194                    let elements_len = elements.len().min(u32::MAX as usize) as u32;
2195                    let elements_head: Vec<GlobalElementId> =
2196                        elements.iter().take(ELEMENT_SAMPLE).copied().collect();
2197                    let elements_tail: Vec<GlobalElementId> = if elements.len() > ELEMENT_SAMPLE {
2198                        elements
2199                            .iter()
2200                            .skip(elements.len().saturating_sub(ELEMENT_SAMPLE))
2201                            .copied()
2202                            .collect()
2203                    } else {
2204                        Vec::new()
2205                    };
2206
2207                    ViewCacheReuseRootElementsSample {
2208                        root,
2209                        node: self.node_entry(root).map(|e| e.node),
2210                        elements_len,
2211                        elements_head,
2212                        elements_tail,
2213                    }
2214                })
2215                .collect();
2216
2217        let mut continuous_frame_leases: Vec<ContinuousFrameLeaseDiagnosticsSnapshot> = {
2218            let owners = self
2219                .continuous_frame_owners
2220                .lock()
2221                .unwrap_or_else(|err| err.into_inner());
2222            owners
2223                .iter()
2224                .map(
2225                    |(&element, &count)| ContinuousFrameLeaseDiagnosticsSnapshot {
2226                        element,
2227                        count,
2228                        debug_path: self.debug_path_for_element(element),
2229                    },
2230                )
2231                .collect()
2232        };
2233        continuous_frame_leases.sort_by(|a, b| {
2234            b.count
2235                .cmp(&a.count)
2236                .then_with(|| a.element.0.cmp(&b.element.0))
2237        });
2238
2239        let mut animation_frame_request_roots: Vec<AnimationFrameRequestRootDiagnosticsSnapshot> =
2240            self.raf_notify_roots_debug
2241                .iter()
2242                .copied()
2243                .map(|element| AnimationFrameRequestRootDiagnosticsSnapshot {
2244                    element,
2245                    debug_path: self.debug_path_for_element(element),
2246                })
2247                .collect();
2248        animation_frame_request_roots.sort_by_key(|entry| entry.element.0);
2249
2250        let rendered_state_entries = self.rendered_state.len() as u64;
2251        let next_state_entries = self.next_state.len() as u64;
2252        let lag_state_frames = self.lag_state.len() as u64;
2253        let lag_state_entries_total = self
2254            .lag_state
2255            .iter()
2256            .fold(0u64, |acc, state| acc.saturating_add(state.len() as u64));
2257        let state_entries_total = rendered_state_entries
2258            .saturating_add(next_state_entries)
2259            .saturating_add(lag_state_entries_total);
2260        let nodes_count = self.nodes.len() as u64;
2261        let bounds_entries_total = (self.root_bounds.len() as u64)
2262            .saturating_add(self.prev_bounds.len() as u64)
2263            .saturating_add(self.cur_bounds.len() as u64)
2264            .saturating_add(self.prev_visual_bounds.len() as u64)
2265            .saturating_add(self.cur_visual_bounds.len() as u64);
2266        let timer_targets_count = self.timer_targets.len() as u64;
2267        let transient_events_count = self.transient_events.len() as u64;
2268        let view_cache_state_key_roots_count = (self.view_cache_state_keys_rendered.len() as u64)
2269            .saturating_add(self.view_cache_state_keys_next.len() as u64);
2270        let view_cache_state_key_entries_total = self
2271            .view_cache_state_keys_rendered
2272            .values()
2273            .fold(0u64, |acc, keys| acc.saturating_add(keys.len() as u64))
2274            .saturating_add(
2275                self.view_cache_state_keys_next
2276                    .values()
2277                    .fold(0u64, |acc, keys| acc.saturating_add(keys.len() as u64)),
2278            );
2279        let view_cache_element_roots_count = (self.view_cache_elements_rendered.len() as u64)
2280            .saturating_add(self.view_cache_elements_next.len() as u64);
2281        let view_cache_element_entries_total = self
2282            .view_cache_elements_rendered
2283            .values()
2284            .fold(0u64, |acc, elements| {
2285                acc.saturating_add(elements.len() as u64)
2286            })
2287            .saturating_add(
2288                self.view_cache_elements_next
2289                    .values()
2290                    .fold(0u64, |acc, elements| {
2291                        acc.saturating_add(elements.len() as u64)
2292                    }),
2293            );
2294        let view_cache_key_mismatch_roots_count = self.view_cache_key_mismatch_roots.len() as u64;
2295        let scratch_element_children_vec_pool_len =
2296            self.scratch_element_children_vec_pool.len() as u64;
2297        let scratch_element_children_vec_pool_capacity_total = self
2298            .scratch_element_children_vec_pool
2299            .iter()
2300            .fold(0u64, |acc, vec| acc.saturating_add(vec.capacity() as u64));
2301        let scratch_element_children_vec_pool_bytes_estimate_total = self
2302            .scratch_element_children_vec_pool
2303            .iter()
2304            .fold(0u64, |acc, vec| {
2305                acc.saturating_add(
2306                    (vec.capacity() as u64)
2307                        .saturating_mul(std::mem::size_of::<AnyElement>() as u64),
2308                )
2309            });
2310
2311        const KEEP_ALIVE_ROOT_SAMPLE: usize = 16;
2312        let mut retained_keep_alive_roots: Vec<NodeId> = self
2313            .retained_virtual_list_keep_alive_roots
2314            .iter()
2315            .copied()
2316            .collect();
2317        retained_keep_alive_roots.sort_by_key(|n| n.data().as_ffi());
2318        let retained_keep_alive_roots_len =
2319            retained_keep_alive_roots.len().min(u32::MAX as usize) as u32;
2320        let retained_keep_alive_roots_head: Vec<NodeId> = retained_keep_alive_roots
2321            .iter()
2322            .take(KEEP_ALIVE_ROOT_SAMPLE)
2323            .copied()
2324            .collect();
2325        let retained_keep_alive_roots_tail: Vec<NodeId> =
2326            if retained_keep_alive_roots.len() > KEEP_ALIVE_ROOT_SAMPLE {
2327                retained_keep_alive_roots
2328                    .iter()
2329                    .skip(
2330                        retained_keep_alive_roots
2331                            .len()
2332                            .saturating_sub(KEEP_ALIVE_ROOT_SAMPLE),
2333                    )
2334                    .copied()
2335                    .collect()
2336            } else {
2337                Vec::new()
2338            };
2339
2340        WindowElementDiagnosticsSnapshot {
2341            focused_element: self.focused_element,
2342            focused_element_node: node_for(self.focused_element),
2343            focused_element_bounds: bounds_for(self.focused_element),
2344            focused_element_visual_bounds: visual_bounds_for(self.focused_element),
2345            active_text_selection: self
2346                .active_text_selection
2347                .map(|sel| (sel.root, sel.element)),
2348            hovered_pressable: self.hovered_pressable,
2349            hovered_pressable_node: self.hovered_pressable_node,
2350            hovered_pressable_bounds: bounds_for(self.hovered_pressable),
2351            hovered_pressable_visual_bounds: visual_bounds_for(self.hovered_pressable),
2352            pressed_pressable: self.pressed_pressable,
2353            pressed_pressable_node: self.pressed_pressable_node,
2354            pressed_pressable_bounds: bounds_for(self.pressed_pressable),
2355            pressed_pressable_visual_bounds: visual_bounds_for(self.pressed_pressable),
2356            hovered_hover_region: self.hovered_hover_region,
2357            wants_continuous_frames: self.wants_continuous_frames(),
2358            continuous_frame_leases,
2359            animation_frame_request_roots,
2360            observed_models: self
2361                .observed_models_next
2362                .iter()
2363                .map(|(element, list)| {
2364                    (
2365                        *element,
2366                        list.iter()
2367                            .map(|(model, inv)| (model.data().as_ffi(), *inv))
2368                            .collect(),
2369                    )
2370                })
2371                .collect(),
2372            observed_globals: self
2373                .observed_globals_next
2374                .iter()
2375                .map(|(element, list)| {
2376                    (
2377                        *element,
2378                        list.iter()
2379                            .map(|(ty, inv)| (format!("{ty:?}"), *inv))
2380                            .collect(),
2381                    )
2382                })
2383                .collect(),
2384            observed_layout_queries,
2385            layout_query_regions,
2386            environment,
2387            observed_environment,
2388            view_cache_reuse_roots,
2389            view_cache_reuse_root_element_counts,
2390            view_cache_reuse_root_element_samples,
2391            rendered_state_entries,
2392            next_state_entries,
2393            lag_state_frames,
2394            lag_state_entries_total,
2395            state_entries_total,
2396            nodes_count,
2397            bounds_entries_total,
2398            timer_targets_count,
2399            transient_events_count,
2400            view_cache_state_key_roots_count,
2401            view_cache_state_key_entries_total,
2402            view_cache_element_roots_count,
2403            view_cache_element_entries_total,
2404            view_cache_key_mismatch_roots_count,
2405            scratch_element_children_vec_pool_len,
2406            scratch_element_children_vec_pool_capacity_total,
2407            scratch_element_children_vec_pool_bytes_estimate_total,
2408            retained_keep_alive_roots_len,
2409            retained_keep_alive_roots_head,
2410            retained_keep_alive_roots_tail,
2411            node_entry_root_overwrites: self.debug_node_entry_root_overwrites.clone(),
2412            overlay_placement: self.overlay_placement.clone(),
2413        }
2414    }
2415
2416    #[cfg(feature = "diagnostics")]
2417    pub(crate) fn record_overlay_placement_anchored_panel(
2418        &mut self,
2419        frame_id: FrameId,
2420        overlay_root_name: Option<StdArc<str>>,
2421        anchor_element: Option<GlobalElementId>,
2422        content_element: Option<GlobalElementId>,
2423        trace: AnchoredPanelLayoutTrace,
2424    ) {
2425        let name = overlay_root_name.clone();
2426        let next = OverlayPlacementDiagnosticsRecord::AnchoredPanel(
2427            OverlayAnchoredPanelPlacementDiagnosticsRecord {
2428                frame_id,
2429                overlay_root_name,
2430                anchor_element,
2431                content_element,
2432                trace,
2433            },
2434        );
2435
2436        if let Some(existing) = self
2437            .overlay_placement
2438            .iter_mut()
2439            .rev()
2440            .find(|rec| match rec {
2441                OverlayPlacementDiagnosticsRecord::AnchoredPanel(r) => {
2442                    r.overlay_root_name == name
2443                        && r.anchor_element == anchor_element
2444                        && r.content_element == content_element
2445                }
2446                _ => false,
2447            })
2448        {
2449            *existing = next;
2450            return;
2451        }
2452
2453        self.overlay_placement.push(next);
2454    }
2455
2456    #[cfg(feature = "diagnostics")]
2457    #[allow(clippy::too_many_arguments)]
2458    pub(crate) fn record_overlay_placement_placed_rect(
2459        &mut self,
2460        frame_id: FrameId,
2461        overlay_root_name: Option<StdArc<str>>,
2462        anchor_element: Option<GlobalElementId>,
2463        content_element: Option<GlobalElementId>,
2464        outer: Rect,
2465        anchor: Rect,
2466        placed: Rect,
2467        side: Option<Side>,
2468    ) {
2469        let name = overlay_root_name.clone();
2470        let next =
2471            OverlayPlacementDiagnosticsRecord::PlacedRect(OverlayPlacedRectDiagnosticsRecord {
2472                frame_id,
2473                overlay_root_name,
2474                anchor_element,
2475                content_element,
2476                outer,
2477                anchor,
2478                placed,
2479                side,
2480            });
2481
2482        if let Some(existing) = self
2483            .overlay_placement
2484            .iter_mut()
2485            .rev()
2486            .find(|rec| match rec {
2487                OverlayPlacementDiagnosticsRecord::PlacedRect(r) => {
2488                    r.overlay_root_name == name
2489                        && r.anchor_element == anchor_element
2490                        && r.content_element == content_element
2491                }
2492                _ => false,
2493            })
2494        {
2495            *existing = next;
2496            return;
2497        }
2498
2499        self.overlay_placement.push(next);
2500    }
2501
2502    #[cfg(feature = "diagnostics")]
2503    pub(crate) fn record_debug_root(
2504        &mut self,
2505        frame_id: FrameId,
2506        root: GlobalElementId,
2507        name: &str,
2508    ) {
2509        self.debug_identity.entries.insert(
2510            root,
2511            DebugIdentityEntry {
2512                parent: None,
2513                segment: DebugIdentitySegment::Root {
2514                    name: StdArc::<str>::from(name),
2515                },
2516                last_seen_frame: frame_id,
2517            },
2518        );
2519    }
2520
2521    #[cfg(feature = "diagnostics")]
2522    #[allow(clippy::too_many_arguments)]
2523    pub(crate) fn record_debug_child(
2524        &mut self,
2525        frame_id: FrameId,
2526        parent: GlobalElementId,
2527        child: GlobalElementId,
2528        file: &'static str,
2529        line: u32,
2530        column: u32,
2531        key_hash: Option<u64>,
2532        name: Option<&str>,
2533        slot: u64,
2534    ) {
2535        self.debug_identity.entries.insert(
2536            child,
2537            DebugIdentityEntry {
2538                parent: Some(parent),
2539                segment: DebugIdentitySegment::Child {
2540                    file,
2541                    line,
2542                    column,
2543                    key_hash,
2544                    name: name.map(StdArc::<str>::from),
2545                    slot,
2546                },
2547                last_seen_frame: frame_id,
2548            },
2549        );
2550    }
2551
2552    #[cfg(feature = "diagnostics")]
2553    pub(crate) fn touch_debug_identity_for_element(
2554        &mut self,
2555        frame_id: FrameId,
2556        element: GlobalElementId,
2557    ) {
2558        // Keep debug paths stable across view-cache reuse frames.
2559        //
2560        // During cache-hit frames we may only "touch" a subset of elements for GC bookkeeping.
2561        // If we only bump the leaf entry, ancestor identity segments can be pruned (based on
2562        // `gc_lag_frames`), causing `debug_path_for_element()` to fail even though the element is
2563        // still alive in the UI tree.
2564        //
2565        // Touch the full ancestor chain, but stop early if we've already touched this chain on
2566        // the current frame.
2567        let mut cur = element;
2568        let mut guard = 0usize;
2569        while guard < 256 {
2570            guard += 1;
2571            let Some(entry) = self.debug_identity.entries.get_mut(&cur) else {
2572                break;
2573            };
2574            if entry.last_seen_frame == frame_id {
2575                break;
2576            }
2577            entry.last_seen_frame = frame_id;
2578            let Some(parent) = entry.parent else {
2579                break;
2580            };
2581            cur = parent;
2582        }
2583    }
2584
2585    #[cfg(feature = "diagnostics")]
2586    pub(crate) fn debug_path_for_element(&self, element: GlobalElementId) -> Option<String> {
2587        let mut segments: Vec<String> = Vec::new();
2588        let mut cur = element;
2589        let mut guard = 0usize;
2590        while guard < 256 {
2591            guard += 1;
2592            let entry = self.debug_identity.entries.get(&cur)?;
2593            segments.push(entry.segment.format());
2594            if let Some(parent) = entry.parent {
2595                cur = parent;
2596                continue;
2597            }
2598            break;
2599        }
2600        segments.reverse();
2601        Some(format!("{} ({:#x})", segments.join("."), element.0))
2602    }
2603}
2604
2605#[must_use]
2606pub struct ContinuousFrames {
2607    leases: Arc<AtomicUsize>,
2608    #[cfg(feature = "diagnostics")]
2609    owners: Option<(Arc<Mutex<HashMap<GlobalElementId, u32>>>, GlobalElementId)>,
2610}
2611
2612impl Drop for ContinuousFrames {
2613    fn drop(&mut self) {
2614        let _ = self
2615            .leases
2616            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| v.checked_sub(1));
2617        #[cfg(feature = "diagnostics")]
2618        if let Some((owners, owner)) = self.owners.as_ref() {
2619            let mut owners = owners.lock().unwrap_or_else(|err| err.into_inner());
2620            if let Some(count) = owners.get_mut(owner) {
2621                if *count <= 1 {
2622                    owners.remove(owner);
2623                } else {
2624                    *count -= 1;
2625                }
2626            }
2627        }
2628    }
2629}
2630
2631#[cfg(feature = "diagnostics")]
2632#[derive(Default)]
2633struct DebugIdentityRegistry {
2634    entries: HashMap<GlobalElementId, DebugIdentityEntry>,
2635}
2636
2637#[cfg(feature = "diagnostics")]
2638struct DebugIdentityEntry {
2639    parent: Option<GlobalElementId>,
2640    segment: DebugIdentitySegment,
2641    last_seen_frame: FrameId,
2642}
2643
2644#[cfg(feature = "diagnostics")]
2645enum DebugIdentitySegment {
2646    Root {
2647        name: StdArc<str>,
2648    },
2649    Child {
2650        file: &'static str,
2651        line: u32,
2652        column: u32,
2653        key_hash: Option<u64>,
2654        name: Option<StdArc<str>>,
2655        slot: u64,
2656    },
2657}
2658
2659#[cfg(feature = "diagnostics")]
2660impl DebugIdentitySegment {
2661    fn format(&self) -> String {
2662        match self {
2663            DebugIdentitySegment::Root { name } => format!("root[{name}]"),
2664            DebugIdentitySegment::Child {
2665                file,
2666                line,
2667                column,
2668                key_hash,
2669                name,
2670                slot,
2671            } => {
2672                if let Some(name) = name.as_deref() {
2673                    format!("{file}:{line}:{column}[name={name}]")
2674                } else if let Some(k) = key_hash {
2675                    format!("{file}:{line}:{column}[key={k:#x}]")
2676                } else {
2677                    format!("{file}:{line}:{column}[slot={slot}]")
2678                }
2679            }
2680        }
2681    }
2682}
2683
2684#[cfg(test)]
2685mod tests {
2686    use super::*;
2687    use fret_core::{Point, Px, Size};
2688
2689    #[test]
2690    fn primary_pointer_type_defaults_to_unknown_until_observed() {
2691        let mut state = WindowElementState::default();
2692        assert_eq!(state.committed_primary_pointer_type(), PointerType::Unknown);
2693
2694        state.record_committed_primary_pointer_type(PointerType::Touch);
2695        assert_eq!(state.committed_primary_pointer_type(), PointerType::Touch);
2696    }
2697
2698    #[test]
2699    fn transient_events_survive_one_frame_and_clear_on_read() {
2700        let mut state = WindowElementState::default();
2701        let element = GlobalElementId(123);
2702        let key = 0xDEAD_BEEF;
2703
2704        state.prepare_for_frame(FrameId(1), 0);
2705        state.record_transient_event(element, key);
2706
2707        // A transient event should be observable in the next prepared frame, since input can
2708        // arrive between frames.
2709        state.prepare_for_frame(FrameId(2), 0);
2710        assert!(state.take_transient_event(element, key));
2711        assert!(!state.take_transient_event(element, key));
2712    }
2713
2714    #[test]
2715    fn transient_events_prune_after_two_frames_if_not_consumed() {
2716        let mut state = WindowElementState::default();
2717        let element = GlobalElementId(123);
2718        let key = 0xDEAD_BEEF;
2719
2720        state.prepare_for_frame(FrameId(10), 0);
2721        state.record_transient_event(element, key);
2722
2723        // Kept for one additional frame to allow delivery on the next render.
2724        state.prepare_for_frame(FrameId(11), 0);
2725        assert!(state.take_transient_event(element, key));
2726
2727        // If we record and never consume, it should not survive beyond the next-next frame.
2728        state.record_transient_event(element, key);
2729        state.prepare_for_frame(FrameId(12), 0);
2730        assert!(state.take_transient_event(element, key));
2731
2732        state.record_transient_event(element, key);
2733        state.prepare_for_frame(FrameId(13), 0);
2734        state.prepare_for_frame(FrameId(14), 0);
2735        assert!(!state.take_transient_event(element, key));
2736    }
2737
2738    #[test]
2739    #[should_panic(expected = "ownership root overwrite detected")]
2740    fn strict_ownership_panics_on_root_overwrite() {
2741        let mut state = WindowElementState::default();
2742        state.set_strict_ownership(true);
2743
2744        let element = GlobalElementId(123);
2745        state.set_node_entry(
2746            element,
2747            NodeEntry {
2748                node: NodeId::default(),
2749                last_seen_frame: FrameId(1),
2750                root: GlobalElementId(1),
2751            },
2752        );
2753        state.set_node_entry(
2754            element,
2755            NodeEntry {
2756                node: NodeId::default(),
2757                last_seen_frame: FrameId(1),
2758                root: GlobalElementId(2),
2759            },
2760        );
2761    }
2762
2763    #[test]
2764    fn selectable_text_span_bounds_can_be_read_by_element() {
2765        let window = AppWindowId::default();
2766        let element = GlobalElementId(123);
2767        let node = NodeId::default();
2768        let expected = crate::element::SelectableTextInteractiveSpanBounds {
2769            range: 1..4,
2770            tag: std::sync::Arc::<str>::from("link"),
2771            bounds_local: Rect::new(
2772                Point::new(Px(10.0), Px(20.0)),
2773                Size::new(Px(30.0), Px(12.0)),
2774            ),
2775        };
2776
2777        let mut runtime = ElementRuntime::new();
2778        runtime.prepare_window_for_frame(window, FrameId(1));
2779        {
2780            let state = runtime.for_window_mut(window);
2781            state.set_node_entry(
2782                element,
2783                NodeEntry {
2784                    node,
2785                    last_seen_frame: FrameId(1),
2786                    root: GlobalElementId(1),
2787                },
2788            );
2789            state.with_state_mut(
2790                element,
2791                crate::element::SelectableTextState::default,
2792                |span_state| {
2793                    span_state.interactive_span_bounds = vec![expected.clone()];
2794                },
2795            );
2796        }
2797
2798        assert_eq!(
2799            runtime.selectable_text_interactive_span_bounds_for_node(window, node),
2800            Some(vec![expected.clone()])
2801        );
2802        assert_eq!(
2803            runtime.selectable_text_interactive_span_bounds_for_element(window, element),
2804            Some(vec![expected])
2805        );
2806    }
2807
2808    #[test]
2809    fn primary_pointer_type_is_unknown_until_committed() {
2810        let state = WindowElementState::default();
2811        assert_eq!(state.committed_primary_pointer_type(), PointerType::Unknown);
2812    }
2813
2814    #[test]
2815    fn primary_pointer_type_revision_increments_on_change() {
2816        let mut state = WindowElementState::default();
2817        state.prepare_for_frame(FrameId(1), 0);
2818
2819        assert!(state.environment_revisions.is_empty());
2820        state.record_committed_primary_pointer_type(PointerType::Mouse);
2821        assert!(
2822            state
2823                .environment_changed_this_frame
2824                .contains(&EnvironmentQueryKey::PrimaryPointerType)
2825        );
2826        let first_revision = state
2827            .environment_revisions
2828            .get(&EnvironmentQueryKey::PrimaryPointerType)
2829            .copied()
2830            .unwrap();
2831
2832        // Same value should not bump the revision.
2833        state.record_committed_primary_pointer_type(PointerType::Mouse);
2834        assert_eq!(
2835            state
2836                .environment_revisions
2837                .get(&EnvironmentQueryKey::PrimaryPointerType)
2838                .copied()
2839                .unwrap(),
2840            first_revision
2841        );
2842
2843        state.record_committed_primary_pointer_type(PointerType::Touch);
2844        assert_eq!(
2845            state
2846                .environment_revisions
2847                .get(&EnvironmentQueryKey::PrimaryPointerType)
2848                .copied()
2849                .unwrap(),
2850            first_revision + 1
2851        );
2852    }
2853
2854    #[test]
2855    fn color_scheme_revision_increments_on_change() {
2856        let mut state = WindowElementState::default();
2857        state.prepare_for_frame(FrameId(1), 0);
2858
2859        assert!(state.environment_revisions.is_empty());
2860        state.set_committed_color_scheme(Some(ColorScheme::Light));
2861        assert!(
2862            state
2863                .environment_changed_this_frame
2864                .contains(&EnvironmentQueryKey::ColorScheme)
2865        );
2866        let first_revision = state
2867            .environment_revisions
2868            .get(&EnvironmentQueryKey::ColorScheme)
2869            .copied()
2870            .unwrap();
2871
2872        // Same value should not bump the revision.
2873        state.set_committed_color_scheme(Some(ColorScheme::Light));
2874        assert_eq!(
2875            state
2876                .environment_revisions
2877                .get(&EnvironmentQueryKey::ColorScheme)
2878                .copied()
2879                .unwrap(),
2880            first_revision
2881        );
2882
2883        state.set_committed_color_scheme(Some(ColorScheme::Dark));
2884        assert_eq!(
2885            state
2886                .environment_revisions
2887                .get(&EnvironmentQueryKey::ColorScheme)
2888                .copied()
2889                .unwrap(),
2890            first_revision + 1
2891        );
2892    }
2893
2894    #[test]
2895    fn prefers_contrast_revision_increments_on_change() {
2896        let mut state = WindowElementState::default();
2897        state.prepare_for_frame(FrameId(1), 0);
2898
2899        assert!(state.environment_revisions.is_empty());
2900        state.set_committed_contrast_preference(Some(ContrastPreference::NoPreference));
2901        assert!(
2902            state
2903                .environment_changed_this_frame
2904                .contains(&EnvironmentQueryKey::PrefersContrast)
2905        );
2906        let first_revision = state
2907            .environment_revisions
2908            .get(&EnvironmentQueryKey::PrefersContrast)
2909            .copied()
2910            .unwrap();
2911
2912        // Same value should not bump the revision.
2913        state.set_committed_contrast_preference(Some(ContrastPreference::NoPreference));
2914        assert_eq!(
2915            state
2916                .environment_revisions
2917                .get(&EnvironmentQueryKey::PrefersContrast)
2918                .copied()
2919                .unwrap(),
2920            first_revision
2921        );
2922
2923        state.set_committed_contrast_preference(Some(ContrastPreference::More));
2924        assert_eq!(
2925            state
2926                .environment_revisions
2927                .get(&EnvironmentQueryKey::PrefersContrast)
2928                .copied()
2929                .unwrap(),
2930            first_revision + 1
2931        );
2932    }
2933
2934    #[test]
2935    fn forced_colors_mode_revision_increments_on_change() {
2936        let mut state = WindowElementState::default();
2937        state.prepare_for_frame(FrameId(1), 0);
2938
2939        assert!(state.environment_revisions.is_empty());
2940        state.set_committed_forced_colors_mode(Some(ForcedColorsMode::None));
2941        assert!(
2942            state
2943                .environment_changed_this_frame
2944                .contains(&EnvironmentQueryKey::ForcedColorsMode)
2945        );
2946        let first_revision = state
2947            .environment_revisions
2948            .get(&EnvironmentQueryKey::ForcedColorsMode)
2949            .copied()
2950            .unwrap();
2951
2952        // Same value should not bump the revision.
2953        state.set_committed_forced_colors_mode(Some(ForcedColorsMode::None));
2954        assert_eq!(
2955            state
2956                .environment_revisions
2957                .get(&EnvironmentQueryKey::ForcedColorsMode)
2958                .copied()
2959                .unwrap(),
2960            first_revision
2961        );
2962
2963        state.set_committed_forced_colors_mode(Some(ForcedColorsMode::Active));
2964        assert_eq!(
2965            state
2966                .environment_revisions
2967                .get(&EnvironmentQueryKey::ForcedColorsMode)
2968                .copied()
2969                .unwrap(),
2970            first_revision + 1
2971        );
2972    }
2973
2974    #[test]
2975    fn safe_area_insets_is_none_until_committed() {
2976        let state = WindowElementState::default();
2977        assert_eq!(state.committed_safe_area_insets(), None);
2978    }
2979
2980    #[test]
2981    fn safe_area_insets_revision_increments_on_change() {
2982        let mut state = WindowElementState::default();
2983        state.prepare_for_frame(FrameId(1), 0);
2984
2985        assert!(state.environment_revisions.is_empty());
2986        state.record_committed_safe_area_insets(Some(Edges::all(fret_core::Px(4.0))));
2987        assert!(
2988            state
2989                .environment_changed_this_frame
2990                .contains(&EnvironmentQueryKey::SafeAreaInsets)
2991        );
2992        let first_revision = state
2993            .environment_revisions
2994            .get(&EnvironmentQueryKey::SafeAreaInsets)
2995            .copied()
2996            .unwrap();
2997
2998        // Same value should not bump the revision.
2999        state.record_committed_safe_area_insets(Some(Edges::all(fret_core::Px(4.0))));
3000        assert_eq!(
3001            state
3002                .environment_revisions
3003                .get(&EnvironmentQueryKey::SafeAreaInsets)
3004                .copied()
3005                .unwrap(),
3006            first_revision
3007        );
3008
3009        state.record_committed_safe_area_insets(None);
3010        assert_eq!(
3011            state
3012                .environment_revisions
3013                .get(&EnvironmentQueryKey::SafeAreaInsets)
3014                .copied()
3015                .unwrap(),
3016            first_revision + 1
3017        );
3018    }
3019
3020    #[test]
3021    fn occlusion_insets_is_none_until_committed() {
3022        let state = WindowElementState::default();
3023        assert_eq!(state.committed_occlusion_insets(), None);
3024    }
3025
3026    #[test]
3027    fn occlusion_insets_revision_increments_on_change() {
3028        let mut state = WindowElementState::default();
3029        state.prepare_for_frame(FrameId(1), 0);
3030
3031        assert!(state.environment_revisions.is_empty());
3032        state.record_committed_occlusion_insets(Some(Edges::all(fret_core::Px(16.0))));
3033        assert!(
3034            state
3035                .environment_changed_this_frame
3036                .contains(&EnvironmentQueryKey::OcclusionInsets)
3037        );
3038        let first_revision = state
3039            .environment_revisions
3040            .get(&EnvironmentQueryKey::OcclusionInsets)
3041            .copied()
3042            .unwrap();
3043
3044        // Same value should not bump the revision.
3045        state.record_committed_occlusion_insets(Some(Edges::all(fret_core::Px(16.0))));
3046        assert_eq!(
3047            state
3048                .environment_revisions
3049                .get(&EnvironmentQueryKey::OcclusionInsets)
3050                .copied()
3051                .unwrap(),
3052            first_revision
3053        );
3054
3055        state.record_committed_occlusion_insets(None);
3056        assert_eq!(
3057            state
3058                .environment_revisions
3059                .get(&EnvironmentQueryKey::OcclusionInsets)
3060                .copied()
3061                .unwrap(),
3062            first_revision + 1
3063        );
3064    }
3065}