use crate::buffer::ScreenBuffer;
use crate::cell::Cell;
use crate::geometry::Rect;
use crate::style::Style;
use super::Widget;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BorderStyle {
#[default]
None,
Single,
Double,
Rounded,
Heavy,
}
#[derive(Clone, Debug)]
pub struct Container {
border: BorderStyle,
border_style: Style,
title: Option<String>,
title_style: Style,
padding: u16,
}
impl Container {
pub fn new() -> Self {
Self {
border: BorderStyle::None,
border_style: Style::default(),
title: None,
title_style: Style::default(),
padding: 0,
}
}
#[must_use]
pub fn border(mut self, style: BorderStyle) -> Self {
self.border = style;
self
}
#[must_use]
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = style;
self
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
#[must_use]
pub fn padding(mut self, padding: u16) -> Self {
self.padding = padding;
self
}
pub fn inner_area(&self, area: Rect) -> Rect {
let border_offset = if self.border != BorderStyle::None {
1
} else {
0
};
let total_offset = border_offset + self.padding;
if area.size.width <= total_offset * 2 || area.size.height <= total_offset * 2 {
return Rect::new(
area.position.x + total_offset,
area.position.y + total_offset,
0,
0,
);
}
Rect::new(
area.position.x + total_offset,
area.position.y + total_offset,
area.size.width - total_offset * 2,
area.size.height - total_offset * 2,
)
}
}
impl Default for Container {
fn default() -> Self {
Self::new()
}
}
impl Widget for Container {
fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
if area.size.width < 2 || area.size.height < 2 {
return;
}
let Some((tl, tr, bl, br, h, v)) = self.border.chars() else {
return; };
let right = area.position.x + area.size.width - 1;
let bottom = area.position.y + area.size.height - 1;
buf.set(
area.position.x,
area.position.y,
Cell::new(tl, self.border_style.clone()),
);
buf.set(
right,
area.position.y,
Cell::new(tr, self.border_style.clone()),
);
buf.set(
area.position.x,
bottom,
Cell::new(bl, self.border_style.clone()),
);
buf.set(right, bottom, Cell::new(br, self.border_style.clone()));
for x in (area.position.x + 1)..right {
buf.set(x, area.position.y, Cell::new(h, self.border_style.clone()));
buf.set(x, bottom, Cell::new(h, self.border_style.clone()));
}
for y in (area.position.y + 1)..bottom {
buf.set(area.position.x, y, Cell::new(v, self.border_style.clone()));
buf.set(right, y, Cell::new(v, self.border_style.clone()));
}
if let Some(ref title) = self.title {
let max_title_width = (area.size.width.saturating_sub(4)) as usize; let display_title = if title.len() > max_title_width {
&title[..max_title_width]
} else {
title.as_str()
};
let start_x = area.position.x + 2; for (i, ch) in display_title.chars().enumerate() {
let x = start_x + i as u16;
if x < right {
buf.set(
x,
area.position.y,
Cell::new(ch.to_string(), self.title_style.clone()),
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::{Color, NamedColor};
use crate::geometry::Size;
#[test]
fn single_border_corners() {
let container = Container::new().border(BorderStyle::Single);
let mut buf = ScreenBuffer::new(Size::new(10, 5));
container.render(Rect::new(0, 0, 10, 5), &mut buf);
assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("┌"));
assert_eq!(buf.get(9, 0).map(|c| c.grapheme.as_str()), Some("┐"));
assert_eq!(buf.get(0, 4).map(|c| c.grapheme.as_str()), Some("└"));
assert_eq!(buf.get(9, 4).map(|c| c.grapheme.as_str()), Some("┘"));
}
#[test]
fn single_border_edges() {
let container = Container::new().border(BorderStyle::Single);
let mut buf = ScreenBuffer::new(Size::new(10, 5));
container.render(Rect::new(0, 0, 10, 5), &mut buf);
assert_eq!(buf.get(1, 0).map(|c| c.grapheme.as_str()), Some("─"));
assert_eq!(buf.get(1, 4).map(|c| c.grapheme.as_str()), Some("─"));
assert_eq!(buf.get(0, 1).map(|c| c.grapheme.as_str()), Some("│"));
assert_eq!(buf.get(9, 1).map(|c| c.grapheme.as_str()), Some("│"));
}
#[test]
fn double_border() {
let container = Container::new().border(BorderStyle::Double);
let mut buf = ScreenBuffer::new(Size::new(10, 5));
container.render(Rect::new(0, 0, 10, 5), &mut buf);
assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("╔"));
assert_eq!(buf.get(9, 0).map(|c| c.grapheme.as_str()), Some("╗"));
}
#[test]
fn rounded_border() {
let container = Container::new().border(BorderStyle::Rounded);
let mut buf = ScreenBuffer::new(Size::new(10, 5));
container.render(Rect::new(0, 0, 10, 5), &mut buf);
assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("╭"));
assert_eq!(buf.get(9, 0).map(|c| c.grapheme.as_str()), Some("╮"));
}
#[test]
fn heavy_border() {
let container = Container::new().border(BorderStyle::Heavy);
let mut buf = ScreenBuffer::new(Size::new(10, 5));
container.render(Rect::new(0, 0, 10, 5), &mut buf);
assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("┏"));
assert_eq!(buf.get(1, 0).map(|c| c.grapheme.as_str()), Some("━"));
}
#[test]
fn border_with_title() {
let container = Container::new().border(BorderStyle::Single).title("Test");
let mut buf = ScreenBuffer::new(Size::new(20, 5));
container.render(Rect::new(0, 0, 20, 5), &mut buf);
assert_eq!(buf.get(2, 0).map(|c| c.grapheme.as_str()), Some("T"));
assert_eq!(buf.get(3, 0).map(|c| c.grapheme.as_str()), Some("e"));
assert_eq!(buf.get(4, 0).map(|c| c.grapheme.as_str()), Some("s"));
assert_eq!(buf.get(5, 0).map(|c| c.grapheme.as_str()), Some("t"));
}
#[test]
fn border_with_style() {
let style = Style::new().fg(Color::Named(NamedColor::Cyan));
let container = Container::new()
.border(BorderStyle::Single)
.border_style(style.clone());
let mut buf = ScreenBuffer::new(Size::new(10, 5));
container.render(Rect::new(0, 0, 10, 5), &mut buf);
assert_eq!(buf.get(0, 0).map(|c| &c.style), Some(&style));
}
#[test]
fn inner_area_with_border() {
let container = Container::new().border(BorderStyle::Single);
let inner = container.inner_area(Rect::new(0, 0, 20, 10));
assert_eq!(inner, Rect::new(1, 1, 18, 8));
}
#[test]
fn inner_area_with_border_and_padding() {
let container = Container::new().border(BorderStyle::Single).padding(1);
let inner = container.inner_area(Rect::new(0, 0, 20, 10));
assert_eq!(inner, Rect::new(2, 2, 16, 6));
}
#[test]
fn inner_area_no_border() {
let container = Container::new();
let inner = container.inner_area(Rect::new(0, 0, 20, 10));
assert_eq!(inner, Rect::new(0, 0, 20, 10));
}
#[test]
fn no_border_renders_nothing() {
let container = Container::new();
let mut buf = ScreenBuffer::new(Size::new(10, 5));
container.render(Rect::new(0, 0, 10, 5), &mut buf);
assert!(buf.get(0, 0).is_some_and(|c| c.is_blank()));
}
}