Skip to main content

cranpose_ui/
debug.rs

1//! Debug utilities for inspecting the current screen state
2//!
3//! This module provides utilities to log and format the UI hierarchy and render operations,
4//! making it easier to debug layout and rendering issues.
5//!
6//! # Usage
7//!
8//! ```text
9//! use cranpose_ui::{log_layout_tree, log_render_scene, log_screen_summary};
10//!
11//! // After computing layout
12//! let layout_tree = applier.compute_layout(root, viewport_size)?;
13//! log_layout_tree(&layout_tree);
14//!
15//! // After rendering
16//! let renderer = HeadlessRenderer::new();
17//! let render_scene = renderer.render(&layout_tree);
18//! log_render_scene(&render_scene);
19//!
20//! // Or get a quick summary
21//! log_screen_summary(&layout_tree, &render_scene);
22//! ```
23
24use std::{fmt::Write, sync::Arc};
25
26use cranpose_foundation::{ModifierNodeChain, NodeCapabilities};
27
28use crate::{
29    layout::{LayoutBox, LayoutTree},
30    modifier::{ModifierChainInspectorNode, ModifierInspectorRecord},
31    renderer::{RecordedRenderScene, RenderOp},
32};
33
34/// Logs the current layout tree through the logger with indentation showing hierarchy.
35pub fn log_layout_tree(layout: &LayoutTree) {
36    log::info!(
37        target: "cranpose::debug::layout",
38        "\n{}",
39        format_layout_tree(layout)
40    );
41}
42
43/// Logs the current render scene through the logger.
44pub fn log_render_scene(scene: &RecordedRenderScene) {
45    log::info!(
46        target: "cranpose::debug::render",
47        "\n{}",
48        format_render_scene(scene)
49    );
50}
51
52/// Returns a formatted string representation of the layout tree
53pub fn format_layout_tree(layout: &LayoutTree) -> String {
54    let mut output = String::new();
55    writeln!(output, "=== LAYOUT TREE (Current Screen) ===").ok();
56    format_layout_box(&mut output, layout.root(), 0);
57    writeln!(output, "=== END LAYOUT TREE ===").ok();
58    output
59}
60
61fn format_layout_box(output: &mut String, layout_box: &LayoutBox, depth: usize) {
62    let indent = "  ".repeat(depth);
63    let rect = &layout_box.rect;
64
65    writeln!(
66        output,
67        "{}[Node #{}] pos: ({:.1}, {:.1}), size: ({:.1}x{:.1})",
68        indent, layout_box.node_id, rect.x, rect.y, rect.width, rect.height
69    )
70    .ok();
71
72    for child in &layout_box.children {
73        format_layout_box(output, child, depth + 1);
74    }
75}
76
77/// Returns a formatted string representation of the render scene
78pub fn format_render_scene(scene: &RecordedRenderScene) -> String {
79    let mut output = String::new();
80    writeln!(output, "=== RENDER SCENE (Current Screen) ===").ok();
81    writeln!(output, "Total operations: {}", scene.operations().len()).ok();
82
83    for (idx, op) in scene.operations().iter().enumerate() {
84        match op {
85            RenderOp::Primitive {
86                node_id,
87                layer,
88                primitive,
89            } => {
90                writeln!(
91                    output,
92                    "[{}] Node #{} - Layer: {:?}, Primitive: {:?}",
93                    idx, node_id, layer, primitive
94                )
95                .ok();
96            }
97            RenderOp::Text {
98                node_id,
99                rect,
100                value,
101            } => {
102                writeln!(
103                    output,
104                    "[{}] Node #{} - Text at ({:.1}, {:.1}): \"{}\"",
105                    idx, node_id, rect.x, rect.y, value
106                )
107                .ok();
108            }
109        }
110    }
111    writeln!(output, "=== END RENDER SCENE ===").ok();
112    output
113}
114
115/// Returns a compact summary of what's on screen (counts by type).
116pub fn format_screen_summary(layout: &LayoutTree, scene: &RecordedRenderScene) -> String {
117    let mut output = String::new();
118    writeln!(output, "=== SCREEN SUMMARY ===").ok();
119    writeln!(
120        output,
121        "Total nodes in layout: {}",
122        count_nodes(layout.root())
123    )
124    .ok();
125
126    let mut text_count = 0;
127    let mut primitive_count = 0;
128
129    for op in scene.operations() {
130        match op {
131            RenderOp::Text { .. } => text_count += 1,
132            RenderOp::Primitive { .. } => primitive_count += 1,
133        }
134    }
135
136    writeln!(output, "Render operations:").ok();
137    writeln!(output, "  - Text elements: {}", text_count).ok();
138    writeln!(output, "  - Primitive shapes: {}", primitive_count).ok();
139    writeln!(output, "=== END SUMMARY ===").ok();
140    output
141}
142
143/// Logs a compact summary of what's on screen (counts by type).
144pub fn log_screen_summary(layout: &LayoutTree, scene: &RecordedRenderScene) {
145    log::info!(
146        target: "cranpose::debug::screen",
147        "\n{}",
148        format_screen_summary(layout, scene)
149    );
150}
151
152fn count_nodes(layout_box: &LayoutBox) -> usize {
153    1 + layout_box.children.iter().map(count_nodes).sum::<usize>()
154}
155
156/// Logs the contents of a modifier node chain including capabilities.
157pub fn log_modifier_chain(chain: &ModifierNodeChain, nodes: &[ModifierChainInspectorNode]) {
158    log::info!(
159        target: "cranpose::debug::modifier",
160        "\n{}",
161        format_modifier_chain(chain, nodes)
162    );
163}
164
165/// Formats the modifier chain using inspector data.
166pub fn format_modifier_chain(
167    chain: &ModifierNodeChain,
168    nodes: &[ModifierChainInspectorNode],
169) -> String {
170    let mut output = String::new();
171    writeln!(output, "\n=== MODIFIER CHAIN ===").ok();
172    writeln!(
173        output,
174        "Total nodes: {} (entries: {})",
175        nodes.len(),
176        chain.len()
177    )
178    .ok();
179    writeln!(
180        output,
181        "Aggregated capabilities: {}",
182        describe_capabilities(chain.capabilities())
183    )
184    .ok();
185    for node in nodes {
186        let indent = "  ".repeat(node.depth);
187        let inspector = node
188            .inspector
189            .as_ref()
190            .map(describe_inspector)
191            .unwrap_or_default();
192        let inspector_suffix = if inspector.is_empty() {
193            String::new()
194        } else {
195            format!(" {inspector}")
196        };
197        writeln!(
198            output,
199            "{}- {} caps={} agg={}{}",
200            indent,
201            node.type_name,
202            describe_capabilities(node.capabilities),
203            describe_capabilities(node.aggregate_child_capabilities),
204            inspector_suffix,
205        )
206        .ok();
207    }
208    writeln!(output, "=== END MODIFIER CHAIN ===\n").ok();
209    output
210}
211
212fn describe_capabilities(mask: NodeCapabilities) -> String {
213    let mut parts = Vec::new();
214    if mask.contains(NodeCapabilities::LAYOUT) {
215        parts.push("LAYOUT");
216    }
217    if mask.contains(NodeCapabilities::DRAW) {
218        parts.push("DRAW");
219    }
220    if mask.contains(NodeCapabilities::POINTER_INPUT) {
221        parts.push("POINTER_INPUT");
222    }
223    if mask.contains(NodeCapabilities::SEMANTICS) {
224        parts.push("SEMANTICS");
225    }
226    if mask.contains(NodeCapabilities::MODIFIER_LOCALS) {
227        parts.push("MODIFIER_LOCALS");
228    }
229    if mask.contains(NodeCapabilities::FOCUS) {
230        parts.push("FOCUS");
231    }
232    if parts.is_empty() {
233        "[NONE]".to_string()
234    } else {
235        format!("[{}]", parts.join("|"))
236    }
237}
238
239fn describe_inspector(record: &ModifierInspectorRecord) -> String {
240    if record.properties.is_empty() {
241        record.name.to_string()
242    } else {
243        let props = record
244            .properties
245            .iter()
246            .map(|prop| format!("{}={}", prop.name, prop.value))
247            .collect::<Vec<_>>()
248            .join(", ");
249        format!("{}({})", record.name, props)
250    }
251}
252
253/// RAII guard returned when installing a modifier chain trace subscriber.
254pub struct ModifierChainTraceGuard {
255    context_id: Option<crate::render_state::AppContextId>,
256}
257
258impl Drop for ModifierChainTraceGuard {
259    fn drop(&mut self) {
260        if let Some(context_id) = self.context_id.take() {
261            crate::render_state::clear_modifier_chain_trace(context_id);
262        }
263    }
264}
265
266/// Installs a callback that receives modifier chain snapshots when debugging is enabled.
267pub fn install_modifier_chain_trace<F>(callback: F) -> ModifierChainTraceGuard
268where
269    F: Fn(&[ModifierChainInspectorNode]) + Send + Sync + 'static,
270{
271    let context_id = crate::render_state::set_modifier_chain_trace(Arc::new(callback));
272    ModifierChainTraceGuard {
273        context_id: Some(context_id),
274    }
275}
276
277pub(crate) fn emit_modifier_chain_trace(nodes: &[ModifierChainInspectorNode]) {
278    crate::render_state::emit_modifier_chain_trace(nodes);
279}
280
281#[cfg(test)]
282#[path = "tests/debug_tests.rs"]
283mod tests;