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 mut report = String::new();
10        writeln!(report, "=== DEBUG: CURRENT SCREEN STATE ===").ok();
11        if let Some(layout_tree) = self.layout_tree() {
12            let renderer = HeadlessRenderer::new();
13            let render_scene = renderer.render(layout_tree);
14            writeln!(report, "{}", format_layout_tree(layout_tree)).ok();
15            writeln!(report, "{}", format_render_scene(&render_scene)).ok();
16            writeln!(
17                report,
18                "{}",
19                format_screen_summary(layout_tree, &render_scene)
20            )
21            .ok();
22        } else {
23            writeln!(report, "No layout available").ok();
24        }
25        report
26    }
27
28    pub fn log_debug_info(&mut self) -> String {
29        let report = self.debug_info_report();
30        log::info!(target: "cranpose::debug::screen", "\n{report}");
31        report
32    }
33
34    /// Get the current layout tree (for robot/testing)
35    pub fn layout_tree(&mut self) -> Option<&LayoutTree> {
36        if self.layout_tree.is_none() {
37            let root = self.composition.root()?;
38            let mut applier = self.composition.applier_mut();
39            match cranpose_ui::build_layout_tree_from_applier(&mut applier, root) {
40                Ok(layout_tree) => {
41                    self.layout_tree = layout_tree;
42                }
43                Err(err) => {
44                    log::debug!("failed to build layout snapshot: {err}");
45                    return None;
46                }
47            }
48        }
49        self.layout_tree.as_ref()
50    }
51
52    /// Get the current semantics tree (for robot/testing)
53    pub fn semantics_tree(&mut self) -> Option<&SemanticsTree> {
54        if !self.semantics_enabled {
55            return None;
56        }
57        let root = self.composition.root()?;
58        let semantics_dirty = {
59            let mut applier = self.composition.applier_mut();
60            cranpose_ui::tree_needs_semantics(&mut *applier, root).unwrap_or_else(|err| {
61                log::debug!("failed to check semantics dirty status for root #{root}: {err}");
62                true
63            })
64        };
65        if self.semantics_tree.is_none() || semantics_dirty {
66            let mut applier = self.composition.applier_mut();
67            match cranpose_ui::build_semantics_tree_from_applier(&mut applier, root) {
68                Ok(semantics_tree) => {
69                    self.semantics_tree = semantics_tree;
70                }
71                Err(err) => {
72                    log::debug!("failed to build semantics snapshot: {err}");
73                    return None;
74                }
75            }
76        }
77        self.semantics_tree.as_ref()
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.composition.runtime_handle();
97        let (applier_stats, live_node_heap_bytes, recycled_node_heap_bytes) = {
98            let applier = self.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.composition.slot_table_heap_bytes(),
110            pass_stats: self.composition.debug_last_pass_stats(),
111            slot_stats: self.composition.debug_slot_table_stats(),
112            observer_stats: self.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.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.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.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) -> Vec<(NodeId, Vec<(u64, Vec<usize>)>)> {
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.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.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<(usize, Key, Option<usize>, usize)>> {
189        let mut applier = self.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}