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                let Some(root) = self.app.composition.root() else {
61                    return false;
62                };
63                let mut applier = self.app.composition.applier_mut();
64                cranpose_ui::tree_needs_semantics(&mut *applier, root).unwrap_or(true)
65            });
66            if semantics_dirty {
67                self.app.semantics_snapshot_revision =
68                    self.app.semantics_snapshot_revision.wrapping_add(1);
69            }
70        }
71        self.app.semantics_snapshot_revision
72    }
73
74    /// Get the current semantics tree (for robot/testing)
75    pub fn semantics_tree(&mut self) -> Option<&SemanticsTree> {
76        let app_context = std::rc::Rc::clone(&self.app.app_context);
77        app_context.enter(|| self.surfaces[0].semantics_tree_in_context(&mut self.app))
78    }
79
80    pub fn root_layout_size(&mut self) -> Option<(f32, f32)> {
81        self.layout_tree().map(|tree| {
82            let root = tree.root();
83            (root.rect.width, root.rect.height)
84        })
85    }
86
87    pub fn node_layout_bounds(&mut self, target: NodeId) -> Option<(f32, f32, f32, f32)> {
88        self.layout_tree()
89            .and_then(|tree| find_layout_box(tree.root(), target))
90            .map(layout_box_bounds)
91    }
92
93    #[cfg(any(test, feature = "test-support"))]
94    #[doc(hidden)]
95    pub fn debug_runtime_leak_stats(&mut self) -> RuntimeLeakDebugStats {
96        let runtime = self.app.composition.runtime_handle();
97        let (applier_stats, live_node_heap_bytes, recycled_node_heap_bytes) = {
98            let applier = self.app.composition.applier_mut();
99            (
100                applier.debug_stats(),
101                applier.debug_live_node_heap_bytes(),
102                applier.debug_recycled_node_heap_bytes(),
103            )
104        };
105        RuntimeLeakDebugStats {
106            applier_stats,
107            live_node_heap_bytes,
108            recycled_node_heap_bytes,
109            slot_table_heap_bytes: self.app.composition.slot_table_heap_bytes(),
110            pass_stats: self.app.composition.debug_last_pass_stats(),
111            slot_stats: self.app.composition.debug_slot_table_stats(),
112            observer_stats: self.app.composition.debug_observer_stats(),
113            runtime_stats: runtime.debug_stats(),
114            state_arena_stats: runtime.state_arena_debug_stats(),
115            recompose_scope_stats: debug_recompose_scope_registry_stats(),
116            snapshot_v2_stats: debug_snapshot_v2_stats(),
117            snapshot_pinning_stats: debug_snapshot_pinning_stats(),
118        }
119    }
120
121    #[cfg(any(test, feature = "test-support"))]
122    #[doc(hidden)]
123    pub fn debug_slot_table_groups(&self) -> Vec<(usize, Key, Option<usize>, usize)> {
124        self.app.composition.debug_dump_slot_table_groups()
125    }
126
127    #[cfg(any(test, feature = "test-support"))]
128    #[doc(hidden)]
129    pub fn debug_slot_entries(&self) -> Vec<cranpose_core::SlotDebugEntry> {
130        self.app.composition.debug_dump_slot_entries()
131    }
132
133    #[cfg(any(test, feature = "test-support"))]
134    #[doc(hidden)]
135    pub fn runtime_handle(&self) -> cranpose_core::RuntimeHandle {
136        self.app.composition.runtime_handle()
137    }
138
139    #[cfg(any(test, feature = "test-support"))]
140    #[doc(hidden)]
141    pub fn debug_live_subcompose_scope_ids(&mut self) -> SubcomposeScopeIds {
142        fn collect_node_ids(layout: &LayoutBox, out: &mut Vec<NodeId>) {
143            out.push(layout.node_id);
144            for child in &layout.children {
145                collect_node_ids(child, out);
146            }
147        }
148
149        let mut node_ids = Vec::new();
150        if let Some(tree) = self.layout_tree() {
151            collect_node_ids(tree.root(), &mut node_ids);
152        }
153
154        let mut applier = self.app.composition.applier_mut();
155        let mut result = Vec::new();
156        for node_id in node_ids {
157            if let Ok(scope_ids) = applier.with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
158                node.debug_scope_ids_by_slot()
159            }) {
160                result.push((node_id, scope_ids));
161            }
162        }
163        result
164    }
165
166    #[cfg(any(test, feature = "test-support"))]
167    #[doc(hidden)]
168    pub fn debug_subcompose_slot_table(
169        &mut self,
170        node_id: NodeId,
171        slot_id: u64,
172    ) -> Option<Vec<cranpose_core::SlotDebugEntry>> {
173        let mut applier = self.app.composition.applier_mut();
174        applier
175            .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
176                node.debug_slot_table_for_slot(SlotId::new(slot_id))
177            })
178            .ok()
179            .flatten()
180    }
181
182    #[cfg(any(test, feature = "test-support"))]
183    #[doc(hidden)]
184    pub fn debug_subcompose_slot_groups(
185        &mut self,
186        node_id: NodeId,
187        slot_id: u64,
188    ) -> Option<Vec<SlotGroupEntry>> {
189        let mut applier = self.app.composition.applier_mut();
190        applier
191            .with_node::<SubcomposeLayoutNode, _>(node_id, |node| {
192                node.debug_slot_table_groups_for_slot(SlotId::new(slot_id))
193            })
194            .ok()
195            .flatten()
196    }
197}
198
199fn find_layout_box(layout_box: &LayoutBox, target: NodeId) -> Option<&LayoutBox> {
200    if layout_box.node_id == target {
201        return Some(layout_box);
202    }
203
204    layout_box
205        .children
206        .iter()
207        .find_map(|child| find_layout_box(child, target))
208}
209
210fn layout_box_bounds(layout_box: &LayoutBox) -> (f32, f32, f32, f32) {
211    (
212        layout_box.rect.x,
213        layout_box.rect.y,
214        layout_box.rect.width,
215        layout_box.rect.height,
216    )
217}
218
219fn debug_info_report<R: Renderer>(app: &mut ShellApp, surface: &mut RootSurface<R>) -> String {
220    let app_context = std::rc::Rc::clone(&app.app_context);
221    app_context.enter(|| {
222        let mut report = String::new();
223        writeln!(report, "=== DEBUG: CURRENT SCREEN STATE ===").ok();
224        if let Some(layout_tree) = surface.layout_tree_in_context(app) {
225            let renderer = HeadlessRenderer::new();
226            let render_scene = renderer.render(layout_tree);
227            writeln!(report, "{}", format_layout_tree(layout_tree)).ok();
228            writeln!(report, "{}", format_render_scene(&render_scene)).ok();
229            writeln!(
230                report,
231                "{}",
232                format_screen_summary(layout_tree, &render_scene)
233            )
234            .ok();
235        } else {
236            writeln!(report, "No layout available").ok();
237        }
238        report
239    })
240}
241
242impl<R> SurfaceMut<'_, R>
243where
244    R: Renderer,
245    R::Error: Debug,
246{
247    /// The layout tree and headless scene of this surface, for a log.
248    pub fn debug_info_report(&mut self) -> String {
249        let (app, surface) = self.parts();
250        debug_info_report(app, surface)
251    }
252
253    /// Logs [`Self::debug_info_report`] and answers it.
254    pub fn log_debug_info(&mut self) -> String {
255        let report = self.debug_info_report();
256        log::info!(target: "cranpose::debug::screen", "\n{report}");
257        report
258    }
259}