use crate::console::RenderContext;
use crate::renderable::{Renderable, Segment};
use crate::style::Style;
use crate::text::{Span, Text};
use crate::box_drawing::{self, Box};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BorderStyle {
#[default]
Rounded,
Square,
Heavy,
Double,
Ascii,
AsciiDoubleHead,
Minimal,
MinimalHeavyHead,
MinimalDoubleHead,
Horizontals,
SquareDoubleHead,
HeavyEdge,
HeavyHead,
DoubleEdge,
Hidden,
}
impl BorderStyle {
pub fn to_box(&self) -> Box {
match self {
BorderStyle::Rounded => box_drawing::ROUNDED,
BorderStyle::Square => box_drawing::SQUARE,
BorderStyle::Heavy => box_drawing::HEAVY,
BorderStyle::Double => box_drawing::DOUBLE,
BorderStyle::Ascii => box_drawing::ASCII,
BorderStyle::AsciiDoubleHead => box_drawing::ASCII_DOUBLE_HEAD,
BorderStyle::Minimal => box_drawing::MINIMAL,
BorderStyle::MinimalHeavyHead => box_drawing::MINIMAL_HEAVY_HEAD,
BorderStyle::MinimalDoubleHead => box_drawing::MINIMAL_DOUBLE_HEAD,
BorderStyle::Horizontals => box_drawing::HORIZONTALS,
BorderStyle::SquareDoubleHead => box_drawing::SQUARE_DOUBLE_HEAD,
BorderStyle::HeavyEdge => box_drawing::HEAVY_EDGE,
BorderStyle::HeavyHead => box_drawing::HEAVY_HEAD,
BorderStyle::DoubleEdge => box_drawing::DOUBLE_EDGE,
BorderStyle::Hidden => Box {
top: box_drawing::Line::new(' ', ' ', ' ', ' '),
head: box_drawing::Line::new(' ', ' ', ' ', ' '),
mid: box_drawing::Line::new(' ', ' ', ' ', ' '),
bottom: box_drawing::Line::new(' ', ' ', ' ', ' '),
header: box_drawing::Line::new(' ', ' ', ' ', ' '),
cell: box_drawing::Line::new(' ', ' ', ' ', ' '),
},
}
}
}
#[derive(Debug, Clone)]
pub struct Panel {
content: Text,
title: Option<String>,
subtitle: Option<String>,
border_style: BorderStyle,
style: Style,
title_style: Style,
padding_x: usize,
padding_y: usize,
expand: bool,
}
impl Panel {
pub fn new<T: Into<Text>>(content: T) -> Self {
Panel {
content: content.into(),
title: None,
subtitle: None,
border_style: BorderStyle::Rounded,
style: Style::new(),
title_style: Style::new(),
padding_x: 1,
padding_y: 0,
expand: true,
}
}
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_string());
self
}
pub fn subtitle(mut self, subtitle: &str) -> Self {
self.subtitle = Some(subtitle.to_string());
self
}
pub fn border_style(mut self, style: BorderStyle) -> Self {
self.border_style = style;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
pub fn padding_x(mut self, padding: usize) -> Self {
self.padding_x = padding;
self
}
pub fn padding_y(mut self, padding: usize) -> Self {
self.padding_y = padding;
self
}
pub fn padding(self, x: usize, y: usize) -> Self {
self.padding_x(x).padding_y(y)
}
pub fn expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
fn render_top_border(&self, width: usize, box_chars: &Box) -> Segment {
let inner_width = width.saturating_sub(2);
let chars = box_chars.top;
match &self.title {
None => {
let line = chars.mid.to_string().repeat(inner_width);
Segment::line(vec![
Span::styled(chars.left.to_string(), self.style),
Span::styled(line, self.style),
Span::styled(chars.right.to_string(), self.style),
])
}
Some(title) => {
let title_with_space = format!(" {} ", title);
let title_width = unicode_width::UnicodeWidthStr::width(title_with_space.as_str());
if title_width >= inner_width {
let line = chars.mid.to_string().repeat(inner_width);
return Segment::line(vec![
Span::styled(chars.left.to_string(), self.style),
Span::styled(line, self.style),
Span::styled(chars.right.to_string(), self.style),
]);
}
let remaining = inner_width - title_width;
let left_len = 2.min(remaining);
let right_len = remaining - left_len;
Segment::line(vec![
Span::styled(chars.left.to_string(), self.style),
Span::styled(chars.mid.to_string().repeat(left_len), self.style),
Span::styled(title_with_space, self.title_style),
Span::styled(chars.mid.to_string().repeat(right_len), self.style),
Span::styled(chars.right.to_string(), self.style),
])
}
}
}
fn render_bottom_border(&self, width: usize, box_chars: &Box) -> Segment {
let inner_width = width.saturating_sub(2);
let chars = box_chars.bottom;
match &self.subtitle {
None => {
let line = chars.mid.to_string().repeat(inner_width);
Segment::line(vec![
Span::styled(chars.left.to_string(), self.style),
Span::styled(line, self.style),
Span::styled(chars.right.to_string(), self.style),
])
}
Some(subtitle) => {
let sub_with_space = format!(" {} ", subtitle);
let sub_width = unicode_width::UnicodeWidthStr::width(sub_with_space.as_str());
if sub_width >= inner_width {
let line = chars.mid.to_string().repeat(inner_width);
return Segment::line(vec![
Span::styled(chars.left.to_string(), self.style),
Span::styled(line, self.style),
Span::styled(chars.right.to_string(), self.style),
]);
}
let remaining = inner_width - sub_width;
let right_len = 2.min(remaining);
let left_len = remaining - right_len;
Segment::line(vec![
Span::styled(chars.left.to_string(), self.style),
Span::styled(chars.mid.to_string().repeat(left_len), self.style),
Span::styled(sub_with_space, self.title_style),
Span::styled(chars.mid.to_string().repeat(right_len), self.style),
Span::styled(chars.right.to_string(), self.style),
])
}
}
}
fn render_content_line(&self, spans: Vec<Span>, width: usize, box_chars: &Box) -> Segment {
let inner_width = width.saturating_sub(2 + self.padding_x * 2);
let content_width: usize = spans.iter().map(|s| s.width()).sum();
let padding_right = inner_width.saturating_sub(content_width);
let chars = box_chars.cell;
let mut line_spans = Vec::new();
line_spans.push(Span::styled(chars.left.to_string(), self.style));
line_spans.push(Span::styled(" ".repeat(self.padding_x), self.style));
line_spans.extend(spans);
line_spans.push(Span::styled(
" ".repeat(padding_right + self.padding_x),
self.style,
));
line_spans.push(Span::styled(chars.right.to_string(), self.style));
Segment::line(line_spans)
}
fn render_empty_line(&self, width: usize, box_chars: &Box) -> Segment {
let inner_width = width.saturating_sub(2);
let chars = box_chars.cell;
Segment::line(vec![
Span::styled(chars.left.to_string(), self.style),
Span::styled(" ".repeat(inner_width), self.style),
Span::styled(chars.right.to_string(), self.style),
])
}
}
impl<T: Into<Text>> From<T> for Panel {
fn from(content: T) -> Self {
Panel::new(content)
}
}
impl Renderable for Panel {
fn render(&self, context: &RenderContext) -> Vec<Segment> {
let box_chars = self.border_style.to_box();
let width = if self.expand {
context.width
} else {
let content_width = self.content.width();
let min_width = content_width + 2 + self.padding_x * 2;
min_width.min(context.width)
};
let inner_width = width.saturating_sub(2 + self.padding_x * 2);
let content_lines = self.content.wrap(inner_width);
let mut segments = Vec::new();
segments.push(self.render_top_border(width, &box_chars));
for _ in 0..self.padding_y {
segments.push(self.render_empty_line(width, &box_chars));
}
for line_spans in content_lines {
segments.push(self.render_content_line(line_spans, width, &box_chars));
}
for _ in 0..self.padding_y {
segments.push(self.render_empty_line(width, &box_chars));
}
segments.push(self.render_bottom_border(width, &box_chars));
segments
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_panel_simple() {
let panel = Panel::new("Hello");
let context = RenderContext {
width: 20,
height: None,
};
let segments = panel.render(&context);
assert!(segments.len() >= 3);
let top = segments[0].plain_text();
assert!(top.starts_with('╭'));
assert!(top.ends_with('╮'));
}
#[test]
fn test_panel_with_title() {
let panel = Panel::new("Content").title("Title");
let context = RenderContext {
width: 30,
height: None,
};
let segments = panel.render(&context);
let top = segments[0].plain_text();
assert!(top.contains("Title"));
}
#[test]
fn test_panel_border_styles() {
let panel = Panel::new("Test").border_style(BorderStyle::Double);
let context = RenderContext {
width: 20,
height: None,
};
let segments = panel.render(&context);
let top = segments[0].plain_text();
assert!(top.starts_with('╔'));
}
}