1use 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
23pub 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 #[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 #[must_use]
46 pub fn with_compact(mut self, compact: bool) -> Self {
47 self.compact = compact;
48 self
49 }
50
51 #[must_use]
53 pub fn with_style(mut self, style: Style) -> Self {
54 self.style = style;
55 self
56 }
57
58 #[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 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 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 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); }
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); }
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}