use crate::align::AlignMethod;
use crate::box_drawing::{get_safe_box, BoxStyle, BOX_ROUNDED};
use crate::console::{ConsoleOptions, DynRenderable, RenderResult, Renderable};
use crate::segment::Segment;
use crate::style::Style;
#[derive(Clone)]
pub struct Panel {
pub renderable: DynRenderable,
pub box_style: BoxStyle,
pub title: Option<String>,
pub title_align: AlignMethod,
pub subtitle: Option<String>,
pub subtitle_align: AlignMethod,
pub expand: bool,
pub style: Style,
pub border_style: Style,
pub width: Option<usize>,
pub height: Option<usize>,
pub padding: (usize, usize, usize, usize),
pub highlight: bool,
}
impl Panel {
pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
Self {
renderable: DynRenderable::new(renderable),
box_style: BOX_ROUNDED.clone(),
title: None,
title_align: AlignMethod::Center,
subtitle: None,
subtitle_align: AlignMethod::Center,
expand: true,
style: Style::new(),
border_style: Style::new(),
width: None,
height: None,
padding: (0, 1, 0, 1), highlight: false,
}
}
pub fn box_style(mut self, bs: BoxStyle) -> Self {
self.box_style = bs;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
self.subtitle = Some(subtitle.into());
self
}
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = style;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: usize) -> Self {
self.height = Some(height);
self
}
pub fn padding(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
self.padding = (top, right, bottom, left);
self
}
pub fn fit(mut self) -> Self {
self.expand = false;
self
}
pub fn title_align(mut self, align: AlignMethod) -> Self {
self.title_align = align;
self
}
}
impl std::fmt::Debug for Panel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Panel")
.field("title", &self.title)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
impl Renderable for Panel {
fn render(&self, options: &ConsoleOptions) -> RenderResult {
let box_style = get_safe_box(&self.box_style, options.ascii_only);
let padding = self.padding;
let inner_max_width = options.max_width.saturating_sub(2 + padding.1 + padding.3);
let inner_options = options
.update_width(inner_max_width.max(1));
let content = self.renderable.render(&inner_options);
let content_width: usize = content
.lines
.iter()
.map(|line| {
line.iter()
.map(|s| s.cell_length())
.sum::<usize>()
})
.max()
.unwrap_or(0);
let panel_width = if self.expand {
options.max_width
} else {
(content_width + 2 + padding.1 + padding.3).min(options.max_width).max(3)
};
let mut lines: Vec<Vec<Segment>> = Vec::new();
let border = &box_style;
let border_ansi = self.border_style.to_ansi();
let border_reset = if border_ansi.is_empty() { "" } else { "\x1b[0m" };
let bs = |ch: char| -> Segment {
let text = format!("{border_ansi}{ch}{border_reset}");
Segment::new(text)
};
let _bs_text = |s: &str| -> Segment {
let text = format!("{border_ansi}{s}{border_reset}");
Segment::new(text)
};
let top_line = self.render_top_border(
&box_style, panel_width, &border_ansi, &border_reset,
);
lines.push(top_line);
for _ in 0..padding.0 {
let pad_line = self.render_pad_line(&box_style, panel_width, &border_ansi, &border_reset);
lines.push(pad_line);
}
for content_line in &content.lines {
let mut line: Vec<Segment> = Vec::new();
line.push(bs(border.mid_vertical));
if padding.3 > 0 {
line.push(Segment::new(" ".repeat(padding.3)));
}
let available = panel_width.saturating_sub(2 + padding.1 + padding.3);
let seg_width: usize = content_line.iter().map(|s| s.cell_length()).sum();
line.extend(content_line.iter().take(seg_width.min(available)).cloned());
let fill = available.saturating_sub(seg_width);
if fill > 0 {
line.push(Segment::new(" ".repeat(fill)));
}
if padding.1 > 0 {
line.push(Segment::new(" ".repeat(padding.1)));
}
line.push(bs(border.mid_right));
line.push(Segment::line());
lines.push(line);
}
for _ in 0..padding.2 {
let pad_line = self.render_pad_line(&box_style, panel_width, &border_ansi, &border_reset);
lines.push(pad_line);
}
let bottom_line = self.render_bottom_border(
&box_style, panel_width, &border_ansi, &border_reset,
);
lines.push(bottom_line);
RenderResult { lines, items: Vec::new() }
}
}
impl Panel {
fn render_top_border(
&self,
b: &BoxStyle,
width: usize,
border_ansi: &str,
border_reset: &str,
) -> Vec<Segment> {
let mut line = Vec::new();
let inner = width.saturating_sub(2);
if let Some(ref title) = self.title {
let title_w = unicode_width::UnicodeWidthStr::width(title.as_str());
if title_w + 2 <= inner {
let rem = inner - title_w - 2;
let (left_w, right_w) = match self.title_align {
AlignMethod::Left => (1, rem - 1),
AlignMethod::Right => (rem - 1, 1),
AlignMethod::Center => {
let l = rem / 2;
(l, rem - l)
}
AlignMethod::Full => (1, rem - 1),
};
let bl = format!("{border_ansi}{}{border_reset}", b.top_left);
let br = format!("{border_ansi}{}{border_reset}", b.top_right);
let bt = format!("{border_ansi}{}{border_reset}", b.top);
line.push(Segment::new(bl));
line.push(Segment::new(bt.repeat(left_w)));
line.push(Segment::new(format!(" {title} ")));
line.push(Segment::new(bt.repeat(right_w)));
line.push(Segment::new(br));
line.push(Segment::line());
return line;
}
}
let bl = format!("{border_ansi}{}{border_reset}", b.top_left);
let br = format!("{border_ansi}{}{border_reset}", b.top_right);
let bt = format!("{border_ansi}{}{border_reset}", b.top);
line.push(Segment::new(bl));
line.push(Segment::new(bt.repeat(inner)));
line.push(Segment::new(br));
line.push(Segment::line());
line
}
fn render_bottom_border(
&self,
b: &BoxStyle,
width: usize,
border_ansi: &str,
border_reset: &str,
) -> Vec<Segment> {
let mut line = Vec::new();
let inner = width.saturating_sub(2);
if let Some(ref subtitle) = self.subtitle {
let sub_w = unicode_width::UnicodeWidthStr::width(subtitle.as_str());
if sub_w + 2 <= inner {
let rem = inner - sub_w - 2;
let (left_w, right_w) = match self.subtitle_align {
AlignMethod::Left => (1, rem - 1),
AlignMethod::Right => (rem - 1, 1),
AlignMethod::Center => {
let l = rem / 2;
(l, rem - l)
}
AlignMethod::Full => (1, rem - 1),
};
let bl = format!("{border_ansi}{}{border_reset}", b.bottom_left);
let br = format!("{border_ansi}{}{border_reset}", b.bottom_right);
let bb = format!("{border_ansi}{}{border_reset}", b.bottom);
line.push(Segment::new(bl));
line.push(Segment::new(bb.repeat(left_w)));
line.push(Segment::new(format!(" {subtitle} ")));
line.push(Segment::new(bb.repeat(right_w)));
line.push(Segment::new(br));
line.push(Segment::line());
return line;
}
}
let bl = format!("{border_ansi}{}{border_reset}", b.bottom_left);
let br = format!("{border_ansi}{}{border_reset}", b.bottom_right);
let bb = format!("{border_ansi}{}{border_reset}", b.bottom);
line.push(Segment::new(bl));
line.push(Segment::new(bb.repeat(inner)));
line.push(Segment::new(br));
line.push(Segment::line());
line
}
fn render_pad_line(
&self,
b: &BoxStyle,
width: usize,
border_ansi: &str,
border_reset: &str,
) -> Vec<Segment> {
let inner = width.saturating_sub(2);
let left = format!("{border_ansi}{}{border_reset}", b.mid_vertical);
let right = format!("{border_ansi}{}{border_reset}", b.mid_right);
vec![
Segment::new(left),
Segment::new(" ".repeat(inner)),
Segment::new(right),
Segment::line(),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::ConsoleOptions;
#[test]
fn test_panel_creation() {
let panel = Panel::new("Hello");
assert!(panel.title.is_none());
}
#[test]
fn test_panel_with_title() {
let panel = Panel::new("Content").title("My Title");
let opts = ConsoleOptions::default();
let result = panel.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("My Title"));
}
}