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::cell::Cell;
19use ftui_render::frame::Frame;
20use ftui_style::Style;
21use std::fmt::Debug;
22
23/// Pretty-printing widget that renders a `Debug` representation.
24///
25/// Wraps any `Debug` value and renders it line-by-line into a frame,
26/// using either compact (`{:?}`) or pretty (`{:#?}`) formatting.
27pub struct Pretty<'a, T: Debug + ?Sized> {
28    value: &'a T,
29    compact: bool,
30    style: Style,
31}
32
33impl<'a, T: Debug + ?Sized> Pretty<'a, T> {
34    /// Create a new pretty widget for a value.
35    #[must_use]
36    pub fn new(value: &'a T) -> Self {
37        Self {
38            value,
39            compact: false,
40            style: Style::default(),
41        }
42    }
43
44    /// Use compact formatting (`{:?}`) instead of pretty (`{:#?}`).
45    #[must_use]
46    pub fn with_compact(mut self, compact: bool) -> Self {
47        self.compact = compact;
48        self
49    }
50
51    /// Set the text style.
52    #[must_use]
53    pub fn with_style(mut self, style: Style) -> Self {
54        self.style = style;
55        self
56    }
57
58    /// Get the formatted text as a string.
59    #[must_use]
60    pub fn formatted_text(&self) -> String {
61        if self.compact {
62            format!("{:?}", self.value)
63        } else {
64            format!("{:#?}", self.value)
65        }
66    }
67}
68
69impl<T: Debug + ?Sized> Widget for Pretty<'_, T> {
70    fn render(&self, area: Rect, frame: &mut Frame) {
71        if area.width == 0 || area.height == 0 {
72            return;
73        }
74
75        let deg = frame.buffer.degradation;
76        if !deg.render_content() {
77            frame.buffer.fill(area, Cell::default());
78            return;
79        }
80
81        let style = if deg.apply_styling() {
82            self.style
83        } else {
84            Style::default()
85        };
86
87        let text = self.formatted_text();
88        let max_x = area.right();
89        frame.buffer.fill(area, Cell::default());
90
91        for (row_idx, line) in text.lines().enumerate() {
92            if row_idx >= area.height as usize {
93                break;
94            }
95            let y = area.y.saturating_add(row_idx as u16);
96            draw_text_span(frame, area.x, y, line, style, max_x);
97        }
98    }
99
100    fn is_essential(&self) -> bool {
101        false
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use ftui_render::budget::DegradationLevel;
109    use ftui_render::cell::PackedRgba;
110    use ftui_render::frame::Frame;
111    use ftui_render::grapheme_pool::GraphemePool;
112
113    #[test]
114    fn format_simple_value() {
115        let widget = Pretty::new(&42i32);
116        assert_eq!(widget.formatted_text(), "42");
117    }
118
119    #[test]
120    fn format_vec() {
121        let data = vec![1, 2, 3];
122        let widget = Pretty::new(&data);
123        let text = widget.formatted_text();
124        assert!(text.contains("1"));
125        assert!(text.contains("2"));
126        assert!(text.contains("3"));
127    }
128
129    #[test]
130    fn format_compact() {
131        let data = vec![1, 2, 3];
132        let compact = Pretty::new(&data).with_compact(true);
133        let text = compact.formatted_text();
134        // Compact is single-line
135        assert_eq!(text.lines().count(), 1);
136    }
137
138    #[test]
139    fn format_pretty() {
140        let data = vec![1, 2, 3];
141        let pretty = Pretty::new(&data).with_compact(false);
142        let text = pretty.formatted_text();
143        // Pretty is multi-line
144        assert!(text.lines().count() > 1);
145    }
146
147    #[derive(Debug)]
148    #[allow(dead_code)]
149    struct TestStruct {
150        name: String,
151        value: i32,
152    }
153
154    #[test]
155    fn format_struct() {
156        let s = TestStruct {
157            name: "hello".to_string(),
158            value: 42,
159        };
160        let widget = Pretty::new(&s);
161        let text = widget.formatted_text();
162        assert!(text.contains("name"));
163        assert!(text.contains("hello"));
164        assert!(text.contains("42"));
165    }
166
167    #[test]
168    fn format_string() {
169        let widget = Pretty::new("hello world");
170        let text = widget.formatted_text();
171        assert!(text.contains("hello world"));
172    }
173
174    #[test]
175    fn render_basic() {
176        let data = vec![1, 2, 3];
177        let widget = Pretty::new(&data);
178
179        let mut pool = GraphemePool::new();
180        let mut frame = Frame::new(40, 10, &mut pool);
181        let area = Rect::new(0, 0, 40, 10);
182        widget.render(area, &mut frame);
183
184        // First line starts with '['
185        let cell = frame.buffer.get(0, 0).unwrap();
186        assert_eq!(cell.content.as_char(), Some('['));
187    }
188
189    #[test]
190    fn render_no_styling_drops_configured_style() {
191        let widget =
192            Pretty::new(&42).with_style(Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold());
193
194        let mut pool = GraphemePool::new();
195        let mut frame = Frame::new(20, 1, &mut pool);
196        frame.buffer.degradation = DegradationLevel::NoStyling;
197        let area = Rect::new(0, 0, 20, 1);
198        widget.render(area, &mut frame);
199
200        let cell = frame.buffer.get(0, 0).unwrap();
201        let default_cell = ftui_render::cell::Cell::from_char('4');
202        assert_eq!(cell.content.as_char(), Some('4'));
203        assert_eq!(cell.fg, default_cell.fg);
204        assert_eq!(cell.bg, default_cell.bg);
205        assert_eq!(cell.attrs, default_cell.attrs);
206    }
207
208    #[test]
209    fn render_skeleton_is_noop() {
210        let widget = Pretty::new(&42);
211
212        let mut pool = GraphemePool::new();
213        let mut frame = Frame::new(20, 1, &mut pool);
214        let area = Rect::new(0, 0, 20, 1);
215        widget.render(area, &mut frame);
216
217        frame.buffer.degradation = DegradationLevel::Skeleton;
218        widget.render(area, &mut frame);
219
220        let cell = frame.buffer.get(0, 0).unwrap();
221        let default_cell = ftui_render::cell::Cell::default();
222        assert_eq!(cell.content, default_cell.content);
223        assert_eq!(cell.fg, default_cell.fg);
224        assert_eq!(cell.bg, default_cell.bg);
225        assert_eq!(cell.attrs, default_cell.attrs);
226    }
227
228    #[test]
229    fn render_zero_area() {
230        let widget = Pretty::new(&42);
231        let mut pool = GraphemePool::new();
232        let mut frame = Frame::new(40, 10, &mut pool);
233        widget.render(Rect::new(0, 0, 0, 0), &mut frame); // No panic
234    }
235
236    #[test]
237    fn render_truncated_height() {
238        let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
239        let widget = Pretty::new(&data);
240        let mut pool = GraphemePool::new();
241        let mut frame = Frame::new(40, 3, &mut pool);
242        let area = Rect::new(0, 0, 40, 3);
243        widget.render(area, &mut frame); // Only 3 lines, no panic
244    }
245
246    #[test]
247    fn render_shorter_line_clears_stale_suffix() {
248        let long_value = vec![1000];
249        let short_value = vec![1];
250        let long = Pretty::new(&long_value).with_compact(true);
251        let short = Pretty::new(&short_value).with_compact(true);
252        let mut pool = GraphemePool::new();
253        let mut frame = Frame::new(20, 2, &mut pool);
254        let area = Rect::new(0, 0, 20, 2);
255
256        long.render(area, &mut frame);
257        short.render(area, &mut frame);
258
259        assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('['));
260        assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('1'));
261        assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some(']'));
262        assert!(frame.buffer.get(3, 0).unwrap().is_empty());
263    }
264
265    #[test]
266    fn render_fewer_lines_clears_stale_rows() {
267        let long_value = vec![1, 2, 3];
268        let long = Pretty::new(&long_value);
269        let short = Pretty::new(&42);
270        let mut pool = GraphemePool::new();
271        let mut frame = Frame::new(20, 6, &mut pool);
272        let area = Rect::new(0, 0, 20, 6);
273
274        long.render(area, &mut frame);
275        short.render(area, &mut frame);
276
277        for x in 0..20u16 {
278            assert!(frame.buffer.get(x, 1).unwrap().is_empty());
279        }
280    }
281
282    #[test]
283    fn is_not_essential() {
284        let widget = Pretty::new(&42);
285        assert!(!widget.is_essential());
286    }
287
288    #[test]
289    fn format_empty_vec() {
290        let data: Vec<i32> = vec![];
291        let widget = Pretty::new(&data);
292        assert_eq!(widget.formatted_text(), "[]");
293    }
294
295    #[test]
296    fn format_nested() {
297        let data = vec![vec![1, 2], vec![3, 4]];
298        let widget = Pretty::new(&data);
299        let text = widget.formatted_text();
300        assert!(text.lines().count() > 1);
301    }
302
303    #[test]
304    fn format_option() {
305        let some: Option<i32> = Some(42);
306        let none: Option<i32> = None;
307        assert!(Pretty::new(&some).formatted_text().contains("42"));
308        assert!(Pretty::new(&none).formatted_text().contains("None"));
309    }
310}