tui_dispatch_core/debug/
cell.rs

1//! Cell inspection utilities
2//!
3//! Provides types and functions for inspecting individual buffer cells,
4//! useful for debugging UI rendering issues.
5
6use ratatui::buffer::Buffer;
7use ratatui::style::{Color, Modifier};
8
9/// Visual representation of a buffer cell for debug preview
10#[derive(Debug, Clone)]
11pub struct CellPreview {
12    /// The symbol/character in the cell
13    pub symbol: String,
14    /// Foreground color
15    pub fg: Color,
16    /// Background color
17    pub bg: Color,
18    /// Text modifiers (bold, italic, etc.)
19    pub modifier: Modifier,
20}
21
22impl CellPreview {
23    /// Create a new cell preview
24    pub fn new(symbol: impl Into<String>, fg: Color, bg: Color, modifier: Modifier) -> Self {
25        Self {
26            symbol: symbol.into(),
27            fg,
28            bg,
29            modifier,
30        }
31    }
32
33    /// Check if the cell has the default/reset style
34    pub fn is_default_style(&self) -> bool {
35        self.fg == Color::Reset && self.bg == Color::Reset && self.modifier.is_empty()
36    }
37}
38
39/// Inspect a cell at the given position in a buffer
40///
41/// Returns `Some(CellPreview)` if the position is within the buffer bounds,
42/// `None` otherwise.
43///
44/// # Example
45///
46/// ```ignore
47/// use tui_dispatch_core::debug::inspect_cell;
48///
49/// let preview = inspect_cell(&buffer, 10, 5);
50/// if let Some(cell) = preview {
51///     println!("Symbol: {}, FG: {:?}", cell.symbol, cell.fg);
52/// }
53/// ```
54pub fn inspect_cell(buffer: &Buffer, x: u16, y: u16) -> Option<CellPreview> {
55    let area = buffer.area;
56    if !point_in_rect(area.x, area.y, area.width, area.height, x, y) {
57        return None;
58    }
59
60    let cell = &buffer[(x, y)];
61    Some(CellPreview {
62        symbol: cell.symbol().to_string(),
63        fg: cell.fg,
64        bg: cell.bg,
65        modifier: cell.modifier,
66    })
67}
68
69/// Check if a point is within a rectangle
70#[inline]
71pub fn point_in_rect(rect_x: u16, rect_y: u16, width: u16, height: u16, x: u16, y: u16) -> bool {
72    let within_x = x >= rect_x && x < rect_x.saturating_add(width);
73    let within_y = y >= rect_y && y < rect_y.saturating_add(height);
74    within_x && within_y
75}
76
77/// Format a color as a compact string for display
78///
79/// # Example
80///
81/// ```
82/// use ratatui::style::Color;
83/// use tui_dispatch_core::debug::format_color_compact;
84///
85/// assert_eq!(format_color_compact(Color::Rgb(255, 128, 0)), "(255,128,0)");
86/// assert_eq!(format_color_compact(Color::Red), "Red");
87/// assert_eq!(format_color_compact(Color::Reset), "Reset");
88/// ```
89pub fn format_color_compact(color: Color) -> String {
90    match color {
91        Color::Rgb(r, g, b) => format!("({r},{g},{b})"),
92        Color::Indexed(i) => format!("#{i}"),
93        other => format!("{other:?}"),
94    }
95}
96
97/// Format modifiers as a compact string
98///
99/// Returns an empty string if no modifiers are set.
100pub fn format_modifier_compact(modifier: Modifier) -> String {
101    if modifier.is_empty() {
102        return String::new();
103    }
104
105    let mut parts = Vec::new();
106    if modifier.contains(Modifier::BOLD) {
107        parts.push("B");
108    }
109    if modifier.contains(Modifier::DIM) {
110        parts.push("D");
111    }
112    if modifier.contains(Modifier::ITALIC) {
113        parts.push("I");
114    }
115    if modifier.contains(Modifier::UNDERLINED) {
116        parts.push("U");
117    }
118    if modifier.contains(Modifier::SLOW_BLINK) || modifier.contains(Modifier::RAPID_BLINK) {
119        parts.push("*");
120    }
121    if modifier.contains(Modifier::REVERSED) {
122        parts.push("R");
123    }
124    if modifier.contains(Modifier::CROSSED_OUT) {
125        parts.push("X");
126    }
127
128    parts.join("")
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use ratatui::layout::Rect;
135
136    #[test]
137    fn test_point_in_rect() {
138        assert!(point_in_rect(0, 0, 10, 10, 5, 5));
139        assert!(point_in_rect(0, 0, 10, 10, 0, 0));
140        assert!(point_in_rect(0, 0, 10, 10, 9, 9));
141        assert!(!point_in_rect(0, 0, 10, 10, 10, 10));
142        assert!(!point_in_rect(5, 5, 10, 10, 4, 5));
143    }
144
145    #[test]
146    fn test_format_color_compact() {
147        assert_eq!(format_color_compact(Color::Rgb(255, 0, 128)), "(255,0,128)");
148        assert_eq!(format_color_compact(Color::Red), "Red");
149        assert_eq!(format_color_compact(Color::Reset), "Reset");
150        assert_eq!(format_color_compact(Color::Indexed(42)), "#42");
151    }
152
153    #[test]
154    fn test_format_modifier_compact() {
155        assert_eq!(format_modifier_compact(Modifier::empty()), "");
156        assert_eq!(format_modifier_compact(Modifier::BOLD), "B");
157        assert_eq!(
158            format_modifier_compact(Modifier::BOLD | Modifier::ITALIC),
159            "BI"
160        );
161    }
162
163    #[test]
164    fn test_inspect_cell() {
165        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
166        buffer[(5, 5)].set_char('X');
167        buffer[(5, 5)].set_fg(Color::Red);
168
169        let preview = inspect_cell(&buffer, 5, 5).unwrap();
170        assert_eq!(preview.symbol, "X");
171        assert_eq!(preview.fg, Color::Red);
172
173        // Out of bounds
174        assert!(inspect_cell(&buffer, 20, 20).is_none());
175    }
176
177    #[test]
178    fn test_cell_preview_is_default() {
179        let default = CellPreview::new(" ", Color::Reset, Color::Reset, Modifier::empty());
180        assert!(default.is_default_style());
181
182        let styled = CellPreview::new("X", Color::Red, Color::Reset, Modifier::empty());
183        assert!(!styled.is_default_style());
184    }
185}