cranpose_app_shell/
shell_debug.rs1use 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 pub fn debug_info_report(&mut self) -> String {
17 debug_info_report(&mut self.app, &mut self.surfaces[0])
18 }
19
20 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 #[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 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 pub fn semantics_active(&self) -> bool {
54 self.app.semantics_enabled
55 }
56
57 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 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 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 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}