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