tui_dispatch_core/debug/
widgets.rs

1//! Debug rendering utilities and widgets
2//!
3//! Provides theme-agnostic widgets for rendering debug information.
4//! Applications provide their own styles to customize appearance.
5
6use ratatui::buffer::Buffer;
7use ratatui::layout::{Constraint, Rect};
8use ratatui::style::{Modifier, Style};
9use ratatui::text::{Line, Span};
10use ratatui::widgets::{Block, Cell, Clear, Paragraph, Row, Table, Widget};
11use ratatui::Frame;
12
13use super::cell::{format_color_compact, format_modifier_compact, CellPreview};
14use super::table::{DebugTableOverlay, DebugTableRow};
15
16/// Convert a buffer to plain text (for clipboard export)
17///
18/// Trims trailing whitespace from each line.
19pub fn buffer_to_text(buffer: &Buffer) -> String {
20    let area = buffer.area;
21    let mut out = String::new();
22
23    for y in area.y..area.y.saturating_add(area.height) {
24        let mut line = String::new();
25        for x in area.x..area.x.saturating_add(area.width) {
26            line.push_str(buffer[(x, y)].symbol());
27        }
28        out.push_str(line.trim_end_matches(' '));
29        if y + 1 < area.y.saturating_add(area.height) {
30            out.push('\n');
31        }
32    }
33
34    out
35}
36
37/// Paint a snapshot buffer onto the current frame
38///
39/// Clears the frame first, then copies cells from the snapshot.
40pub fn paint_snapshot(f: &mut Frame, snapshot: &Buffer) {
41    let screen = f.area();
42    f.render_widget(Clear, screen);
43
44    let snap_area = snapshot.area;
45    let x_end = screen
46        .x
47        .saturating_add(screen.width)
48        .min(snap_area.x.saturating_add(snap_area.width));
49    let y_end = screen
50        .y
51        .saturating_add(screen.height)
52        .min(snap_area.y.saturating_add(snap_area.height));
53
54    for y in screen.y..y_end {
55        for x in screen.x..x_end {
56            f.buffer_mut()[(x, y)] = snapshot[(x, y)].clone();
57        }
58    }
59}
60
61/// Dim a buffer by blending with a darker shade
62///
63/// `factor` ranges from 0.0 (no change) to 1.0 (fully dimmed)
64pub fn dim_buffer(buffer: &mut Buffer, factor: f32) {
65    let factor = factor.clamp(0.0, 1.0);
66    let dim_amount = (255.0 * factor) as u8;
67
68    for cell in buffer.content.iter_mut() {
69        if let ratatui::style::Color::Rgb(r, g, b) = cell.bg {
70            cell.bg = ratatui::style::Color::Rgb(
71                r.saturating_sub(dim_amount),
72                g.saturating_sub(dim_amount),
73                b.saturating_sub(dim_amount),
74            );
75        }
76    }
77}
78
79/// An item in a debug banner (status bar)
80#[derive(Clone)]
81pub struct BannerItem<'a> {
82    /// The key/label shown in a highlighted style
83    pub key: &'a str,
84    /// The description shown after the key
85    pub label: &'a str,
86    /// Style for the key
87    pub key_style: Style,
88}
89
90impl<'a> BannerItem<'a> {
91    /// Create a new banner item
92    pub fn new(key: &'a str, label: &'a str, key_style: Style) -> Self {
93        Self {
94            key,
95            label,
96            key_style,
97        }
98    }
99}
100
101/// A debug banner widget (status bar at bottom)
102///
103/// # Example
104///
105/// ```ignore
106/// let banner = DebugBanner::new()
107///     .title("DEBUG")
108///     .item(BannerItem::new("F12", "resume", key_style))
109///     .item(BannerItem::new("S", "state", key_style))
110///     .background(bg_style);
111///
112/// f.render_widget(banner, area);
113/// ```
114pub struct DebugBanner<'a> {
115    title: Option<&'a str>,
116    title_style: Style,
117    items: Vec<BannerItem<'a>>,
118    label_style: Style,
119    background: Style,
120}
121
122impl<'a> Default for DebugBanner<'a> {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128impl<'a> DebugBanner<'a> {
129    /// Create a new empty debug banner
130    pub fn new() -> Self {
131        Self {
132            title: None,
133            title_style: Style::default(),
134            items: Vec::new(),
135            label_style: Style::default(),
136            background: Style::default(),
137        }
138    }
139
140    /// Set the banner title (e.g., "DEBUG")
141    pub fn title(mut self, title: &'a str) -> Self {
142        self.title = Some(title);
143        self
144    }
145
146    /// Set the title style
147    pub fn title_style(mut self, style: Style) -> Self {
148        self.title_style = style;
149        self
150    }
151
152    /// Add an item to the banner
153    pub fn item(mut self, item: BannerItem<'a>) -> Self {
154        self.items.push(item);
155        self
156    }
157
158    /// Set the style for item labels
159    pub fn label_style(mut self, style: Style) -> Self {
160        self.label_style = style;
161        self
162    }
163
164    /// Set the background style
165    pub fn background(mut self, style: Style) -> Self {
166        self.background = style;
167        self
168    }
169}
170
171impl Widget for DebugBanner<'_> {
172    fn render(self, area: Rect, buf: &mut Buffer) {
173        if area.height == 0 || area.width == 0 {
174            return;
175        }
176
177        // Fill background
178        Block::default().style(self.background).render(area, buf);
179
180        let mut spans = Vec::new();
181
182        // Add title if present
183        if let Some(title) = self.title {
184            spans.push(Span::styled(format!(" {title} "), self.title_style));
185            spans.push(Span::raw(" "));
186        }
187
188        // Add items
189        for item in &self.items {
190            spans.push(Span::styled(format!(" {} ", item.key), item.key_style));
191            spans.push(Span::styled(format!(" {} ", item.label), self.label_style));
192        }
193
194        let line = Paragraph::new(Line::from(spans)).style(self.background);
195        line.render(area, buf);
196    }
197}
198
199/// Style configuration for debug table rendering
200#[derive(Clone)]
201pub struct DebugTableStyle {
202    /// Style for the header row
203    pub header: Style,
204    /// Style for section titles
205    pub section: Style,
206    /// Style for entry keys
207    pub key: Style,
208    /// Style for entry values
209    pub value: Style,
210    /// Alternating row styles (even, odd)
211    pub row_styles: (Style, Style),
212}
213
214impl Default for DebugTableStyle {
215    fn default() -> Self {
216        use super::config::DebugStyle;
217        Self {
218            header: Style::default()
219                .fg(DebugStyle::neon_cyan())
220                .add_modifier(Modifier::BOLD),
221            section: Style::default()
222                .fg(DebugStyle::neon_purple())
223                .add_modifier(Modifier::BOLD),
224            key: Style::default()
225                .fg(DebugStyle::neon_amber())
226                .add_modifier(Modifier::BOLD),
227            value: Style::default().fg(DebugStyle::text_primary()),
228            row_styles: (
229                Style::default().bg(DebugStyle::bg_panel()),
230                Style::default().bg(DebugStyle::bg_surface()),
231            ),
232        }
233    }
234}
235
236/// A debug table widget that renders a DebugTableOverlay
237pub struct DebugTableWidget<'a> {
238    table: &'a DebugTableOverlay,
239    style: DebugTableStyle,
240}
241
242impl<'a> DebugTableWidget<'a> {
243    /// Create a new debug table widget
244    pub fn new(table: &'a DebugTableOverlay) -> Self {
245        Self {
246            table,
247            style: DebugTableStyle::default(),
248        }
249    }
250
251    /// Set the style configuration
252    pub fn style(mut self, style: DebugTableStyle) -> Self {
253        self.style = style;
254        self
255    }
256}
257
258impl Widget for DebugTableWidget<'_> {
259    fn render(self, area: Rect, buf: &mut Buffer) {
260        if area.height < 2 || area.width < 10 {
261            return;
262        }
263
264        // Calculate column widths
265        let max_key_len = self
266            .table
267            .rows
268            .iter()
269            .filter_map(|row| match row {
270                DebugTableRow::Entry { key, .. } => Some(key.chars().count()),
271                DebugTableRow::Section(_) => None,
272            })
273            .max()
274            .unwrap_or(0) as u16;
275
276        let max_label = area.width.saturating_sub(8).max(10);
277        let label_width = max_key_len.saturating_add(2).clamp(12, 30).min(max_label);
278        let constraints = [Constraint::Length(label_width), Constraint::Min(0)];
279
280        // Build header
281        let header = Row::new(vec![
282            Cell::from("Field").style(self.style.header),
283            Cell::from("Value").style(self.style.header),
284        ]);
285
286        // Build rows
287        let rows: Vec<Row> = self
288            .table
289            .rows
290            .iter()
291            .enumerate()
292            .map(|(idx, row)| match row {
293                DebugTableRow::Section(title) => Row::new(vec![
294                    Cell::from(format!(" {title} ")).style(self.style.section),
295                    Cell::from(""),
296                ]),
297                DebugTableRow::Entry { key, value } => {
298                    let row_style = if idx % 2 == 0 {
299                        self.style.row_styles.0
300                    } else {
301                        self.style.row_styles.1
302                    };
303                    Row::new(vec![
304                        Cell::from(key.clone()).style(self.style.key),
305                        Cell::from(value.clone()).style(self.style.value),
306                    ])
307                    .style(row_style)
308                }
309            })
310            .collect();
311
312        let table = Table::new(rows, constraints)
313            .header(header)
314            .column_spacing(2);
315
316        table.render(area, buf);
317    }
318}
319
320/// A widget that renders a cell preview
321pub struct CellPreviewWidget<'a> {
322    preview: &'a CellPreview,
323    label_style: Style,
324    value_style: Style,
325}
326
327impl<'a> CellPreviewWidget<'a> {
328    /// Create a new cell preview widget with default neon styling
329    pub fn new(preview: &'a CellPreview) -> Self {
330        use super::config::DebugStyle;
331        Self {
332            preview,
333            label_style: Style::default().fg(DebugStyle::text_secondary()),
334            value_style: Style::default().fg(DebugStyle::text_primary()),
335        }
336    }
337
338    /// Set the style for labels (fg, bg, etc.)
339    pub fn label_style(mut self, style: Style) -> Self {
340        self.label_style = style;
341        self
342    }
343
344    /// Set the style for values
345    pub fn value_style(mut self, style: Style) -> Self {
346        self.value_style = style;
347        self
348    }
349}
350
351impl Widget for CellPreviewWidget<'_> {
352    fn render(self, area: Rect, buf: &mut Buffer) {
353        use super::config::DebugStyle;
354
355        if area.width < 20 || area.height < 1 {
356            return;
357        }
358
359        // Render the character with its actual style
360        let char_style = Style::default()
361            .fg(self.preview.fg)
362            .bg(self.preview.bg)
363            .add_modifier(self.preview.modifier);
364
365        // Format RGB values compactly
366        let fg_str = format_color_compact(self.preview.fg);
367        let bg_str = format_color_compact(self.preview.bg);
368        let mod_str = format_modifier_compact(self.preview.modifier);
369
370        // Character background highlight
371        let char_bg = Style::default().bg(DebugStyle::bg_surface());
372        let mod_style = Style::default().fg(DebugStyle::neon_purple());
373
374        // Single line: [char]  fg █ RGB  bg █ RGB  mod
375        let mut spans = vec![
376            Span::styled(" ", char_bg),
377            Span::styled(self.preview.symbol.clone(), char_style),
378            Span::styled(" ", char_bg),
379            Span::styled("  fg ", self.label_style),
380            Span::styled("█", Style::default().fg(self.preview.fg)),
381            Span::styled(format!(" {fg_str}"), self.value_style),
382            Span::styled("  bg ", self.label_style),
383            Span::styled("█", Style::default().fg(self.preview.bg)),
384            Span::styled(format!(" {bg_str}"), self.value_style),
385        ];
386
387        if !mod_str.is_empty() {
388            spans.push(Span::styled(format!("  {mod_str}"), mod_style));
389        }
390
391        let line = Paragraph::new(Line::from(spans));
392        line.render(area, buf);
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use ratatui::layout::Rect;
400
401    #[test]
402    fn test_buffer_to_text() {
403        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
404
405        // Write some text
406        buffer[(0, 0)].set_char('H');
407        buffer[(1, 0)].set_char('i');
408        buffer[(0, 1)].set_char('!');
409
410        let text = buffer_to_text(&buffer);
411        let lines: Vec<&str> = text.lines().collect();
412
413        assert_eq!(lines[0], "Hi");
414        assert_eq!(lines[1], "!");
415    }
416
417    #[test]
418    fn test_debug_banner() {
419        let banner =
420            DebugBanner::new()
421                .title("TEST")
422                .item(BannerItem::new("F1", "help", Style::default()));
423
424        let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 1));
425        banner.render(Rect::new(0, 0, 40, 1), &mut buffer);
426
427        let text = buffer_to_text(&buffer);
428        assert!(text.contains("TEST"));
429        assert!(text.contains("F1"));
430        assert!(text.contains("help"));
431    }
432}