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