1use astrelis_render::Color;
4
5use crate::dirty::DirtyFlags;
6use crate::metrics::UiMetrics;
7use crate::tree::{LayoutRect, NodeId, UiNode, UiTree};
8
9#[derive(Debug, Clone)]
11pub struct DebugOverlay {
12 pub show_dirty_rects: bool,
14
15 pub show_layout_bounds: bool,
17
18 pub show_metrics: bool,
20
21 pub show_node_ids: bool,
23
24 pub show_dirty_flags: bool,
26
27 pub highlight_layout_dirty: bool,
29 pub highlight_text_dirty: bool,
30 pub highlight_paint_only: bool,
31
32 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 pub fn new() -> Self {
55 Self::default()
56 }
57
58 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 pub fn dirty_rects_only() -> Self {
75 Self {
76 show_dirty_rects: true,
77 ..Default::default()
78 }
79 }
80
81 pub fn layout_bounds_only() -> Self {
83 Self {
84 show_layout_bounds: true,
85 ..Default::default()
86 }
87 }
88
89 pub fn metrics_only() -> Self {
91 Self {
92 show_metrics: true,
93 ..Default::default()
94 }
95 }
96
97 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 pub fn toggle_dirty_rects(&mut self) {
111 self.show_dirty_rects = !self.show_dirty_rects;
112 }
113
114 pub fn toggle_layout_bounds(&mut self) {
116 self.show_layout_bounds = !self.show_layout_bounds;
117 }
118
119 pub fn toggle_metrics(&mut self) {
121 self.show_metrics = !self.show_metrics;
122 }
123}
124
125#[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 pub fn from_node(node_id: NodeId, node: &UiNode, overlay: &DebugOverlay) -> Option<Self> {
138 let dirty_flags = node.dirty_flags;
139
140 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
165fn get_debug_color(flags: DirtyFlags, overlay: &DebugOverlay) -> Color {
167 if flags.is_empty() && overlay.show_layout_bounds {
168 return Color::from_rgba_u8(128, 128, 128, (overlay.overlay_opacity * 255.0) as u8);
170 }
171
172 if flags.contains(DirtyFlags::LAYOUT) && overlay.highlight_layout_dirty {
174 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 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 Color::from_rgba_u8(0, 255, 0, (overlay.overlay_opacity * 255.0) as u8)
182 } else if flags.contains(DirtyFlags::CHILDREN_ORDER) {
183 Color::from_rgba_u8(255, 0, 255, (overlay.overlay_opacity * 255.0) as u8)
185 } else if flags.contains(DirtyFlags::GEOMETRY) {
186 Color::from_rgba_u8(0, 255, 255, (overlay.overlay_opacity * 255.0) as u8)
188 } else if flags.contains(DirtyFlags::TRANSFORM) {
189 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 Color::from_rgba_u8(255, 255, 255, (overlay.overlay_opacity * 255.0) as u8)
194 } else {
195 Color::from_rgba_u8(128, 128, 128, (overlay.overlay_opacity * 255.0) as u8)
197 }
198}
199
200fn 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
207fn 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
222fn 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
270pub 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
281pub 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
300pub 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}