Skip to main content

ftui_widgets/
pretty.rs

1//! Pretty-printing widget for Rust values.
2//!
3//! Renders a value's [`Debug`] representation with optional wrapping and
4//! configurable formatting into a [`Frame`].
5//!
6//! # Example
7//!
8//! ```
9//! use ftui_widgets::pretty::Pretty;
10//!
11//! let data = vec![1, 2, 3];
12//! let widget = Pretty::new(&data);
13//! assert!(!widget.formatted_text().is_empty());
14//! ```
15
16use crate::{Widget, draw_text_span};
17use ftui_core::geometry::Rect;
18use ftui_render::frame::Frame;
19use ftui_style::Style;
20use std::fmt::Debug;
21
22/// Pretty-printing widget that renders a `Debug` representation.
23///
24/// Wraps any `Debug` value and renders it line-by-line into a frame,
25/// using either compact (`{:?}`) or pretty (`{:#?}`) formatting.
26pub struct Pretty<'a, T: Debug + ?Sized> {
27    value: &'a T,
28    compact: bool,
29    style: Style,
30}
31
32impl<'a, T: Debug + ?Sized> Pretty<'a, T> {
33    /// Create a new pretty widget for a value.
34    #[must_use]
35    pub fn new(value: &'a T) -> Self {
36        Self {
37            value,
38            compact: false,
39            style: Style::default(),
40        }
41    }
42
43    /// Use compact formatting (`{:?}`) instead of pretty (`{:#?}`).
44    #[must_use]
45    pub fn with_compact(mut self, compact: bool) -> Self {
46        self.compact = compact;
47        self
48    }
49
50    /// Set the text style.
51    #[must_use]
52    pub fn with_style(mut self, style: Style) -> Self {
53        self.style = style;
54        self
55    }
56
57    /// Get the formatted text as a string.
58    #[must_use]
59    pub fn formatted_text(&self) -> String {
60        if self.compact {
61            format!("{:?}", self.value)
62        } else {
63            format!("{:#?}", self.value)
64        }
65    }
66}
67
68impl<T: Debug + ?Sized> Widget for Pretty<'_, T> {
69    fn render(&self, area: Rect, frame: &mut Frame) {
70        if area.width == 0 || area.height == 0 {
71            return;
72        }
73
74        let text = self.formatted_text();
75        let max_x = area.right();
76
77        for (row_idx, line) in text.lines().enumerate() {
78            if row_idx >= area.height as usize {
79                break;
80            }
81            let y = area.y.saturating_add(row_idx as u16);
82            draw_text_span(frame, area.x, y, line, self.style, max_x);
83        }
84    }
85
86    fn is_essential(&self) -> bool {
87        false
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use ftui_render::frame::Frame;
95    use ftui_render::grapheme_pool::GraphemePool;
96
97    #[test]
98    fn format_simple_value() {
99        let widget = Pretty::new(&42i32);
100        assert_eq!(widget.formatted_text(), "42");
101    }
102
103    #[test]
104    fn format_vec() {
105        let data = vec![1, 2, 3];
106        let widget = Pretty::new(&data);
107        let text = widget.formatted_text();
108        assert!(text.contains("1"));
109        assert!(text.contains("2"));
110        assert!(text.contains("3"));
111    }
112
113    #[test]
114    fn format_compact() {
115        let data = vec![1, 2, 3];
116        let compact = Pretty::new(&data).with_compact(true);
117        let text = compact.formatted_text();
118        // Compact is single-line
119        assert_eq!(text.lines().count(), 1);
120    }
121
122    #[test]
123    fn format_pretty() {
124        let data = vec![1, 2, 3];
125        let pretty = Pretty::new(&data).with_compact(false);
126        let text = pretty.formatted_text();
127        // Pretty is multi-line
128        assert!(text.lines().count() > 1);
129    }
130
131    #[derive(Debug)]
132    #[allow(dead_code)]
133    struct TestStruct {
134        name: String,
135        value: i32,
136    }
137
138    #[test]
139    fn format_struct() {
140        let s = TestStruct {
141            name: "hello".to_string(),
142            value: 42,
143        };
144        let widget = Pretty::new(&s);
145        let text = widget.formatted_text();
146        assert!(text.contains("name"));
147        assert!(text.contains("hello"));
148        assert!(text.contains("42"));
149    }
150
151    #[test]
152    fn format_string() {
153        let widget = Pretty::new("hello world");
154        let text = widget.formatted_text();
155        assert!(text.contains("hello world"));
156    }
157
158    #[test]
159    fn render_basic() {
160        let data = vec![1, 2, 3];
161        let widget = Pretty::new(&data);
162
163        let mut pool = GraphemePool::new();
164        let mut frame = Frame::new(40, 10, &mut pool);
165        let area = Rect::new(0, 0, 40, 10);
166        widget.render(area, &mut frame);
167
168        // First line starts with '['
169        let cell = frame.buffer.get(0, 0).unwrap();
170        assert_eq!(cell.content.as_char(), Some('['));
171    }
172
173    #[test]
174    fn render_zero_area() {
175        let widget = Pretty::new(&42);
176        let mut pool = GraphemePool::new();
177        let mut frame = Frame::new(40, 10, &mut pool);
178        widget.render(Rect::new(0, 0, 0, 0), &mut frame); // No panic
179    }
180
181    #[test]
182    fn render_truncated_height() {
183        let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
184        let widget = Pretty::new(&data);
185        let mut pool = GraphemePool::new();
186        let mut frame = Frame::new(40, 3, &mut pool);
187        let area = Rect::new(0, 0, 40, 3);
188        widget.render(area, &mut frame); // Only 3 lines, no panic
189    }
190
191    #[test]
192    fn is_not_essential() {
193        let widget = Pretty::new(&42);
194        assert!(!widget.is_essential());
195    }
196
197    #[test]
198    fn format_empty_vec() {
199        let data: Vec<i32> = vec![];
200        let widget = Pretty::new(&data);
201        assert_eq!(widget.formatted_text(), "[]");
202    }
203
204    #[test]
205    fn format_nested() {
206        let data = vec![vec![1, 2], vec![3, 4]];
207        let widget = Pretty::new(&data);
208        let text = widget.formatted_text();
209        assert!(text.lines().count() > 1);
210    }
211
212    #[test]
213    fn format_option() {
214        let some: Option<i32> = Some(42);
215        let none: Option<i32> = None;
216        assert!(Pretty::new(&some).formatted_text().contains("42"));
217        assert!(Pretty::new(&none).formatted_text().contains("None"));
218    }
219}