1use crate::console::RenderContext;
7use crate::renderable::{Renderable, Segment};
8use crate::style::Style;
9use crate::text::{Span, Text};
10
11use crate::box_drawing::{self, Box};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum BorderStyle {
16 #[default]
18 Rounded,
19 Square,
21 Heavy,
23 Double,
25 Ascii,
27 AsciiDoubleHead,
29 Minimal,
31 MinimalHeavyHead,
33 MinimalDoubleHead,
35 Horizontals,
37 SquareDoubleHead,
39 HeavyEdge,
41 HeavyHead,
43 DoubleEdge,
45 Hidden,
47}
48
49impl BorderStyle {
50 pub fn to_box(&self) -> Box {
52 match self {
53 BorderStyle::Rounded => box_drawing::ROUNDED,
54 BorderStyle::Square => box_drawing::SQUARE,
55 BorderStyle::Heavy => box_drawing::HEAVY,
56 BorderStyle::Double => box_drawing::DOUBLE,
57 BorderStyle::Ascii => box_drawing::ASCII,
58 BorderStyle::AsciiDoubleHead => box_drawing::ASCII_DOUBLE_HEAD,
59 BorderStyle::Minimal => box_drawing::MINIMAL,
60 BorderStyle::MinimalHeavyHead => box_drawing::MINIMAL_HEAVY_HEAD,
61 BorderStyle::MinimalDoubleHead => box_drawing::MINIMAL_DOUBLE_HEAD,
62 BorderStyle::Horizontals => box_drawing::HORIZONTALS,
63 BorderStyle::SquareDoubleHead => box_drawing::SQUARE_DOUBLE_HEAD,
64 BorderStyle::HeavyEdge => box_drawing::HEAVY_EDGE,
65 BorderStyle::HeavyHead => box_drawing::HEAVY_HEAD,
66 BorderStyle::DoubleEdge => box_drawing::DOUBLE_EDGE,
67 BorderStyle::Hidden => Box {
68 top: box_drawing::Line::new(' ', ' ', ' ', ' '),
69 head: box_drawing::Line::new(' ', ' ', ' ', ' '),
70 mid: box_drawing::Line::new(' ', ' ', ' ', ' '),
71 bottom: box_drawing::Line::new(' ', ' ', ' ', ' '),
72 header: box_drawing::Line::new(' ', ' ', ' ', ' '),
73 cell: box_drawing::Line::new(' ', ' ', ' ', ' '),
74 },
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct Panel {
82 content: Text,
84 title: Option<String>,
86 subtitle: Option<String>,
88 border_style: BorderStyle,
90 style: Style,
92 title_style: Style,
94 padding_x: usize,
96 padding_y: usize,
98 expand: bool,
100}
101
102impl Panel {
103 pub fn new<T: Into<Text>>(content: T) -> Self {
105 Panel {
106 content: content.into(),
107 title: None,
108 subtitle: None,
109 border_style: BorderStyle::Rounded,
110 style: Style::new(),
111 title_style: Style::new(),
112 padding_x: 1,
113 padding_y: 0,
114 expand: true,
115 }
116 }
117
118 pub fn title(mut self, title: &str) -> Self {
120 self.title = Some(title.to_string());
121 self
122 }
123
124 pub fn subtitle(mut self, subtitle: &str) -> Self {
126 self.subtitle = Some(subtitle.to_string());
127 self
128 }
129
130 pub fn border_style(mut self, style: BorderStyle) -> Self {
132 self.border_style = style;
133 self
134 }
135
136 pub fn style(mut self, style: Style) -> Self {
138 self.style = style;
139 self
140 }
141
142 pub fn title_style(mut self, style: Style) -> Self {
144 self.title_style = style;
145 self
146 }
147
148 pub fn padding_x(mut self, padding: usize) -> Self {
150 self.padding_x = padding;
151 self
152 }
153
154 pub fn padding_y(mut self, padding: usize) -> Self {
156 self.padding_y = padding;
157 self
158 }
159
160 pub fn padding(self, x: usize, y: usize) -> Self {
162 self.padding_x(x).padding_y(y)
163 }
164
165 pub fn expand(mut self, expand: bool) -> Self {
167 self.expand = expand;
168 self
169 }
170
171 fn render_top_border(&self, width: usize, box_chars: &Box) -> Segment {
172 let inner_width = width.saturating_sub(2);
173 let chars = box_chars.top;
174
175 match &self.title {
176 None => {
177 let line = chars.mid.to_string().repeat(inner_width);
178 Segment::line(vec![
179 Span::styled(chars.left.to_string(), self.style),
180 Span::styled(line, self.style),
181 Span::styled(chars.right.to_string(), self.style),
182 ])
183 }
184 Some(title) => {
185 let title_with_space = format!(" {} ", title);
186 let title_width = unicode_width::UnicodeWidthStr::width(title_with_space.as_str());
187
188 if title_width >= inner_width {
189 let line = chars.mid.to_string().repeat(inner_width);
190 return Segment::line(vec![
191 Span::styled(chars.left.to_string(), self.style),
192 Span::styled(line, self.style),
193 Span::styled(chars.right.to_string(), self.style),
194 ]);
195 }
196
197 let remaining = inner_width - title_width;
198 let left_len = 2.min(remaining);
199 let right_len = remaining - left_len;
200
201 Segment::line(vec![
202 Span::styled(chars.left.to_string(), self.style),
203 Span::styled(chars.mid.to_string().repeat(left_len), self.style),
204 Span::styled(title_with_space, self.title_style),
205 Span::styled(chars.mid.to_string().repeat(right_len), self.style),
206 Span::styled(chars.right.to_string(), self.style),
207 ])
208 }
209 }
210 }
211
212 fn render_bottom_border(&self, width: usize, box_chars: &Box) -> Segment {
213 let inner_width = width.saturating_sub(2);
214 let chars = box_chars.bottom;
215
216 match &self.subtitle {
217 None => {
218 let line = chars.mid.to_string().repeat(inner_width);
219 Segment::line(vec![
220 Span::styled(chars.left.to_string(), self.style),
221 Span::styled(line, self.style),
222 Span::styled(chars.right.to_string(), self.style),
223 ])
224 }
225 Some(subtitle) => {
226 let sub_with_space = format!(" {} ", subtitle);
227 let sub_width = unicode_width::UnicodeWidthStr::width(sub_with_space.as_str());
228
229 if sub_width >= inner_width {
230 let line = chars.mid.to_string().repeat(inner_width);
231 return Segment::line(vec![
232 Span::styled(chars.left.to_string(), self.style),
233 Span::styled(line, self.style),
234 Span::styled(chars.right.to_string(), self.style),
235 ]);
236 }
237
238 let remaining = inner_width - sub_width;
239 let right_len = 2.min(remaining);
240 let left_len = remaining - right_len;
241
242 Segment::line(vec![
243 Span::styled(chars.left.to_string(), self.style),
244 Span::styled(chars.mid.to_string().repeat(left_len), self.style),
245 Span::styled(sub_with_space, self.title_style),
246 Span::styled(chars.mid.to_string().repeat(right_len), self.style),
247 Span::styled(chars.right.to_string(), self.style),
248 ])
249 }
250 }
251 }
252
253 fn render_content_line(&self, spans: Vec<Span>, width: usize, box_chars: &Box) -> Segment {
254 let inner_width = width.saturating_sub(2 + self.padding_x * 2);
255 let content_width: usize = spans.iter().map(|s| s.width()).sum();
256 let padding_right = inner_width.saturating_sub(content_width);
257 let chars = box_chars.cell;
258
259 let mut line_spans = Vec::new();
260 line_spans.push(Span::styled(chars.left.to_string(), self.style));
261 line_spans.push(Span::styled(" ".repeat(self.padding_x), self.style));
262 line_spans.extend(spans);
263 line_spans.push(Span::styled(
264 " ".repeat(padding_right + self.padding_x),
265 self.style,
266 ));
267 line_spans.push(Span::styled(chars.right.to_string(), self.style));
268
269 Segment::line(line_spans)
270 }
271
272 fn render_empty_line(&self, width: usize, box_chars: &Box) -> Segment {
273 let inner_width = width.saturating_sub(2);
274 let chars = box_chars.cell;
275 Segment::line(vec![
276 Span::styled(chars.left.to_string(), self.style),
277 Span::styled(" ".repeat(inner_width), self.style),
278 Span::styled(chars.right.to_string(), self.style),
279 ])
280 }
281}
282
283impl<T: Into<Text>> From<T> for Panel {
284 fn from(content: T) -> Self {
285 Panel::new(content)
286 }
287}
288
289impl Renderable for Panel {
290 fn render(&self, context: &RenderContext) -> Vec<Segment> {
291 let box_chars = self.border_style.to_box();
292 let width = if self.expand {
293 context.width
294 } else {
295 let content_width = self.content.width();
296 let min_width = content_width + 2 + self.padding_x * 2;
297 min_width.min(context.width)
298 };
299
300 let inner_width = width.saturating_sub(2 + self.padding_x * 2);
301 let content_lines = self.content.wrap(inner_width);
302
303 let mut segments = Vec::new();
304
305 segments.push(self.render_top_border(width, &box_chars));
307
308 for _ in 0..self.padding_y {
310 segments.push(self.render_empty_line(width, &box_chars));
311 }
312
313 for line_spans in content_lines {
315 segments.push(self.render_content_line(line_spans, width, &box_chars));
316 }
317
318 for _ in 0..self.padding_y {
320 segments.push(self.render_empty_line(width, &box_chars));
321 }
322
323 segments.push(self.render_bottom_border(width, &box_chars));
325
326 segments
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_panel_simple() {
336 let panel = Panel::new("Hello");
337 let context = RenderContext {
338 width: 20,
339 height: None,
340 };
341 let segments = panel.render(&context);
342
343 assert!(segments.len() >= 3);
345
346 let top = segments[0].plain_text();
348 assert!(top.starts_with('╭'));
349 assert!(top.ends_with('╮'));
350 }
351
352 #[test]
353 fn test_panel_with_title() {
354 let panel = Panel::new("Content").title("Title");
355 let context = RenderContext {
356 width: 30,
357 height: None,
358 };
359 let segments = panel.render(&context);
360
361 let top = segments[0].plain_text();
362 assert!(top.contains("Title"));
363 }
364
365 #[test]
366 fn test_panel_border_styles() {
367 let panel = Panel::new("Test").border_style(BorderStyle::Double);
368 let context = RenderContext {
369 width: 20,
370 height: None,
371 };
372 let segments = panel.render(&context);
373
374 let top = segments[0].plain_text();
375 assert!(top.starts_with('╔'));
376 }
377}