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