1use crate::align::AlignMethod;
4use crate::box_drawing::{get_safe_box, BoxStyle, BOX_ROUNDED};
5use crate::console::{ConsoleOptions, DynRenderable, RenderResult, Renderable};
6use crate::segment::Segment;
7use crate::style::Style;
8
9#[derive(Clone)]
15pub struct Panel {
16 pub renderable: DynRenderable,
18 pub box_style: BoxStyle,
20 pub title: Option<String>,
22 pub title_align: AlignMethod,
24 pub subtitle: Option<String>,
26 pub subtitle_align: AlignMethod,
28 pub expand: bool,
30 pub style: Style,
32 pub border_style: Style,
34 pub width: Option<usize>,
36 pub height: Option<usize>,
38 pub padding: (usize, usize, usize, usize),
40 pub highlight: bool,
42}
43
44impl Panel {
45 pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
47 Self {
48 renderable: DynRenderable::new(renderable),
49 box_style: BOX_ROUNDED.clone(),
50 title: None,
51 title_align: AlignMethod::Center,
52 subtitle: None,
53 subtitle_align: AlignMethod::Center,
54 expand: true,
55 style: Style::new(),
56 border_style: Style::new(),
57 width: None,
58 height: None,
59 padding: (0, 1, 0, 1), highlight: false,
61 }
62 }
63
64 pub fn box_style(mut self, bs: BoxStyle) -> Self {
66 self.box_style = bs;
67 self
68 }
69
70 pub fn title(mut self, title: impl Into<String>) -> Self {
72 self.title = Some(title.into());
73 self
74 }
75
76 pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
78 self.subtitle = Some(subtitle.into());
79 self
80 }
81
82 pub fn border_style(mut self, style: Style) -> Self {
84 self.border_style = style;
85 self
86 }
87
88 pub fn style(mut self, style: Style) -> Self {
90 self.style = style;
91 self
92 }
93
94 pub fn width(mut self, width: usize) -> Self {
96 self.width = Some(width);
97 self
98 }
99
100 pub fn height(mut self, height: usize) -> Self {
102 self.height = Some(height);
103 self
104 }
105
106 pub fn padding(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
108 self.padding = (top, right, bottom, left);
109 self
110 }
111
112 pub fn fit(mut self) -> Self {
114 self.expand = false;
115 self
116 }
117
118 pub fn title_align(mut self, align: AlignMethod) -> Self {
120 self.title_align = align;
121 self
122 }
123}
124
125impl std::fmt::Debug for Panel {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 f.debug_struct("Panel")
128 .field("title", &self.title)
129 .field("width", &self.width)
130 .field("height", &self.height)
131 .finish()
132 }
133}
134
135impl Renderable for Panel {
136 fn render(&self, options: &ConsoleOptions) -> RenderResult {
137 let box_style = get_safe_box(&self.box_style, options.ascii_only);
138 let padding = self.padding;
139 let inner_max_width = options.max_width.saturating_sub(2 + padding.1 + padding.3);
140
141 let inner_options = options
143 .update_width(inner_max_width.max(1));
144 let content = self.renderable.render(&inner_options);
145
146 let content_width: usize = content
148 .lines
149 .iter()
150 .map(|line| {
151 line.iter()
152 .map(|s| s.cell_length())
153 .sum::<usize>()
154 })
155 .max()
156 .unwrap_or(0);
157
158 let panel_width = if self.expand {
159 options.max_width
160 } else {
161 (content_width + 2 + padding.1 + padding.3).min(options.max_width).max(3)
162 };
163
164 let mut lines: Vec<Vec<Segment>> = Vec::new();
166 let border = &box_style;
167 let border_ansi = self.border_style.to_ansi();
168 let border_reset = if border_ansi.is_empty() { "" } else { "\x1b[0m" };
169
170 let bs = |ch: char| -> Segment {
172 let text = format!("{border_ansi}{ch}{border_reset}");
173 Segment::new(text)
174 };
175 let _bs_text = |s: &str| -> Segment {
176 let text = format!("{border_ansi}{s}{border_reset}");
177 Segment::new(text)
178 };
179
180 let top_line = self.render_top_border(
182 &box_style, panel_width, &border_ansi, &border_reset,
183 );
184 lines.push(top_line);
185
186 for _ in 0..padding.0 {
188 let pad_line = self.render_pad_line(&box_style, panel_width, &border_ansi, &border_reset);
189 lines.push(pad_line);
190 }
191
192 for content_line in &content.lines {
194 let mut line: Vec<Segment> = Vec::new();
195 line.push(bs(border.mid_vertical));
197 if padding.3 > 0 {
199 line.push(Segment::new(" ".repeat(padding.3)));
200 }
201
202 let available = panel_width.saturating_sub(2 + padding.1 + padding.3);
204 let seg_width: usize = content_line.iter().map(|s| s.cell_length()).sum();
205 line.extend(content_line.iter().take(seg_width.min(available)).cloned());
206
207 let fill = available.saturating_sub(seg_width);
209 if fill > 0 {
210 line.push(Segment::new(" ".repeat(fill)));
211 }
212
213 if padding.1 > 0 {
215 line.push(Segment::new(" ".repeat(padding.1)));
216 }
217 line.push(bs(border.mid_right));
219 line.push(Segment::line());
220 lines.push(line);
221 }
222
223 for _ in 0..padding.2 {
225 let pad_line = self.render_pad_line(&box_style, panel_width, &border_ansi, &border_reset);
226 lines.push(pad_line);
227 }
228
229 let bottom_line = self.render_bottom_border(
231 &box_style, panel_width, &border_ansi, &border_reset,
232 );
233 lines.push(bottom_line);
234
235 RenderResult { lines, items: Vec::new() }
236 }
237}
238
239impl Panel {
240 fn render_top_border(
241 &self,
242 b: &BoxStyle,
243 width: usize,
244 border_ansi: &str,
245 border_reset: &str,
246 ) -> Vec<Segment> {
247 let mut line = Vec::new();
248 let inner = width.saturating_sub(2);
249
250 if let Some(ref title) = self.title {
251 let title_w = unicode_width::UnicodeWidthStr::width(title.as_str());
252 if title_w + 2 <= inner {
253 let rem = inner - title_w - 2;
254 let (left_w, right_w) = match self.title_align {
255 AlignMethod::Left => (1, rem - 1),
256 AlignMethod::Right => (rem - 1, 1),
257 AlignMethod::Center => {
258 let l = rem / 2;
259 (l, rem - l)
260 }
261 AlignMethod::Full => (1, rem - 1),
262 };
263
264 let bl = format!("{border_ansi}{}{border_reset}", b.top_left);
265 let br = format!("{border_ansi}{}{border_reset}", b.top_right);
266 let bt = format!("{border_ansi}{}{border_reset}", b.top);
267
268 line.push(Segment::new(bl));
269 line.push(Segment::new(bt.repeat(left_w)));
270 line.push(Segment::new(format!(" {title} ")));
271 line.push(Segment::new(bt.repeat(right_w)));
272 line.push(Segment::new(br));
273 line.push(Segment::line());
274 return line;
275 }
276 }
277
278 let bl = format!("{border_ansi}{}{border_reset}", b.top_left);
280 let br = format!("{border_ansi}{}{border_reset}", b.top_right);
281 let bt = format!("{border_ansi}{}{border_reset}", b.top);
282
283 line.push(Segment::new(bl));
284 line.push(Segment::new(bt.repeat(inner)));
285 line.push(Segment::new(br));
286 line.push(Segment::line());
287 line
288 }
289
290 fn render_bottom_border(
291 &self,
292 b: &BoxStyle,
293 width: usize,
294 border_ansi: &str,
295 border_reset: &str,
296 ) -> Vec<Segment> {
297 let mut line = Vec::new();
298 let inner = width.saturating_sub(2);
299
300 if let Some(ref subtitle) = self.subtitle {
301 let sub_w = unicode_width::UnicodeWidthStr::width(subtitle.as_str());
302 if sub_w + 2 <= inner {
303 let rem = inner - sub_w - 2;
304 let (left_w, right_w) = match self.subtitle_align {
305 AlignMethod::Left => (1, rem - 1),
306 AlignMethod::Right => (rem - 1, 1),
307 AlignMethod::Center => {
308 let l = rem / 2;
309 (l, rem - l)
310 }
311 AlignMethod::Full => (1, rem - 1),
312 };
313
314 let bl = format!("{border_ansi}{}{border_reset}", b.bottom_left);
315 let br = format!("{border_ansi}{}{border_reset}", b.bottom_right);
316 let bb = format!("{border_ansi}{}{border_reset}", b.bottom);
317
318 line.push(Segment::new(bl));
319 line.push(Segment::new(bb.repeat(left_w)));
320 line.push(Segment::new(format!(" {subtitle} ")));
321 line.push(Segment::new(bb.repeat(right_w)));
322 line.push(Segment::new(br));
323 line.push(Segment::line());
324 return line;
325 }
326 }
327
328 let bl = format!("{border_ansi}{}{border_reset}", b.bottom_left);
329 let br = format!("{border_ansi}{}{border_reset}", b.bottom_right);
330 let bb = format!("{border_ansi}{}{border_reset}", b.bottom);
331
332 line.push(Segment::new(bl));
333 line.push(Segment::new(bb.repeat(inner)));
334 line.push(Segment::new(br));
335 line.push(Segment::line());
336 line
337 }
338
339 fn render_pad_line(
340 &self,
341 b: &BoxStyle,
342 width: usize,
343 border_ansi: &str,
344 border_reset: &str,
345 ) -> Vec<Segment> {
346 let inner = width.saturating_sub(2);
347 let left = format!("{border_ansi}{}{border_reset}", b.mid_vertical);
348 let right = format!("{border_ansi}{}{border_reset}", b.mid_right);
349 vec![
350 Segment::new(left),
351 Segment::new(" ".repeat(inner)),
352 Segment::new(right),
353 Segment::line(),
354 ]
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::console::ConsoleOptions;
362
363 #[test]
364 fn test_panel_creation() {
365 let panel = Panel::new("Hello");
366 assert!(panel.title.is_none());
367 }
368
369 #[test]
370 fn test_panel_with_title() {
371 let panel = Panel::new("Content").title("My Title");
372 let opts = ConsoleOptions::default();
373 let result = panel.render(&opts);
374 let ansi = result.to_ansi();
375 assert!(ansi.contains("My Title"));
376 }
377}