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 has_edge = box_style.has_visible_edges();
140 let edge_width: usize = if has_edge { 2 } else { 0 };
142 let inner_max_width = options
143 .max_width
144 .saturating_sub(edge_width + padding.1 + padding.3);
145
146 let inner_options = options.update_width(inner_max_width.max(1));
148 let content = self.renderable.render(&inner_options);
149
150 let content_width: usize = content
152 .lines
153 .iter()
154 .map(|line| line.iter().map(|s| s.cell_length()).sum::<usize>())
155 .max()
156 .unwrap_or(0);
157
158 let panel_width = if self.expand {
159 options.max_width
160 } else {
161 (content_width + edge_width + padding.1 + padding.3)
162 .min(options.max_width)
163 .max(3)
164 };
165
166 let mut lines: Vec<Vec<Segment>> = Vec::new();
168 let border = &box_style;
169 let border_ansi = self.border_style.to_ansi();
170 let border_reset = if border_ansi.is_empty() {
171 ""
172 } else {
173 "\x1b[0m"
174 };
175
176 let bs = |ch: char| -> Segment {
178 let text = format!("{border_ansi}{ch}{border_reset}");
179 Segment::new(text)
180 };
181
182 if !has_edge {
184 if let Some(ref title) = self.title {
186 let aligned = self.title_align.align_text(title, panel_width);
187 lines.push(vec![Segment::new(&aligned), Segment::line()]);
188 }
189 for _ in 0..padding.0 {
191 lines.push(vec![Segment::new(" ".repeat(panel_width)), Segment::line()]);
192 }
193 for content_line in &content.lines {
195 let mut line: Vec<Segment> = Vec::new();
196 if padding.3 > 0 {
197 line.push(Segment::new(" ".repeat(padding.3)));
198 }
199 let available = panel_width.saturating_sub(padding.1 + padding.3);
200 let seg_width: usize = content_line.iter().map(|s| s.cell_length()).sum();
201 line.extend(content_line.iter().take(seg_width.min(available)).cloned());
202 let fill = available.saturating_sub(seg_width);
203 if fill > 0 {
204 line.push(Segment::new(" ".repeat(fill)));
205 }
206 if padding.1 > 0 {
207 line.push(Segment::new(" ".repeat(padding.1)));
208 }
209 line.push(Segment::line());
210 lines.push(line);
211 }
212 for _ in 0..padding.2 {
214 lines.push(vec![Segment::new(" ".repeat(panel_width)), Segment::line()]);
215 }
216 if let Some(ref subtitle) = self.subtitle {
218 let aligned = self.subtitle_align.align_text(subtitle, panel_width);
219 lines.push(vec![Segment::new(&aligned), Segment::line()]);
220 }
221 return RenderResult {
222 lines,
223 items: Vec::new(),
224 };
225 }
226
227 let top_line =
230 self.render_top_border(&box_style, panel_width, border_ansi.as_str(), border_reset);
231 lines.push(top_line);
232
233 for _ in 0..padding.0 {
235 let pad_line =
236 self.render_pad_line(&box_style, panel_width, border_ansi.as_str(), border_reset);
237 lines.push(pad_line);
238 }
239
240 for content_line in &content.lines {
242 let mut line: Vec<Segment> = Vec::new();
243 line.push(bs(border.mid_left));
245 if padding.3 > 0 {
247 line.push(Segment::new(" ".repeat(padding.3)));
248 }
249
250 let available = panel_width.saturating_sub(2 + padding.1 + padding.3);
252 let seg_width: usize = content_line.iter().map(|s| s.cell_length()).sum();
253 line.extend(content_line.iter().take(seg_width.min(available)).cloned());
254
255 let fill = available.saturating_sub(seg_width);
257 if fill > 0 {
258 line.push(Segment::new(" ".repeat(fill)));
259 }
260
261 if padding.1 > 0 {
263 line.push(Segment::new(" ".repeat(padding.1)));
264 }
265 line.push(bs(border.mid_right));
267 line.push(Segment::line());
268 lines.push(line);
269 }
270
271 for _ in 0..padding.2 {
273 let pad_line =
274 self.render_pad_line(&box_style, panel_width, border_ansi.as_str(), border_reset);
275 lines.push(pad_line);
276 }
277
278 let bottom_line =
280 self.render_bottom_border(&box_style, panel_width, border_ansi.as_str(), border_reset);
281 lines.push(bottom_line);
282
283 RenderResult {
284 lines,
285 items: Vec::new(),
286 }
287 }
288}
289
290impl Panel {
291 fn render_top_border(
292 &self,
293 b: &BoxStyle,
294 width: usize,
295 border_ansi: &str,
296 border_reset: &str,
297 ) -> Vec<Segment> {
298 let mut line = Vec::new();
299 let inner = width.saturating_sub(2);
300
301 if let Some(ref title) = self.title {
302 let title_w = unicode_width::UnicodeWidthStr::width(title.as_str());
303 if title_w + 2 <= inner {
304 let rem = inner - title_w - 2;
305 let (left_w, right_w) = match self.title_align {
306 AlignMethod::Left => (1, rem - 1),
307 AlignMethod::Right => (rem - 1, 1),
308 AlignMethod::Center => {
309 let l = rem / 2;
310 (l, rem - l)
311 }
312 AlignMethod::Full => (1, rem - 1),
313 };
314
315 let bl = format!("{border_ansi}{}{border_reset}", b.top_left);
317 let br = format!("{border_ansi}{}{border_reset}", b.top_right);
318 let bt_left = format!(
319 "{border_ansi}{}{border_reset}",
320 b.top.to_string().repeat(left_w)
321 );
322 let bt_right = format!(
323 "{border_ansi}{}{border_reset}",
324 b.top.to_string().repeat(right_w)
325 );
326
327 line.push(Segment::new(bl));
328 line.push(Segment::new(bt_left));
329 line.push(Segment::new(format!(" {title} ")));
330 line.push(Segment::new(bt_right));
331 line.push(Segment::new(br));
332 line.push(Segment::line());
333 return line;
334 }
335 }
336
337 let bl = format!("{border_ansi}{}{border_reset}", b.top_left);
339 let br = format!("{border_ansi}{}{border_reset}", b.top_right);
340 let bt = format!(
341 "{border_ansi}{}{border_reset}",
342 b.top.to_string().repeat(inner)
343 );
344
345 line.push(Segment::new(bl));
346 line.push(Segment::new(bt));
347 line.push(Segment::new(br));
348 line.push(Segment::line());
349 line
350 }
351
352 fn render_bottom_border(
353 &self,
354 b: &BoxStyle,
355 width: usize,
356 border_ansi: &str,
357 border_reset: &str,
358 ) -> Vec<Segment> {
359 let mut line = Vec::new();
360 let inner = width.saturating_sub(2);
361
362 if let Some(ref subtitle) = self.subtitle {
363 let sub_w = unicode_width::UnicodeWidthStr::width(subtitle.as_str());
364 if sub_w + 2 <= inner {
365 let rem = inner - sub_w - 2;
366 let (left_w, right_w) = match self.subtitle_align {
367 AlignMethod::Left => (1, rem - 1),
368 AlignMethod::Right => (rem - 1, 1),
369 AlignMethod::Center => {
370 let l = rem / 2;
371 (l, rem - l)
372 }
373 AlignMethod::Full => (1, rem - 1),
374 };
375
376 let bl = format!("{border_ansi}{}{border_reset}", b.bottom_left);
377 let br = format!("{border_ansi}{}{border_reset}", b.bottom_right);
378 let bb_left = format!(
379 "{border_ansi}{}{border_reset}",
380 b.bottom.to_string().repeat(left_w)
381 );
382 let bb_right = format!(
383 "{border_ansi}{}{border_reset}",
384 b.bottom.to_string().repeat(right_w)
385 );
386
387 line.push(Segment::new(bl));
388 line.push(Segment::new(bb_left));
389 line.push(Segment::new(format!(" {subtitle} ")));
390 line.push(Segment::new(bb_right));
391 line.push(Segment::new(br));
392 line.push(Segment::line());
393 return line;
394 }
395 }
396
397 let bl = format!("{border_ansi}{}{border_reset}", b.bottom_left);
398 let br = format!("{border_ansi}{}{border_reset}", b.bottom_right);
399 let bb = format!(
400 "{border_ansi}{}{border_reset}",
401 b.bottom.to_string().repeat(inner)
402 );
403
404 line.push(Segment::new(bl));
405 line.push(Segment::new(bb));
406 line.push(Segment::new(br));
407 line.push(Segment::line());
408 line
409 }
410
411 fn render_pad_line(
412 &self,
413 b: &BoxStyle,
414 width: usize,
415 border_ansi: &str,
416 border_reset: &str,
417 ) -> Vec<Segment> {
418 let inner = width.saturating_sub(2);
419 let left = format!("{border_ansi}{}{border_reset}", b.mid_left);
420 let right = format!("{border_ansi}{}{border_reset}", b.mid_right);
421 vec![
422 Segment::new(left),
423 Segment::new(" ".repeat(inner)),
424 Segment::new(right),
425 Segment::line(),
426 ]
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::console::ConsoleOptions;
434
435 #[test]
436 fn test_panel_creation() {
437 let panel = Panel::new("Hello");
438 assert!(panel.title.is_none());
439 }
440
441 #[test]
442 fn test_panel_with_title() {
443 let panel = Panel::new("Content").title("My Title");
444 let opts = ConsoleOptions::default();
445 let result = panel.render(&opts);
446 let ansi = result.to_ansi();
447 assert!(ansi.contains("My Title"));
448 }
449}