Skip to main content

fret_ui/tree/ui_tree_debug/
query.rs

1use super::super::*;
2
3impl<H: UiHost> UiTree<H> {
4    pub fn debug_cache_root_stats(&self) -> Vec<UiDebugCacheRootStats> {
5        if !self.debug_enabled {
6            return Vec::new();
7        }
8
9        let mut out: Vec<UiDebugCacheRootStats> = self
10            .debug_view_cache_roots
11            .iter()
12            .map(|r| UiDebugCacheRootStats {
13                root: r.root,
14                element: self.nodes.get(r.root).and_then(|n| n.element),
15                reused: r.reused,
16                contained_layout: r.contained_layout,
17                paint_replayed_ops: self
18                    .debug_paint_cache_replays
19                    .get(&r.root)
20                    .copied()
21                    .unwrap_or(0),
22                reuse_reason: r.reuse_reason,
23            })
24            .collect();
25
26        out.sort_by_key(|s| std::cmp::Reverse(s.paint_replayed_ops));
27        out
28    }
29
30    pub fn debug_view_cache_contained_relayout_roots(&self) -> &[NodeId] {
31        if !self.debug_enabled {
32            return &[];
33        }
34        &self.debug_view_cache_contained_relayout_roots
35    }
36
37    #[cfg(feature = "diagnostics")]
38    pub fn debug_set_children_write_for(&self, parent: NodeId) -> Option<UiDebugSetChildrenWrite> {
39        if !self.debug_enabled {
40            return None;
41        }
42        self.debug_set_children_writes.get(&parent).copied()
43    }
44
45    #[cfg(feature = "diagnostics")]
46    pub fn debug_parent_sever_write_for(&self, child: NodeId) -> Option<UiDebugParentSeverWrite> {
47        if !self.debug_enabled {
48            return None;
49        }
50        self.debug_parent_sever_writes.get(&child).copied()
51    }
52
53    #[cfg(feature = "diagnostics")]
54    pub fn debug_layer_visible_writes(&self) -> &[UiDebugSetLayerVisibleWrite] {
55        if !self.debug_enabled {
56            return &[];
57        }
58        self.debug_layer_visible_writes.as_slice()
59    }
60
61    #[cfg(feature = "diagnostics")]
62    pub fn debug_overlay_policy_decisions(&self) -> &[UiDebugOverlayPolicyDecisionWrite] {
63        if !self.debug_enabled {
64            return &[];
65        }
66        self.debug_overlay_policy_decisions.as_slice()
67    }
68
69    pub(crate) fn debug_record_notify_request(
70        &mut self,
71        frame_id: FrameId,
72        caller_node: NodeId,
73        location: Option<crate::widget::UiSourceLocation>,
74    ) {
75        #[cfg(feature = "diagnostics")]
76        {
77            if !self.debug_enabled {
78                return;
79            }
80
81            let Some(location) = location else {
82                return;
83            };
84
85            // Mirror the v1 notify routing: the default target is the nearest view-cache root,
86            // falling back to the caller node when no cache boundary exists.
87            let target = self
88                .nearest_view_cache_root(caller_node)
89                .unwrap_or(caller_node);
90
91            if self.debug_notify_requests.len() >= 256 {
92                return;
93            }
94
95            self.debug_notify_requests.push(UiDebugNotifyRequest {
96                frame_id,
97                caller_node,
98                target_view: ViewId(target),
99                file: location.file,
100                line: location.line,
101                column: location.column,
102            });
103        }
104
105        #[cfg(not(feature = "diagnostics"))]
106        {
107            let _ = (frame_id, caller_node, location);
108        }
109    }
110
111    #[track_caller]
112    #[allow(clippy::too_many_arguments)]
113    pub fn debug_record_overlay_policy_decision(
114        &mut self,
115        frame_id: FrameId,
116        layer: UiLayerId,
117        kind: &'static str,
118        present: bool,
119        interactive: bool,
120        wants_timer_events: bool,
121        reason: &'static str,
122    ) {
123        #[cfg(feature = "diagnostics")]
124        {
125            if !self.debug_enabled {
126                return;
127            }
128            let caller = std::panic::Location::caller();
129            self.debug_overlay_policy_decisions
130                .push(UiDebugOverlayPolicyDecisionWrite {
131                    layer,
132                    frame_id,
133                    kind,
134                    present,
135                    interactive,
136                    wants_timer_events,
137                    reason,
138                    file: caller.file(),
139                    line: caller.line(),
140                    column: caller.column(),
141                });
142        }
143
144        #[cfg(not(feature = "diagnostics"))]
145        {
146            let _ = (
147                frame_id,
148                layer,
149                kind,
150                present,
151                interactive,
152                wants_timer_events,
153                reason,
154            );
155        }
156    }
157
158    #[cfg(feature = "diagnostics")]
159    pub(crate) fn debug_set_remove_subtree_frame_context(
160        &mut self,
161        root: NodeId,
162        ctx: UiDebugRemoveSubtreeFrameContext,
163    ) {
164        if !self.debug_enabled {
165            return;
166        }
167        self.debug_remove_subtree_frame_context.insert(root, ctx);
168    }
169
170    #[cfg(feature = "diagnostics")]
171    pub fn debug_removed_subtrees(&self) -> &[UiDebugRemoveSubtreeRecord] {
172        if !self.debug_enabled {
173            return &[];
174        }
175        self.debug_removed_subtrees.as_slice()
176    }
177
178    pub fn debug_layout_engine_solves(&self) -> &[UiDebugLayoutEngineSolve] {
179        if !self.debug_enabled {
180            return &[];
181        }
182        self.debug_layout_engine_solves.as_slice()
183    }
184
185    pub fn debug_layout_hotspots(&self) -> &[UiDebugLayoutHotspot] {
186        if !self.debug_enabled {
187            return &[];
188        }
189        self.debug_layout_hotspots.as_slice()
190    }
191
192    pub fn debug_layout_inclusive_hotspots(&self) -> &[UiDebugLayoutHotspot] {
193        if !self.debug_enabled {
194            return &[];
195        }
196        self.debug_layout_inclusive_hotspots.as_slice()
197    }
198
199    pub fn debug_widget_measure_hotspots(&self) -> &[UiDebugWidgetMeasureHotspot] {
200        if !self.debug_enabled {
201            return &[];
202        }
203        self.debug_widget_measure_hotspots.as_slice()
204    }
205
206    pub fn debug_paint_widget_hotspots(&self) -> &[UiDebugPaintWidgetHotspot] {
207        if !self.debug_enabled {
208            return &[];
209        }
210        self.debug_paint_widget_hotspots.as_slice()
211    }
212
213    pub fn debug_paint_text_prepare_hotspots(&self) -> &[UiDebugPaintTextPrepareHotspot] {
214        if !self.debug_enabled {
215            return &[];
216        }
217        self.debug_paint_text_prepare_hotspots.as_slice()
218    }
219
220    #[cfg(feature = "diagnostics")]
221    pub(in crate::tree) fn debug_sample_child_elements_head(
222        &self,
223        children: &[NodeId],
224    ) -> [Option<GlobalElementId>; 4] {
225        let mut out: [Option<GlobalElementId>; 4] = [None; 4];
226        for (i, &child) in children.iter().take(out.len()).enumerate() {
227            out[i] = self.nodes.get(child).and_then(|n| n.element);
228        }
229        out
230    }
231
232    pub fn set_debug_enabled(&mut self, enabled: bool) {
233        self.debug_enabled = enabled;
234    }
235
236    pub(crate) fn debug_enabled(&self) -> bool {
237        self.debug_enabled
238    }
239
240    pub fn debug_stats(&self) -> UiDebugFrameStats {
241        self.debug_stats
242    }
243
244    pub(crate) fn debug_set_element_children_vec_pool_stats(&mut self, reuses: u32, misses: u32) {
245        if !self.debug_enabled {
246            return;
247        }
248        self.debug_stats.element_children_vec_pool_reuses = reuses;
249        self.debug_stats.element_children_vec_pool_misses = misses;
250    }
251
252    #[cfg(test)]
253    pub(crate) fn debug_measure_child_calls_for_parent(&self, parent: NodeId) -> u64 {
254        self.debug_measure_children
255            .get(&parent)
256            .map(|m| m.values().map(|r| r.calls).sum())
257            .unwrap_or(0)
258    }
259
260    pub fn debug_hover_declarative_invalidation_hotspots(
261        &self,
262        max: usize,
263    ) -> Vec<UiDebugHoverDeclarativeInvalidationHotspot> {
264        if !self.debug_enabled || max == 0 {
265            return Vec::new();
266        }
267
268        let mut out: Vec<UiDebugHoverDeclarativeInvalidationHotspot> = self
269            .debug_hover_declarative_invalidations
270            .iter()
271            .map(
272                |(&node, counts)| UiDebugHoverDeclarativeInvalidationHotspot {
273                    node,
274                    element: self.nodes.get(node).and_then(|n| n.element),
275                    hit_test: counts.hit_test,
276                    layout: counts.layout,
277                    paint: counts.paint,
278                },
279            )
280            .collect();
281
282        out.sort_by_key(|hs| {
283            (
284                std::cmp::Reverse(hs.layout),
285                std::cmp::Reverse(hs.hit_test),
286                std::cmp::Reverse(hs.paint),
287            )
288        });
289        out.truncate(max);
290        out
291    }
292
293    pub fn debug_invalidation_walks(&self) -> &[UiDebugInvalidationWalk] {
294        if !self.debug_enabled {
295            return &[];
296        }
297        self.debug_invalidation_walks.as_slice()
298    }
299
300    pub fn debug_dirty_views(&self) -> &[UiDebugDirtyView] {
301        if !self.debug_enabled {
302            return &[];
303        }
304        self.debug_dirty_views.as_slice()
305    }
306
307    pub fn debug_notify_requests(&self) -> &[UiDebugNotifyRequest] {
308        #[cfg(feature = "diagnostics")]
309        {
310            if !self.debug_enabled {
311                &[]
312            } else {
313                self.debug_notify_requests.as_slice()
314            }
315        }
316
317        #[cfg(not(feature = "diagnostics"))]
318        {
319            &[]
320        }
321    }
322
323    pub fn debug_virtual_list_windows(&self) -> &[UiDebugVirtualListWindow] {
324        if !self.debug_enabled {
325            return &[];
326        }
327        self.debug_virtual_list_windows.as_slice()
328    }
329
330    pub fn debug_virtual_list_window_shift_samples(
331        &self,
332    ) -> &[UiDebugVirtualListWindowShiftSample] {
333        if !self.debug_enabled {
334            return &[];
335        }
336        self.debug_virtual_list_window_shift_samples.as_slice()
337    }
338
339    pub fn debug_retained_virtual_list_reconciles(&self) -> &[UiDebugRetainedVirtualListReconcile] {
340        if !self.debug_enabled {
341            return &[];
342        }
343        self.debug_retained_virtual_list_reconciles.as_slice()
344    }
345
346    pub fn debug_scroll_handle_changes(&self) -> &[UiDebugScrollHandleChange] {
347        if !self.debug_enabled {
348            return &[];
349        }
350        self.debug_scroll_handle_changes.as_slice()
351    }
352
353    pub fn debug_scroll_nodes(&self) -> &[UiDebugScrollNodeTelemetry] {
354        if !self.debug_enabled {
355            return &[];
356        }
357        self.debug_scroll_nodes.as_slice()
358    }
359
360    pub fn debug_scrollbars(&self) -> &[UiDebugScrollbarTelemetry] {
361        if !self.debug_enabled {
362            return &[];
363        }
364        self.debug_scrollbars.as_slice()
365    }
366
367    pub fn debug_prepaint_actions(&self) -> &[UiDebugPrepaintAction] {
368        if !self.debug_enabled {
369            return &[];
370        }
371        self.debug_prepaint_actions.as_slice()
372    }
373
374    pub fn debug_model_change_hotspots(&self) -> &[UiDebugModelChangeHotspot] {
375        if !self.debug_enabled {
376            return &[];
377        }
378        self.debug_model_change_hotspots.as_slice()
379    }
380
381    pub fn debug_model_change_unobserved(&self) -> &[UiDebugModelChangeUnobserved] {
382        if !self.debug_enabled {
383            return &[];
384        }
385        self.debug_model_change_unobserved.as_slice()
386    }
387
388    pub fn debug_global_change_hotspots(&self) -> &[UiDebugGlobalChangeHotspot] {
389        if !self.debug_enabled {
390            return &[];
391        }
392        self.debug_global_change_hotspots.as_slice()
393    }
394
395    pub fn debug_global_change_unobserved(&self) -> &[UiDebugGlobalChangeUnobserved] {
396        if !self.debug_enabled {
397            return &[];
398        }
399        self.debug_global_change_unobserved.as_slice()
400    }
401
402    pub fn debug_node_bounds(&self, node: NodeId) -> Option<Rect> {
403        self.nodes.get(node).map(|n| n.bounds)
404    }
405
406    pub fn debug_node_element(&self, node: NodeId) -> Option<GlobalElementId> {
407        self.nodes.get(node).and_then(|n| n.element)
408    }
409
410    pub fn debug_node_clips_hit_test(&self, node: NodeId) -> Option<bool> {
411        let n = self.nodes.get(node)?;
412        let widget = n.widget.as_ref();
413        let prepaint = (!self.inspection_active && !n.invalidation.hit_test)
414            .then_some(n.prepaint_hit_test)
415            .flatten();
416        Some(
417            prepaint
418                .as_ref()
419                .map(|p| p.clips_hit_test)
420                .unwrap_or_else(|| widget.map(|w| w.clips_hit_test(n.bounds)).unwrap_or(true)),
421        )
422    }
423
424    pub fn debug_node_can_scroll_descendant_into_view(&self, node: NodeId) -> Option<bool> {
425        let n = self.nodes.get(node)?;
426        let widget = n.widget.as_ref();
427        let prepaint = (!self.inspection_active && !n.invalidation.hit_test)
428            .then_some(n.prepaint_hit_test)
429            .flatten();
430        Some(
431            prepaint
432                .as_ref()
433                .map(|p| p.can_scroll_descendant_into_view)
434                .unwrap_or_else(|| {
435                    widget
436                        .map(|w| w.can_scroll_descendant_into_view())
437                        .unwrap_or(false)
438                }),
439        )
440    }
441
442    pub fn debug_node_render_transform(&self, node: NodeId) -> Option<Transform2D> {
443        let n = self.nodes.get(node)?;
444        let widget = n.widget.as_ref();
445        let prepaint = (!self.inspection_active && !n.invalidation.hit_test)
446            .then_some(n.prepaint_hit_test)
447            .flatten();
448        if let Some(inv) = prepaint.as_ref().and_then(|p| p.render_transform_inv) {
449            return inv.inverse();
450        }
451        widget.and_then(|w| w.render_transform(n.bounds))
452    }
453
454    pub fn debug_node_children_render_transform(&self, node: NodeId) -> Option<Transform2D> {
455        let n = self.nodes.get(node)?;
456        let widget = n.widget.as_ref();
457        let prepaint = (!self.inspection_active && !n.invalidation.hit_test)
458            .then_some(n.prepaint_hit_test)
459            .flatten();
460        if let Some(inv) = prepaint
461            .as_ref()
462            .and_then(|p| p.children_render_transform_inv)
463        {
464            return inv.inverse();
465        }
466        widget.and_then(|w| w.children_render_transform(n.bounds))
467    }
468
469    pub fn debug_text_constraints_snapshot(&self, node: NodeId) -> UiDebugTextConstraintsSnapshot {
470        #[cfg(feature = "diagnostics")]
471        {
472            return UiDebugTextConstraintsSnapshot {
473                measured: self.debug_text_constraints_measured.get(&node).copied(),
474                prepared: self.debug_text_constraints_prepared.get(&node).copied(),
475            };
476        }
477        #[cfg(not(feature = "diagnostics"))]
478        {
479            let _ = node;
480        }
481
482        #[allow(unreachable_code)]
483        UiDebugTextConstraintsSnapshot::default()
484    }
485
486    /// Returns the node bounds after applying the accumulated `render_transform` stack.
487    ///
488    /// This is intended for debugging and tests that need screen-space geometry for overlay
489    /// placement/hit-testing scenarios. Unlike `debug_node_bounds`, this includes render-time
490    /// transforms such as `Anchored` placement.
491    ///
492    /// This is not a stable cross-frame geometry query (see
493    /// `fret_ui::elements::visual_bounds_for_element` for that contract).
494    pub fn debug_node_visual_bounds(&self, node: NodeId) -> Option<Rect> {
495        let bounds = self.nodes.get(node).map(|n| n.bounds)?;
496        let path = self.debug_node_path(node);
497        let mut before = Transform2D::IDENTITY;
498        let mut transform = Transform2D::IDENTITY;
499        for (idx, id) in path.iter().copied().enumerate() {
500            let node_transform = self
501                .debug_node_render_transform(id)
502                .unwrap_or(Transform2D::IDENTITY);
503            let at_node = before.compose(node_transform);
504            if id == node {
505                transform = at_node;
506                break;
507            }
508            let child_transform = self
509                .debug_node_children_render_transform(id)
510                .unwrap_or(Transform2D::IDENTITY);
511            before = at_node.compose(child_transform);
512
513            // Defensive: if the node wasn't found in `path`, keep identity.
514            if idx == path.len().saturating_sub(1) {
515                transform = at_node;
516            }
517        }
518
519        Some(rect_aabb_transformed(bounds, transform))
520    }
521
522    pub fn debug_node_path(&self, node: NodeId) -> Vec<NodeId> {
523        let mut out: Vec<NodeId> = Vec::new();
524        let mut current = Some(node);
525        while let Some(id) = current {
526            out.push(id);
527            current = self.nodes.get(id).and_then(|n| n.parent);
528        }
529        out.reverse();
530        out
531    }
532
533    pub fn debug_layers_in_paint_order(&self) -> Vec<UiDebugLayerInfo> {
534        self.layer_order
535            .iter()
536            .copied()
537            .filter_map(|id| {
538                let layer = self.layers.get(id)?;
539                Some(UiDebugLayerInfo {
540                    id,
541                    root: layer.root,
542                    visible: layer.visible,
543                    blocks_underlay_input: layer.blocks_underlay_input,
544                    hit_testable: layer.hit_testable,
545                    pointer_occlusion: layer.pointer_occlusion,
546                    wants_pointer_down_outside_events: layer.wants_pointer_down_outside_events,
547                    consume_pointer_down_outside_events: layer.consume_pointer_down_outside_events,
548                    pointer_down_outside_branches: layer.pointer_down_outside_branches.clone(),
549                    wants_pointer_move_events: layer.wants_pointer_move_events,
550                    wants_timer_events: layer.wants_timer_events,
551                })
552            })
553            .collect()
554    }
555
556    pub fn debug_hit_test(&self, position: Point) -> UiDebugHitTest {
557        let (active_roots, barrier_root) = self.active_input_layers();
558        let hit = self.hit_test_layers(&active_roots, position);
559        UiDebugHitTest {
560            hit,
561            active_layer_roots: active_roots,
562            barrier_root,
563        }
564    }
565
566    /// Hit-test using the same cached fast paths used by pointer routing.
567    ///
568    /// This is intended for diagnostics tooling that needs “what the runtime would route right
569    /// now”, including bounds-tree acceleration and view-cache/prepaint interaction data.
570    ///
571    /// Note: this method mutates internal hit-test caches.
572    pub fn debug_hit_test_routing(&mut self, position: Point) -> UiDebugHitTest {
573        let (active_roots, barrier_root) = self.active_input_layers();
574
575        // Avoid leaking a stale cached path into diagnostics queries. Pointer routing already
576        // manages when the cache is eligible for reuse (e.g. move vs click).
577        self.hit_test_path_cache = None;
578
579        let hit = self.hit_test_layers_cached(&active_roots, position);
580        UiDebugHitTest {
581            hit,
582            active_layer_roots: active_roots,
583            barrier_root,
584        }
585    }
586
587    #[cfg(feature = "diagnostics")]
588    pub fn debug_dispatch_snapshot(&mut self, frame_id: FrameId) -> UiDebugDispatchSnapshot {
589        if self
590            .debug_dispatch_snapshot
591            .as_ref()
592            .is_some_and(|s| s.frame_id == frame_id)
593        {
594            let snapshot = self
595                .debug_dispatch_snapshot
596                .as_ref()
597                .expect("snapshot presence checked");
598            return UiDebugDispatchSnapshot::from_snapshot(snapshot);
599        }
600
601        let snapshot = self.build_dispatch_snapshot(frame_id);
602        let debug = UiDebugDispatchSnapshot::from_snapshot(&snapshot);
603        self.debug_dispatch_snapshot = Some(snapshot);
604        debug
605    }
606
607    #[cfg(feature = "diagnostics")]
608    pub fn debug_dispatch_snapshot_parity(
609        &mut self,
610        frame_id: FrameId,
611    ) -> UiDebugDispatchSnapshotParityReport {
612        // Reuse the snapshot cache if possible, but keep this method self-contained so diagnostics
613        // callers don't need to remember ordering.
614        let snapshot = if self
615            .debug_dispatch_snapshot
616            .as_ref()
617            .is_some_and(|s| s.frame_id == frame_id)
618        {
619            self.debug_dispatch_snapshot
620                .as_ref()
621                .expect("snapshot presence checked")
622                .clone()
623        } else {
624            let snapshot = self.build_dispatch_snapshot(frame_id);
625            self.debug_dispatch_snapshot = Some(snapshot.clone());
626            snapshot
627        };
628
629        // Phase A baseline: reachability from active layer roots via child edges.
630        let mut reachable: HashSet<NodeId> = HashSet::new();
631        let mut stack: Vec<NodeId> = snapshot.active_layer_roots.clone();
632        while let Some(id) = stack.pop() {
633            if !self.nodes.contains_key(id) {
634                continue;
635            }
636            if !reachable.insert(id) {
637                continue;
638            }
639            if let Some(entry) = self.nodes.get(id) {
640                for &child in &entry.children {
641                    stack.push(child);
642                }
643            }
644        }
645
646        let snapshot_nodes: HashSet<NodeId> = snapshot.nodes.iter().copied().collect();
647
648        const SAMPLE_LIMIT: usize = 64;
649
650        let mut missing_in_snapshot_sample: Vec<NodeId> = Vec::new();
651        let mut missing_in_snapshot_total = 0usize;
652        for id in reachable.iter().copied() {
653            if snapshot_nodes.contains(&id) {
654                continue;
655            }
656            missing_in_snapshot_total = missing_in_snapshot_total.saturating_add(1);
657            if missing_in_snapshot_sample.len() < SAMPLE_LIMIT {
658                missing_in_snapshot_sample.push(id);
659            }
660        }
661
662        let mut extra_in_snapshot_sample: Vec<NodeId> = Vec::new();
663        let mut extra_in_snapshot_total = 0usize;
664        for id in snapshot_nodes.iter().copied() {
665            if reachable.contains(&id) {
666                continue;
667            }
668            extra_in_snapshot_total = extra_in_snapshot_total.saturating_add(1);
669            if extra_in_snapshot_sample.len() < SAMPLE_LIMIT {
670                extra_in_snapshot_sample.push(id);
671            }
672        }
673
674        UiDebugDispatchSnapshotParityReport {
675            frame_id,
676            window: snapshot.window,
677            active_layer_roots: snapshot.active_layer_roots,
678            barrier_root: snapshot.barrier_root,
679            reachable_count: reachable.len(),
680            snapshot_count: snapshot_nodes.len(),
681            missing_in_snapshot_total,
682            missing_in_snapshot_sample,
683            extra_in_snapshot_total,
684            extra_in_snapshot_sample,
685        }
686    }
687}