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        Self {
217            header: Style::default().add_modifier(Modifier::BOLD),
218            section: Style::default().add_modifier(Modifier::BOLD),
219            key: Style::default().add_modifier(Modifier::BOLD),
220            value: Style::default(),
221            row_styles: (Style::default(), Style::default()),
222        }
223    }
224}
225
226/// A debug table widget that renders a DebugTableOverlay
227pub struct DebugTableWidget<'a> {
228    table: &'a DebugTableOverlay,
229    style: DebugTableStyle,
230}
231
232impl<'a> DebugTableWidget<'a> {
233    /// Create a new debug table widget
234    pub fn new(table: &'a DebugTableOverlay) -> Self {
235        Self {
236            table,
237            style: DebugTableStyle::default(),
238        }
239    }
240
241    /// Set the style configuration
242    pub fn style(mut self, style: DebugTableStyle) -> Self {
243        self.style = style;
244        self
245    }
246}
247
248impl Widget for DebugTableWidget<'_> {
249    fn render(self, area: Rect, buf: &mut Buffer) {
250        if area.height < 2 || area.width < 10 {
251            return;
252        }
253
254        // Calculate column widths
255        let max_key_len = self
256            .table
257            .rows
258            .iter()
259            .filter_map(|row| match row {
260                DebugTableRow::Entry { key, .. } => Some(key.chars().count()),
261                DebugTableRow::Section(_) => None,
262            })
263            .max()
264            .unwrap_or(0) as u16;
265
266        let max_label = area.width.saturating_sub(8).max(10);
267        let label_width = max_key_len.saturating_add(2).clamp(12, 30).min(max_label);
268        let constraints = [Constraint::Length(label_width), Constraint::Min(0)];
269
270        // Build header
271        let header = Row::new(vec![
272            Cell::from("Field").style(self.style.header),
273            Cell::from("Value").style(self.style.header),
274        ]);
275
276        // Build rows
277        let rows: Vec<Row> = self
278            .table
279            .rows
280            .iter()
281            .enumerate()
282            .map(|(idx, row)| match row {
283                DebugTableRow::Section(title) => Row::new(vec![
284                    Cell::from(format!(" {title} ")).style(self.style.section),
285                    Cell::from(""),
286                ]),
287                DebugTableRow::Entry { key, value } => {
288                    let row_style = if idx % 2 == 0 {
289                        self.style.row_styles.0
290                    } else {
291                        self.style.row_styles.1
292                    };
293                    Row::new(vec![
294                        Cell::from(key.clone()).style(self.style.key),
295                        Cell::from(value.clone()).style(self.style.value),
296                    ])
297                    .style(row_style)
298                }
299            })
300            .collect();
301
302        let table = Table::new(rows, constraints)
303            .header(header)
304            .column_spacing(2);
305
306        table.render(area, buf);
307    }
308}
309
310/// A widget that renders a cell preview
311pub struct CellPreviewWidget<'a> {
312    preview: &'a CellPreview,
313    label_style: Style,
314    value_style: Style,
315}
316
317impl<'a> CellPreviewWidget<'a> {
318    /// Create a new cell preview widget
319    pub fn new(preview: &'a CellPreview) -> Self {
320        Self {
321            preview,
322            label_style: Style::default(),
323            value_style: Style::default(),
324        }
325    }
326
327    /// Set the style for labels (fg, bg, etc.)
328    pub fn label_style(mut self, style: Style) -> Self {
329        self.label_style = style;
330        self
331    }
332
333    /// Set the style for values
334    pub fn value_style(mut self, style: Style) -> Self {
335        self.value_style = style;
336        self
337    }
338}
339
340impl Widget for CellPreviewWidget<'_> {
341    fn render(self, area: Rect, buf: &mut Buffer) {
342        if area.width < 20 || area.height < 1 {
343            return;
344        }
345
346        // Render the character with its actual style
347        let char_style = Style::default()
348            .fg(self.preview.fg)
349            .bg(self.preview.bg)
350            .add_modifier(self.preview.modifier);
351
352        // Format RGB values compactly
353        let fg_str = format_color_compact(self.preview.fg);
354        let bg_str = format_color_compact(self.preview.bg);
355        let mod_str = format_modifier_compact(self.preview.modifier);
356
357        // Single line: [char]  fg: RGB  bg: RGB  mod
358        let mut spans = vec![
359            Span::raw(" "),
360            Span::styled(self.preview.symbol.clone(), char_style),
361            Span::raw("  "),
362            Span::styled("fg ", self.label_style),
363            Span::styled("█", Style::default().fg(self.preview.fg)),
364            Span::styled(format!(" {fg_str}"), self.value_style),
365            Span::styled("  bg ", self.label_style),
366            Span::styled("█", Style::default().fg(self.preview.bg)),
367            Span::styled(format!(" {bg_str}"), self.value_style),
368        ];
369
370        if !mod_str.is_empty() {
371            spans.push(Span::styled(format!("  mod {mod_str}"), self.value_style));
372        }
373
374        let line = Paragraph::new(Line::from(spans));
375        line.render(area, buf);
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use ratatui::layout::Rect;
383
384    #[test]
385    fn test_buffer_to_text() {
386        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
387
388        // Write some text
389        buffer[(0, 0)].set_char('H');
390        buffer[(1, 0)].set_char('i');
391        buffer[(0, 1)].set_char('!');
392
393        let text = buffer_to_text(&buffer);
394        let lines: Vec<&str> = text.lines().collect();
395
396        assert_eq!(lines[0], "Hi");
397        assert_eq!(lines[1], "!");
398    }
399
400    #[test]
401    fn test_debug_banner() {
402        let banner =
403            DebugBanner::new()
404                .title("TEST")
405                .item(BannerItem::new("F1", "help", Style::default()));
406
407        let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 1));
408        banner.render(Rect::new(0, 0, 40, 1), &mut buffer);
409
410        let text = buffer_to_text(&buffer);
411        assert!(text.contains("TEST"));
412        assert!(text.contains("F1"));
413        assert!(text.contains("help"));
414    }
415}