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