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 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
236pub struct DebugTableWidget<'a> {
238 table: &'a DebugTableOverlay,
239 style: DebugTableStyle,
240}
241
242impl<'a> DebugTableWidget<'a> {
243 pub fn new(table: &'a DebugTableOverlay) -> Self {
245 Self {
246 table,
247 style: DebugTableStyle::default(),
248 }
249 }
250
251 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 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 let header = Row::new(vec![
282 Cell::from("Field").style(self.style.header),
283 Cell::from("Value").style(self.style.header),
284 ]);
285
286 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
320pub struct CellPreviewWidget<'a> {
322 preview: &'a CellPreview,
323 label_style: Style,
324 value_style: Style,
325}
326
327impl<'a> CellPreviewWidget<'a> {
328 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 pub fn label_style(mut self, style: Style) -> Self {
340 self.label_style = style;
341 self
342 }
343
344 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 let char_style = Style::default()
361 .fg(self.preview.fg)
362 .bg(self.preview.bg)
363 .add_modifier(self.preview.modifier);
364
365 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 let char_bg = Style::default().bg(DebugStyle::bg_surface());
372 let mod_style = Style::default().fg(DebugStyle::neon_purple());
373
374 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 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}