1use 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
16pub 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
37pub 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
61pub 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#[derive(Clone)]
81pub struct BannerItem<'a> {
82 pub key: &'a str,
84 pub label: &'a str,
86 pub key_style: Style,
88}
89
90impl<'a> BannerItem<'a> {
91 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
101pub 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 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 pub fn title(mut self, title: &'a str) -> Self {
142 self.title = Some(title);
143 self
144 }
145
146 pub fn title_style(mut self, style: Style) -> Self {
148 self.title_style = style;
149 self
150 }
151
152 pub fn item(mut self, item: BannerItem<'a>) -> Self {
154 self.items.push(item);
155 self
156 }
157
158 pub fn label_style(mut self, style: Style) -> Self {
160 self.label_style = style;
161 self
162 }
163
164 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 Block::default().style(self.background).render(area, buf);
179
180 let mut spans = Vec::new();
181
182 if let Some(title) = self.title {
184 spans.push(Span::styled(format!(" {title} "), self.title_style));
185 spans.push(Span::raw(" "));
186 }
187
188 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#[derive(Clone)]
201pub struct DebugTableStyle {
202 pub header: Style,
204 pub section: Style,
206 pub key: Style,
208 pub value: Style,
210 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
226pub struct DebugTableWidget<'a> {
228 table: &'a DebugTableOverlay,
229 style: DebugTableStyle,
230}
231
232impl<'a> DebugTableWidget<'a> {
233 pub fn new(table: &'a DebugTableOverlay) -> Self {
235 Self {
236 table,
237 style: DebugTableStyle::default(),
238 }
239 }
240
241 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 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 let header = Row::new(vec![
272 Cell::from("Field").style(self.style.header),
273 Cell::from("Value").style(self.style.header),
274 ]);
275
276 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
310pub struct CellPreviewWidget<'a> {
312 preview: &'a CellPreview,
313 label_style: Style,
314 value_style: Style,
315}
316
317impl<'a> CellPreviewWidget<'a> {
318 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 pub fn label_style(mut self, style: Style) -> Self {
329 self.label_style = style;
330 self
331 }
332
333 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 let char_style = Style::default()
348 .fg(self.preview.fg)
349 .bg(self.preview.bg)
350 .add_modifier(self.preview.modifier);
351
352 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 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 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}