rusticity_term/ui/
summary.rs1use super::{
3 calculate_dynamic_height, labeled_field, render_fields_with_dynamic_columns, titled_block,
4};
5use ratatui::prelude::*;
6
7pub trait SummaryField {
8 fn summary_line(&self) -> Line<'_>;
9}
10impl SummaryField for Line<'_> {
11 fn summary_line(&self) -> Line<'_> {
12 self.clone()
13 }
14}
15impl SummaryField for (&str, String) {
16 fn summary_line(&self) -> Line<'_> {
17 labeled_field(self.0, self.1.clone())
18 }
19}
20
21pub struct Summary<'a> {
22 title: &'a str,
23 lines: Vec<Line<'a>>,
24}
25impl<'a> Summary<'a> {
26 pub fn new(title: &'a str, lines: Vec<Line<'a>>) -> Self {
27 Self { title, lines }
28 }
29 pub fn height(&self, width: u16) -> u16 {
31 calculate_dynamic_height(&self.lines, width.saturating_sub(2)).saturating_add(2)
32 }
33 pub fn render(self, frame: &mut Frame, area: Rect) -> Rect {
35 let area = Rect {
36 height: self.height(area.width).min(area.height),
37 ..area
38 };
39 self.render_fill(frame, area);
40 area
41 }
42 pub fn render_fill(self, frame: &mut Frame, area: Rect) {
44 let block = titled_block(self.title);
45 let inner = block.inner(area);
46 frame.render_widget(block, area);
47 render_fields_with_dynamic_columns(frame, inner, self.lines);
48 }
49}
50
51pub fn render_summary<F: SummaryField>(
52 frame: &mut Frame,
53 area: Rect,
54 title: &str,
55 fields: &[F],
56) -> Rect {
57 Summary::new(
58 title,
59 fields.iter().map(SummaryField::summary_line).collect(),
60 )
61 .render(frame, area)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use ratatui::{backend::TestBackend, Terminal};
68
69 #[test]
70 fn summary_fits_content_and_preserves_styles_and_unicode() {
71 for width in [20, 45, 180] {
72 let lines = vec![
73 Line::from(vec![
74 Span::raw("State: "),
75 Span::styled("✅ Available", Style::default().fg(Color::Green)),
76 ]),
77 Line::raw("Name: 資源"),
78 Line::raw("Owner: test"),
79 ];
80 let height = Summary::new("General", lines.clone()).height(width);
81 let mut terminal = Terminal::new(TestBackend::new(width, 20)).unwrap();
82 terminal
83 .draw(|frame| {
84 let used = render_summary(frame, frame.area(), "General", &lines);
85 assert_eq!(used.height, height);
86 })
87 .unwrap();
88 let buffer = terminal.backend().buffer();
89 assert_eq!(buffer[(0, height - 1)].symbol(), "╰");
90 assert_eq!(buffer[(0, height)].symbol(), " ");
91 assert!(buffer
92 .content()
93 .iter()
94 .any(|c| c.symbol().contains('✅') && c.fg == Color::Green));
95 }
96 }
97
98 #[test]
99 fn summary_accepts_plain_fields_and_clamps_tiny_areas() {
100 let mut terminal = Terminal::new(TestBackend::new(80, 10)).unwrap();
101 terminal
102 .draw(|frame| {
103 let fields = [("Name", "example".to_string())];
104 let used = render_summary(frame, Rect::new(3, 2, 40, 8), "Summary", &fields);
105 assert_eq!(used, Rect::new(3, 2, 40, 3));
106 for area in [Rect::new(0, 0, 0, 0), Rect::new(0, 0, 1, 1)] {
107 let used = render_summary(frame, area, "Tiny", &fields);
108 assert!(used.height <= area.height);
109 }
110 })
111 .unwrap();
112 }
113}