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