Skip to main content

astrelis_ui/
debug.rs

1//! Visual debug overlay for UI system.
2
3use astrelis_render::Color;
4
5use crate::dirty::DirtyFlags;
6use crate::metrics::UiMetrics;
7use crate::tree::{LayoutRect, NodeId, UiNode, UiTree};
8
9/// Configuration for debug overlay visualization.
10#[derive(Debug, Clone)]
11pub struct DebugOverlay {
12    /// Show rectangles around dirty nodes
13    pub show_dirty_rects: bool,
14
15    /// Show layout bounds for all nodes
16    pub show_layout_bounds: bool,
17
18    /// Show performance metrics as text overlay
19    pub show_metrics: bool,
20
21    /// Show node IDs
22    pub show_node_ids: bool,
23
24    /// Show dirty flags as text labels
25    pub show_dirty_flags: bool,
26
27    /// Highlight nodes with specific dirty flags
28    pub highlight_layout_dirty: bool,
29    pub highlight_text_dirty: bool,
30    pub highlight_paint_only: bool,
31
32    /// Opacity of debug overlays (0.0 to 1.0)
33    pub overlay_opacity: f32,
34}
35
36impl Default for DebugOverlay {
37    fn default() -> Self {
38        Self {
39            show_dirty_rects: false,
40            show_layout_bounds: false,
41            show_metrics: false,
42            show_node_ids: false,
43            show_dirty_flags: false,
44            highlight_layout_dirty: false,
45            highlight_text_dirty: false,
46            highlight_paint_only: false,
47            overlay_opacity: 0.5,
48        }
49    }
50}
51
52impl DebugOverlay {
53    /// Create a new debug overlay with all features disabled.
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Enable all debug features.
59    pub fn all() -> Self {
60        Self {
61            show_dirty_rects: true,
62            show_layout_bounds: true,
63            show_metrics: true,
64            show_node_ids: true,
65            show_dirty_flags: true,
66            highlight_layout_dirty: true,
67            highlight_text_dirty: true,
68            highlight_paint_only: true,
69            overlay_opacity: 0.7,
70        }
71    }
72
73    /// Enable only dirty rect visualization.
74    pub fn dirty_rects_only() -> Self {
75        Self {
76            show_dirty_rects: true,
77            ..Default::default()
78        }
79    }
80
81    /// Enable only layout bounds.
82    pub fn layout_bounds_only() -> Self {
83        Self {
84            show_layout_bounds: true,
85            ..Default::default()
86        }
87    }
88
89    /// Enable only metrics display.
90    pub fn metrics_only() -> Self {
91        Self {
92            show_metrics: true,
93            ..Default::default()
94        }
95    }
96
97    /// Check if any debug features are enabled.
98    pub fn is_enabled(&self) -> bool {
99        self.show_dirty_rects
100            || self.show_layout_bounds
101            || self.show_metrics
102            || self.show_node_ids
103            || self.show_dirty_flags
104            || self.highlight_layout_dirty
105            || self.highlight_text_dirty
106            || self.highlight_paint_only
107    }
108
109    /// Toggle dirty rect visualization.
110    pub fn toggle_dirty_rects(&mut self) {
111        self.show_dirty_rects = !self.show_dirty_rects;
112    }
113
114    /// Toggle layout bounds visualization.
115    pub fn toggle_layout_bounds(&mut self) {
116        self.show_layout_bounds = !self.show_layout_bounds;
117    }
118
119    /// Toggle metrics display.
120    pub fn toggle_metrics(&mut self) {
121        self.show_metrics = !self.show_metrics;
122    }
123}
124
125/// Information about a node to be rendered in the debug overlay.
126#[derive(Debug, Clone)]
127pub struct DebugNodeInfo {
128    pub node_id: NodeId,
129    pub layout: LayoutRect,
130    pub dirty_flags: DirtyFlags,
131    pub color: Color,
132    pub label: Option<String>,
133}
134
135impl DebugNodeInfo {
136    /// Create debug info for a node.
137    pub fn from_node(node_id: NodeId, node: &UiNode, overlay: &DebugOverlay) -> Option<Self> {
138        let dirty_flags = node.dirty_flags;
139
140        // Determine if this node should be shown
141        if !overlay.show_dirty_rects
142            && !overlay.show_layout_bounds
143            && !should_highlight(dirty_flags, overlay)
144        {
145            return None;
146        }
147
148        let color = get_debug_color(dirty_flags, overlay);
149        let label = if overlay.show_node_ids || overlay.show_dirty_flags {
150            Some(format_label(node_id, dirty_flags, overlay))
151        } else {
152            None
153        };
154
155        Some(DebugNodeInfo {
156            node_id,
157            layout: node.layout,
158            dirty_flags,
159            color,
160            label,
161        })
162    }
163}
164
165/// Get the debug color for a node based on its dirty flags.
166fn get_debug_color(flags: DirtyFlags, overlay: &DebugOverlay) -> Color {
167    if flags.is_empty() && overlay.show_layout_bounds {
168        // Clean node - gray
169        return Color::from_rgba_u8(128, 128, 128, (overlay.overlay_opacity * 255.0) as u8);
170    }
171
172    // Priority-based coloring
173    if flags.contains(DirtyFlags::LAYOUT) && overlay.highlight_layout_dirty {
174        // Layout dirty - red
175        Color::from_rgba_u8(255, 0, 0, (overlay.overlay_opacity * 255.0) as u8)
176    } else if flags.contains(DirtyFlags::TEXT_SHAPING) && overlay.highlight_text_dirty {
177        // Text dirty - yellow
178        Color::from_rgba_u8(255, 255, 0, (overlay.overlay_opacity * 255.0) as u8)
179    } else if flags.is_paint_only() && overlay.highlight_paint_only {
180        // Paint only - green
181        Color::from_rgba_u8(0, 255, 0, (overlay.overlay_opacity * 255.0) as u8)
182    } else if flags.contains(DirtyFlags::CHILDREN_ORDER) {
183        // Children order - purple
184        Color::from_rgba_u8(255, 0, 255, (overlay.overlay_opacity * 255.0) as u8)
185    } else if flags.contains(DirtyFlags::GEOMETRY) {
186        // Geometry - cyan
187        Color::from_rgba_u8(0, 255, 255, (overlay.overlay_opacity * 255.0) as u8)
188    } else if flags.contains(DirtyFlags::TRANSFORM) {
189        // Transform - orange
190        Color::from_rgba_u8(255, 165, 0, (overlay.overlay_opacity * 255.0) as u8)
191    } else if overlay.show_dirty_rects && !flags.is_empty() {
192        // Generic dirty - white
193        Color::from_rgba_u8(255, 255, 255, (overlay.overlay_opacity * 255.0) as u8)
194    } else {
195        // Fallback - gray
196        Color::from_rgba_u8(128, 128, 128, (overlay.overlay_opacity * 255.0) as u8)
197    }
198}
199
200/// Check if a node should be highlighted based on its flags.
201fn should_highlight(flags: DirtyFlags, overlay: &DebugOverlay) -> bool {
202    (overlay.highlight_layout_dirty && flags.contains(DirtyFlags::LAYOUT))
203        || (overlay.highlight_text_dirty && flags.contains(DirtyFlags::TEXT_SHAPING))
204        || (overlay.highlight_paint_only && flags.is_paint_only())
205}
206
207/// Format a label for a node.
208fn format_label(node_id: NodeId, flags: DirtyFlags, overlay: &DebugOverlay) -> String {
209    let mut parts = Vec::new();
210
211    if overlay.show_node_ids {
212        parts.push(format!("#{}", node_id.0));
213    }
214
215    if overlay.show_dirty_flags && !flags.is_empty() {
216        parts.push(format_flags(flags));
217    }
218
219    parts.join(" ")
220}
221
222/// Format dirty flags as a compact string.
223fn format_flags(flags: DirtyFlags) -> String {
224    if flags.is_empty() {
225        return String::from("CLEAN");
226    }
227
228    let mut parts = Vec::new();
229
230    if flags.contains(DirtyFlags::LAYOUT) {
231        parts.push("L");
232    }
233    if flags.contains(DirtyFlags::TEXT_SHAPING) {
234        parts.push("T");
235    }
236    if flags.contains(DirtyFlags::COLOR) {
237        parts.push("C");
238    }
239    if flags.contains(DirtyFlags::OPACITY) {
240        parts.push("O");
241    }
242    if flags.contains(DirtyFlags::GEOMETRY) {
243        parts.push("G");
244    }
245    if flags.contains(DirtyFlags::IMAGE) {
246        parts.push("I");
247    }
248    if flags.contains(DirtyFlags::FOCUS) {
249        parts.push("F");
250    }
251    if flags.contains(DirtyFlags::TRANSFORM) {
252        parts.push("X");
253    }
254    if flags.contains(DirtyFlags::CLIP) {
255        parts.push("CL");
256    }
257    if flags.contains(DirtyFlags::VISIBILITY) {
258        parts.push("V");
259    }
260    if flags.contains(DirtyFlags::SCROLL) {
261        parts.push("S");
262    }
263    if flags.contains(DirtyFlags::CHILDREN_ORDER) {
264        parts.push("CH");
265    }
266
267    parts.join("|")
268}
269
270/// Collect debug info for all nodes in a tree.
271pub fn collect_debug_info(tree: &UiTree, overlay: &DebugOverlay) -> Vec<DebugNodeInfo> {
272    if !overlay.is_enabled() {
273        return Vec::new();
274    }
275
276    tree.iter()
277        .filter_map(|(node_id, node)| DebugNodeInfo::from_node(node_id, node, overlay))
278        .collect()
279}
280
281/// Format metrics for overlay display.
282pub fn format_metrics_overlay(metrics: &UiMetrics) -> String {
283    format!(
284        "UI Metrics:\n\
285         Total: {:.2}ms\n\
286         Layout: {:.2}ms ({} nodes)\n\
287         Text: {:.2}ms ({} dirty)\n\
288         Paint: {} nodes\n\
289         Cache: {:.0}% hits",
290        metrics.total_time.as_secs_f64() * 1000.0,
291        metrics.layout_time.as_secs_f64() * 1000.0,
292        metrics.nodes_layout_dirty,
293        metrics.text_shape_time.as_secs_f64() * 1000.0,
294        metrics.nodes_text_dirty,
295        metrics.nodes_paint_dirty,
296        metrics.text_cache_hit_rate() * 100.0,
297    )
298}
299
300/// Color legend for debug overlay.
301pub fn color_legend() -> Vec<(Color, &'static str)> {
302    vec![
303        (Color::from_rgb_u8(255, 0, 0), "Layout Dirty"),
304        (Color::from_rgb_u8(255, 255, 0), "Text Dirty"),
305        (Color::from_rgb_u8(0, 255, 0), "Paint Only"),
306        (Color::from_rgb_u8(255, 0, 255), "Children Order"),
307        (Color::from_rgb_u8(0, 255, 255), "Geometry"),
308        (Color::from_rgb_u8(255, 165, 0), "Transform"),
309        (Color::from_rgb_u8(128, 128, 128), "Clean"),
310    ]
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_debug_overlay_default() {
319        let overlay = DebugOverlay::default();
320        assert!(!overlay.is_enabled());
321    }
322
323    #[test]
324    fn test_debug_overlay_all() {
325        let overlay = DebugOverlay::all();
326        assert!(overlay.is_enabled());
327        assert!(overlay.show_dirty_rects);
328        assert!(overlay.show_metrics);
329    }
330
331    #[test]
332    fn test_format_flags() {
333        assert_eq!(format_flags(DirtyFlags::NONE), "CLEAN");
334        assert_eq!(format_flags(DirtyFlags::LAYOUT), "L");
335        assert_eq!(
336            format_flags(DirtyFlags::LAYOUT | DirtyFlags::TEXT_SHAPING),
337            "L|T"
338        );
339    }
340
341    #[test]
342    fn test_should_highlight() {
343        let overlay = DebugOverlay {
344            highlight_layout_dirty: true,
345            ..Default::default()
346        };
347        assert!(should_highlight(DirtyFlags::LAYOUT, &overlay));
348        assert!(!should_highlight(DirtyFlags::COLOR, &overlay));
349    }
350
351    #[test]
352    fn test_toggle() {
353        let mut overlay = DebugOverlay::default();
354        assert!(!overlay.show_dirty_rects);
355        overlay.toggle_dirty_rects();
356        assert!(overlay.show_dirty_rects);
357    }
358}