use astrelis_render::Color;
use crate::dirty::DirtyFlags;
use crate::metrics::UiMetrics;
use crate::tree::{LayoutRect, NodeId, UiNode, UiTree};
#[derive(Debug, Clone)]
pub struct DebugOverlay {
pub show_dirty_rects: bool,
pub show_layout_bounds: bool,
pub show_metrics: bool,
pub show_node_ids: bool,
pub show_dirty_flags: bool,
pub highlight_layout_dirty: bool,
pub highlight_text_dirty: bool,
pub highlight_paint_only: bool,
pub overlay_opacity: f32,
}
impl Default for DebugOverlay {
fn default() -> Self {
Self {
show_dirty_rects: false,
show_layout_bounds: false,
show_metrics: false,
show_node_ids: false,
show_dirty_flags: false,
highlight_layout_dirty: false,
highlight_text_dirty: false,
highlight_paint_only: false,
overlay_opacity: 0.5,
}
}
}
impl DebugOverlay {
pub fn new() -> Self {
Self::default()
}
pub fn all() -> Self {
Self {
show_dirty_rects: true,
show_layout_bounds: true,
show_metrics: true,
show_node_ids: true,
show_dirty_flags: true,
highlight_layout_dirty: true,
highlight_text_dirty: true,
highlight_paint_only: true,
overlay_opacity: 0.7,
}
}
pub fn dirty_rects_only() -> Self {
Self {
show_dirty_rects: true,
..Default::default()
}
}
pub fn layout_bounds_only() -> Self {
Self {
show_layout_bounds: true,
..Default::default()
}
}
pub fn metrics_only() -> Self {
Self {
show_metrics: true,
..Default::default()
}
}
pub fn is_enabled(&self) -> bool {
self.show_dirty_rects
|| self.show_layout_bounds
|| self.show_metrics
|| self.show_node_ids
|| self.show_dirty_flags
|| self.highlight_layout_dirty
|| self.highlight_text_dirty
|| self.highlight_paint_only
}
pub fn toggle_dirty_rects(&mut self) {
self.show_dirty_rects = !self.show_dirty_rects;
}
pub fn toggle_layout_bounds(&mut self) {
self.show_layout_bounds = !self.show_layout_bounds;
}
pub fn toggle_metrics(&mut self) {
self.show_metrics = !self.show_metrics;
}
}
#[derive(Debug, Clone)]
pub struct DebugNodeInfo {
pub node_id: NodeId,
pub layout: LayoutRect,
pub dirty_flags: DirtyFlags,
pub color: Color,
pub label: Option<String>,
}
impl DebugNodeInfo {
pub fn from_node(node_id: NodeId, node: &UiNode, overlay: &DebugOverlay) -> Option<Self> {
let dirty_flags = node.dirty_flags;
if !overlay.show_dirty_rects
&& !overlay.show_layout_bounds
&& !should_highlight(dirty_flags, overlay)
{
return None;
}
let color = get_debug_color(dirty_flags, overlay);
let label = if overlay.show_node_ids || overlay.show_dirty_flags {
Some(format_label(node_id, dirty_flags, overlay))
} else {
None
};
Some(DebugNodeInfo {
node_id,
layout: node.layout,
dirty_flags,
color,
label,
})
}
}
fn get_debug_color(flags: DirtyFlags, overlay: &DebugOverlay) -> Color {
if flags.is_empty() && overlay.show_layout_bounds {
return Color::from_rgba_u8(128, 128, 128, (overlay.overlay_opacity * 255.0) as u8);
}
if flags.contains(DirtyFlags::LAYOUT) && overlay.highlight_layout_dirty {
Color::from_rgba_u8(255, 0, 0, (overlay.overlay_opacity * 255.0) as u8)
} else if flags.contains(DirtyFlags::TEXT_SHAPING) && overlay.highlight_text_dirty {
Color::from_rgba_u8(255, 255, 0, (overlay.overlay_opacity * 255.0) as u8)
} else if flags.is_paint_only() && overlay.highlight_paint_only {
Color::from_rgba_u8(0, 255, 0, (overlay.overlay_opacity * 255.0) as u8)
} else if flags.contains(DirtyFlags::CHILDREN_ORDER) {
Color::from_rgba_u8(255, 0, 255, (overlay.overlay_opacity * 255.0) as u8)
} else if flags.contains(DirtyFlags::GEOMETRY) {
Color::from_rgba_u8(0, 255, 255, (overlay.overlay_opacity * 255.0) as u8)
} else if flags.contains(DirtyFlags::TRANSFORM) {
Color::from_rgba_u8(255, 165, 0, (overlay.overlay_opacity * 255.0) as u8)
} else if overlay.show_dirty_rects && !flags.is_empty() {
Color::from_rgba_u8(255, 255, 255, (overlay.overlay_opacity * 255.0) as u8)
} else {
Color::from_rgba_u8(128, 128, 128, (overlay.overlay_opacity * 255.0) as u8)
}
}
fn should_highlight(flags: DirtyFlags, overlay: &DebugOverlay) -> bool {
(overlay.highlight_layout_dirty && flags.contains(DirtyFlags::LAYOUT))
|| (overlay.highlight_text_dirty && flags.contains(DirtyFlags::TEXT_SHAPING))
|| (overlay.highlight_paint_only && flags.is_paint_only())
}
fn format_label(node_id: NodeId, flags: DirtyFlags, overlay: &DebugOverlay) -> String {
let mut parts = Vec::new();
if overlay.show_node_ids {
parts.push(format!("#{}", node_id.0));
}
if overlay.show_dirty_flags && !flags.is_empty() {
parts.push(format_flags(flags));
}
parts.join(" ")
}
fn format_flags(flags: DirtyFlags) -> String {
if flags.is_empty() {
return String::from("CLEAN");
}
let mut parts = Vec::new();
if flags.contains(DirtyFlags::LAYOUT) {
parts.push("L");
}
if flags.contains(DirtyFlags::TEXT_SHAPING) {
parts.push("T");
}
if flags.contains(DirtyFlags::COLOR) {
parts.push("C");
}
if flags.contains(DirtyFlags::OPACITY) {
parts.push("O");
}
if flags.contains(DirtyFlags::GEOMETRY) {
parts.push("G");
}
if flags.contains(DirtyFlags::IMAGE) {
parts.push("I");
}
if flags.contains(DirtyFlags::FOCUS) {
parts.push("F");
}
if flags.contains(DirtyFlags::TRANSFORM) {
parts.push("X");
}
if flags.contains(DirtyFlags::CLIP) {
parts.push("CL");
}
if flags.contains(DirtyFlags::VISIBILITY) {
parts.push("V");
}
if flags.contains(DirtyFlags::SCROLL) {
parts.push("S");
}
if flags.contains(DirtyFlags::CHILDREN_ORDER) {
parts.push("CH");
}
parts.join("|")
}
pub fn collect_debug_info(tree: &UiTree, overlay: &DebugOverlay) -> Vec<DebugNodeInfo> {
if !overlay.is_enabled() {
return Vec::new();
}
tree.iter()
.filter_map(|(node_id, node)| DebugNodeInfo::from_node(node_id, node, overlay))
.collect()
}
pub fn format_metrics_overlay(metrics: &UiMetrics) -> String {
format!(
"UI Metrics:\n\
Total: {:.2}ms\n\
Layout: {:.2}ms ({} nodes)\n\
Text: {:.2}ms ({} dirty)\n\
Paint: {} nodes\n\
Cache: {:.0}% hits",
metrics.total_time.as_secs_f64() * 1000.0,
metrics.layout_time.as_secs_f64() * 1000.0,
metrics.nodes_layout_dirty,
metrics.text_shape_time.as_secs_f64() * 1000.0,
metrics.nodes_text_dirty,
metrics.nodes_paint_dirty,
metrics.text_cache_hit_rate() * 100.0,
)
}
pub fn color_legend() -> Vec<(Color, &'static str)> {
vec![
(Color::from_rgb_u8(255, 0, 0), "Layout Dirty"),
(Color::from_rgb_u8(255, 255, 0), "Text Dirty"),
(Color::from_rgb_u8(0, 255, 0), "Paint Only"),
(Color::from_rgb_u8(255, 0, 255), "Children Order"),
(Color::from_rgb_u8(0, 255, 255), "Geometry"),
(Color::from_rgb_u8(255, 165, 0), "Transform"),
(Color::from_rgb_u8(128, 128, 128), "Clean"),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug_overlay_default() {
let overlay = DebugOverlay::default();
assert!(!overlay.is_enabled());
}
#[test]
fn test_debug_overlay_all() {
let overlay = DebugOverlay::all();
assert!(overlay.is_enabled());
assert!(overlay.show_dirty_rects);
assert!(overlay.show_metrics);
}
#[test]
fn test_format_flags() {
assert_eq!(format_flags(DirtyFlags::NONE), "CLEAN");
assert_eq!(format_flags(DirtyFlags::LAYOUT), "L");
assert_eq!(
format_flags(DirtyFlags::LAYOUT | DirtyFlags::TEXT_SHAPING),
"L|T"
);
}
#[test]
fn test_should_highlight() {
let overlay = DebugOverlay {
highlight_layout_dirty: true,
..Default::default()
};
assert!(should_highlight(DirtyFlags::LAYOUT, &overlay));
assert!(!should_highlight(DirtyFlags::COLOR, &overlay));
}
#[test]
fn test_toggle() {
let mut overlay = DebugOverlay::default();
assert!(!overlay.show_dirty_rects);
overlay.toggle_dirty_rects();
assert!(overlay.show_dirty_rects);
}
}