Skip to main content

cranpose_app_shell/
shell_debug.rs

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