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