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