1use crate::align::HorizontalAlign;
10use crate::cells::{cell_len, truncate};
11use crate::console::{Console, ConsoleOptions};
12use crate::padding::join_rows;
13use crate::protocol::Renderable;
14use crate::r#box::{Box as BoxSet, ROUNDED};
15use crate::segment::Segment;
16use crate::style::Style;
17
18pub struct Panel {
20 child: Box<dyn Renderable>,
21 box_set: BoxSet,
22 title: Option<String>,
23 title_align: HorizontalAlign,
24 subtitle: Option<String>,
25 subtitle_align: HorizontalAlign,
26 padding: (usize, usize, usize, usize),
27 border_style: Style,
28 style: Style,
29}
30
31impl Panel {
32 pub fn new(child: Box<dyn Renderable>) -> Self {
34 Panel {
35 child,
36 box_set: ROUNDED,
37 title: None,
38 title_align: HorizontalAlign::Center,
39 subtitle: None,
40 subtitle_align: HorizontalAlign::Center,
41 padding: (0, 1, 0, 1),
42 border_style: Style::new(),
43 style: Style::new(),
44 }
45 }
46
47 pub fn title(mut self, title: impl Into<String>) -> Self {
49 self.title = Some(title.into());
50 self
51 }
52
53 pub fn title_align(mut self, align: HorizontalAlign) -> Self {
55 self.title_align = align;
56 self
57 }
58
59 pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
61 self.subtitle = Some(subtitle.into());
62 self
63 }
64
65 pub fn subtitle_align(mut self, align: HorizontalAlign) -> Self {
67 self.subtitle_align = align;
68 self
69 }
70
71 pub fn box_set(mut self, box_set: BoxSet) -> Self {
73 self.box_set = box_set;
74 self
75 }
76
77 pub fn padding(mut self, padding: (usize, usize, usize, usize)) -> Self {
79 self.padding = padding;
80 self
81 }
82
83 pub fn border_style(mut self, style: Style) -> Self {
85 self.border_style = style;
86 self
87 }
88
89 fn border_line(
91 &self,
92 inner_width: usize,
93 left_corner: char,
94 fill_char: char,
95 right_corner: char,
96 label: Option<&String>,
97 align: HorizontalAlign,
98 ) -> String {
99 let mut border = String::new();
100 border.push(left_corner);
101 match label {
102 None => border.extend(std::iter::repeat_n(fill_char, inner_width)),
103 Some(label) => {
104 let label = truncate(label, inner_width.saturating_sub(2));
106 let padded = format!(" {label} ");
107 let fill = inner_width.saturating_sub(cell_len(&padded));
108 let (left, right) = match align {
109 HorizontalAlign::Center => (fill / 2, fill - fill / 2),
110 HorizontalAlign::Left => (1.min(fill), fill.saturating_sub(1)),
112 HorizontalAlign::Right => (fill.saturating_sub(1), 1.min(fill)),
113 };
114 border.extend(std::iter::repeat_n(fill_char, left));
115 border.push_str(&padded);
116 border.extend(std::iter::repeat_n(fill_char, right));
117 }
118 }
119 border.push(right_corner);
120 border
121 }
122}
123
124impl Renderable for Panel {
125 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
126 let width = options.max_width;
127 let box_set = self.box_set.substitute(
129 console.legacy_windows(),
130 console.safe_box(),
131 console.ascii_only(),
132 );
133 let inner_width = width.saturating_sub(2);
134 let (pt, pr, pb, pl) = self.padding;
135 let child_width = inner_width.saturating_sub(pl).saturating_sub(pr);
136
137 let mut child_options = options.update_width(child_width);
138 child_options.height = options.height.map(|h| h.saturating_sub(2 + pt + pb));
143 let child_lines = console.render_lines(self.child.as_ref(), &child_options, true);
144
145 let border = Some(self.border_style.clone());
146 let inner_style = Some(self.style.clone());
147 let left_border = || Segment::new(box_set.mid_left.to_string(), border.clone());
148 let right_border = || Segment::new(box_set.mid_right.to_string(), border.clone());
149 let blank_inner = || Segment::new(" ".repeat(inner_width), inner_style.clone());
150
151 let mut rows: Vec<Vec<Segment>> = Vec::new();
152
153 rows.push(vec![Segment::new(
155 self.border_line(
156 inner_width,
157 box_set.top_left,
158 box_set.top,
159 box_set.top_right,
160 self.title.as_ref(),
161 self.title_align,
162 ),
163 border.clone(),
164 )]);
165
166 for _ in 0..pt {
168 rows.push(vec![left_border(), blank_inner(), right_border()]);
169 }
170
171 for line in child_lines {
173 let mut row = vec![left_border()];
174 if pl > 0 {
175 row.push(Segment::new(" ".repeat(pl), inner_style.clone()));
176 }
177 row.extend(line);
178 if pr > 0 {
179 row.push(Segment::new(" ".repeat(pr), inner_style.clone()));
180 }
181 row.push(right_border());
182 rows.push(row);
183 }
184
185 for _ in 0..pb {
187 rows.push(vec![left_border(), blank_inner(), right_border()]);
188 }
189
190 rows.push(vec![Segment::new(
192 self.border_line(
193 inner_width,
194 box_set.bottom_left,
195 box_set.bottom,
196 box_set.bottom_right,
197 self.subtitle.as_ref(),
198 self.subtitle_align,
199 ),
200 border.clone(),
201 )]);
202
203 join_rows(rows)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::r#box::SQUARE;
211 use crate::text::Text;
212
213 fn console() -> Console {
214 Console::builder()
215 .force_terminal(true)
216 .color_system(Some(crate::color::ColorSystem::Truecolor))
217 .width(20)
218 .build()
219 }
220
221 #[test]
222 fn plain_panel() {
223 let out = console().render_export(&Panel::new(Box::new(Text::new("hello"))));
224 assert_eq!(
225 out,
226 "╭──────────────────╮\n│ hello │\n╰──────────────────╯\n"
227 );
228 }
229
230 #[test]
231 fn titled_panel() {
232 let out = console().render_export(&Panel::new(Box::new(Text::new("hello"))).title("T"));
233 assert_eq!(
234 out,
235 "╭─────── T ────────╮\n│ hello │\n╰──────────────────╯\n"
236 );
237 }
238
239 #[test]
240 fn square_box() {
241 let out = console().render_export(&Panel::new(Box::new(Text::new("hi"))).box_set(SQUARE));
242 assert_eq!(
243 out,
244 "┌──────────────────┐\n│ hi │\n└──────────────────┘\n"
245 );
246 }
247
248 #[test]
249 fn legacy_windows_substitutes_rounded_to_square() {
250 let legacy = Console::builder()
253 .force_terminal(true)
254 .color_system(Some(crate::color::ColorSystem::Truecolor))
255 .width(12)
256 .no_color(false)
257 .legacy_windows(true)
258 .build();
259 let out = legacy.render_export(&Panel::new(Box::new(Text::new("hi"))));
260 assert_eq!(out, "┌──────────┐\n│ hi │\n└──────────┘\n");
261 }
262}