Skip to main content

cranpose_app_shell/
shell_debug.rs

1use super::*;
2
3#[cfg(any(test, feature = "test-support"))]
4type SubcomposeScopeIds = Vec<(NodeId, Vec<(u64, Vec<usize>)>)>;
5#[cfg(any(test, feature = "test-support"))]
6type SlotGroupEntry = (usize, Key, Option<usize>, usize);
7
8impl<R> AppShell<R>
9where
10    R: Renderer,
11    R::Error: Debug,
12{
13    /// Returns a text report of the first surface: its layout tree, its headless render scene and a
14    /// summary.
15    pub fn debug_info_report(&mut self) -> String {
16        debug_info_report(&mut self.app, &mut self.surfaces[0])
17    }
18
19    /// Logs the report from `debug_info_report` at info level under `cranpose::debug::screen` and
20    /// returns it.
21    pub fn log_debug_info(&mut self) -> String {
22        let report = self.debug_info_report();
23        log::info!(target: "cranpose::debug::screen", "\n{report}");
24        report
25    }
26
27    /// Get the current layout tree (for robot/testing)
28    pub fn layout_tree(&mut self) -> Option<&LayoutTree> {
29        let app_context = std::rc::Rc::clone(&self.app.app_context);
30        app_context.enter(|| self.surfaces[0].layout_tree_in_context(&mut self.app))
31    }
32
33    #[doc(hidden)]
34    pub fn with_layout_tree<T>(&mut self, block: impl FnOnce(Option<&LayoutTree>) -> T) -> T {
35        let app_context = std::rc::Rc::clone(&self.app.app_context);
36        app_context.enter(|| {
37            let layout_tree = self.surfaces[0].layout_tree_in_context(&mut self.app);
38            block(layout_tree)
39        })
40    }
41
42    /// Whether a semantics snapshot could contain anything at all. Reading the
43    /// flag costs nothing, unlike collecting the layout-bounds map only to find
44    /// semantics tracking disabled.
45    pub fn semantics_active(&self) -> bool {
46        self.app.semantics_enabled
47    }
48
49    /// Monotonic revision of the accessibility-relevant state. It moves when a
50    /// layout pass invalidated the cached snapshots, when semantics tracking is
51    /// toggled, and when any node marked its semantics dirty since the last
52    /// look. A bridge that stored the revision it last projected can skip the
53    /// whole snapshot-and-compare while this still reads the same — which on an
54    /// animation-only frame is every frame.
55    pub fn semantics_snapshot_revision(&mut self) -> u64 {
56        if self.app.semantics_enabled
57            || self
58                .surfaces
59                .iter()
60                .any(|surface| surface.inspector.state.open)
61        {
62            let app_context = std::rc::Rc::clone(&self.app.app_context);
63            let semantics_dirty = app_context.enter(|| {
64                self.app.flush_semantics_invalidations();
65                let Some(root) = self.app.composition.root() else {
66                    return false;
67                };
68                let mut applier = self.app.composition.applier_mut();
69                cranpose_ui::tree_needs_semantics(&mut *applier, root).unwrap_or(true)
70            });
71            if semantics_dirty {
72                self.app.semantics_snapshot_revision =
73                    self.app.semantics_snapshot_revision.wrapping_add(1);
74            }
75        }
76        self.app.semantics_snapshot_revision
77    }
78
79    /// Get the current semantics tree (for robot/testing)
80    pub fn semantics_tree(&mut self) -> Option<&SemanticsTree> {
81        let app_context = std::rc::Rc::clone(&self.app.app_context);
82        app_context.enter(|| self.surfaces[0].semantics_tree_in_context(&mut self.app))
83    }
84
85    /// Returns the width and height of the first surface's root layout box in logical pixels, or
86    /// `None` before a layout exists.
87    pub fn root_layout_size(&mut self) -> Option<(f32, f32)> {
88        self.layout_tree().map(|tree| {
89            let root = tree.root();
90            (root.rect.width, root.rect.height)
91        })
92    }
93
94    /// Returns `(x, y, width, height)` of the layout box of `target` in logical pixels, or `None`
95    /// when the node is not in the layout tree.
96    pub fn node_layout_bounds(&mut self, target: NodeId) -> Option<(f32, f32, f32, f32)> {
97        self.layout_tree()
98            .and_then(|tree| find_layout_box(tree.root(), target))
99            .map(layout_box_bounds)
100    }
101
102    #[cfg(any(test, feature = "test-support"))]
103    #[doc(hidden)]
104    pub fn debug_runtime_leak_stats(&mut self) -> RuntimeLeakDebugStats {
105        let runtime = self.app.composition.runtime_handle();
106        let (applier_stats, live_node_heap_bytes, recycled_node_heap_bytes) = {
107            let applier = self.app.composition.applier_mut();
108            (
109                applier.debug_stats(),
110                applier.debug_live_node_heap_bytes(),
111                applier.debug_recycled_node_heap_bytes(),
112            )
113        };
114        RuntimeLeakDebugStats {
115            applier_stats,
116            live_node_heap_bytes,
117            recycled_node_heap_bytes,
118            slot_table_heap_bytes: self.app.composition.slot_table_heap_bytes(),
119            pass_stats: self.app.composition.debug_last_pass_stats(),
120            slot_stats: self.app.composition.debug_slot_table_stats(),
121            runtime_stats: runtime.debug_stats(),
122            state_arena_stats: runtime.state_arena_debug_stats(),
123            recompose_scope_stats: debug_recompose_scope_registry_stats(),
124            snapshot_v2_stats: debug_snapshot_v2_stats(),
125            snapshot_pinning_stats: debug_snapshot_pinning_stats(),
126        }
127    }
128
129    #[cfg(any(test, feature = "test-support"))]
130    #[doc(hidden)]
131    pub fn debug_slot_table_groups(&self) -> Vec<(usize, Key, Option<usize>, usize)> {
132        self.app.composition.debug_dump_slot_table_groups()
133    }
134
135    #[cfg(any(test, feature = "test-support"))]
136    #[doc(hidden)]
137    pub fn debug_slot_entries(&self) -> Vec<cranpose_core::SlotDebugEntry> {
138        self.app.composition.debug_dump_slot_entries()
139    }
140
141    #[cfg(any(test, feature = "test-support"))]
142    #[doc(hidden)]
143    pub fn runtime_handle(&self) -> cranpose_core::RuntimeHandle {
144        self.app.composition.runtime_handle()
145    }
146
147    #[cfg(any(test, feature = "test-support"))]
148    #[doc(hidden)]
149    pub fn debug_live_subcompose_scope_ids(&mut self) -> SubcomposeScopeIds {
150        fn collect_node_ids(layout: &LayoutBox, out: &mut Vec<NodeId>) {
151            out.push(layout.node_id);
152            for child in &layout.children {
153                collect_node_ids(child, out);
154            }
155        }
156
157        let mut node_ids = Vec::new();
158        if let Some(tree) = self.layout_tree() {
159            collect_node_ids(tree.root(), &mut node_ids);
160        }
161
162        let mut applier = self.app.composition.applier_mut();
163        let mut result = Vec::new();
164        for node_id in node_ids {
165            if let Ok(scope_ids) = applier.with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
166                node.debug_scope_ids_by_slot()
167            }) {
168                result.push((node_id, scope_ids));
169            }
170        }
171        result
172    }
173
174    #[cfg(any(test, feature = "test-support"))]
175    #[doc(hidden)]
176    pub fn debug_subcompose_slot_table(
177        &mut self,
178        node_id: NodeId,
179        slot_id: u64,
180    ) -> Option<Vec<cranpose_core::SlotDebugEntry>> {
181        let mut applier = self.app.composition.applier_mut();
182        applier
183            .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
184                node.debug_slot_table_for_slot(SlotId::new(slot_id))
185            })
186            .ok()
187            .flatten()
188    }
189
190    #[cfg(any(test, feature = "test-support"))]
191    #[doc(hidden)]
192    pub fn debug_subcompose_slot_groups(
193        &mut self,
194        node_id: NodeId,
195        slot_id: u64,
196    ) -> Option<Vec<SlotGroupEntry>> {
197        let mut applier = self.app.composition.applier_mut();
198        applier
199            .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
200                node.debug_slot_table_groups_for_slot(SlotId::new(slot_id))
201            })
202            .ok()
203            .flatten()
204    }
205}
206
207fn find_layout_box(layout_box: &LayoutBox, target: NodeId) -> Option<&LayoutBox> {
208    if layout_box.node_id == target {
209        return Some(layout_box);
210    }
211
212    layout_box
213        .children
214        .iter()
215        .find_map(|child| find_layout_box(child, target))
216}
217
218fn layout_box_bounds(layout_box: &LayoutBox) -> (f32, f32, f32, f32) {
219    (
220        layout_box.rect.x,
221        layout_box.rect.y,
222        layout_box.rect.width,
223        layout_box.rect.height,
224    )
225}
226
227fn debug_info_report<R: Renderer>(app: &mut ShellApp, surface: &mut RootSurface<R>) -> String {
228    let app_context = std::rc::Rc::clone(&app.app_context);
229    app_context.enter(|| {
230        let mut report = String::new();
231        writeln!(report, "=== DEBUG: CURRENT SCREEN STATE ===").ok();
232        if let Some(layout_tree) = surface.layout_tree_in_context(app) {
233            let renderer = HeadlessRenderer::new();
234            let render_scene = renderer.render(layout_tree);
235            writeln!(report, "{}", format_layout_tree(layout_tree)).ok();
236            writeln!(report, "{}", format_render_scene(&render_scene)).ok();
237            writeln!(
238                report,
239                "{}",
240                format_screen_summary(layout_tree, &render_scene)
241            )
242            .ok();
243        } else {
244            writeln!(report, "No layout available").ok();
245        }
246        report
247    })
248}
249
250impl<R> SurfaceMut<'_, R>
251where
252    R: Renderer,
253    R::Error: Debug,
254{
255    /// The layout tree and headless scene of this surface, for a log.
256    pub fn debug_info_report(&mut self) -> String {
257        let (app, surface) = self.parts();
258        debug_info_report(app, surface)
259    }
260
261    /// Logs [`Self::debug_info_report`] and answers it.
262    pub fn log_debug_info(&mut self) -> String {
263        let report = self.debug_info_report();
264        log::info!(target: "cranpose::debug::screen", "\n{report}");
265        report
266    }
267}