use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::style::Style;
use crate::widgets::Widget;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum BorderStyle {
None,
Plain,
Rounded,
Double,
Thick,
Custom {
top_left: char,
top_right: char,
bottom_left: char,
bottom_right: char,
top: char,
bottom: char,
left: char,
right: char,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Block<'a> {
pub title: &'a str,
pub title_right: Option<&'a str>,
pub borders: BorderStyle,
pub border_color: Color,
pub bg: Option<Color>,
pub style: Style,
pub inner_margin: Rect,
}
impl<'a> Block<'a> {
pub fn new(title: &'a str) -> Self {
Self {
title,
title_right: None,
borders: BorderStyle::Rounded,
border_color: Color::rgb(48, 54, 61),
bg: None,
style: Style::new(),
inner_margin: Rect::ZERO,
}
}
pub fn bordered() -> Self {
Self::new("").with_borders(BorderStyle::Plain)
}
pub fn title(mut self, title: &'a str) -> Self {
self.title = title;
self
}
pub fn border_style(mut self, style: Style) -> Self {
if let Some(fg) = style.fg {
self.border_color = fg;
}
if let Some(bg) = style.bg {
self.bg = Some(bg);
}
self.style = self.style.merge(&style);
self
}
pub fn with_borders(mut self, borders: BorderStyle) -> Self {
self.borders = borders;
self
}
pub fn with_border_color(mut self, color: Color) -> Self {
self.border_color = color;
self
}
pub fn with_bg(mut self, bg: Color) -> Self {
self.bg = Some(bg);
self
}
pub fn with_title_right(mut self, title: &'a str) -> Self {
self.title_right = Some(title);
self
}
pub fn with_inner_margin(mut self, margin: Rect) -> Self {
self.inner_margin = margin;
self
}
pub fn inner(&self, area: Rect) -> Rect {
match self.borders {
BorderStyle::None => area,
_ => Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(1),
area.width.saturating_sub(2),
area.height.saturating_sub(2),
)
.inner(self.inner_margin),
}
}
fn chars_for_border(
&self,
style: &BorderStyle,
) -> (char, char, char, char, char, char, char, char) {
match style {
BorderStyle::None => (' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
BorderStyle::Plain => ('┌', '┐', '└', '┘', '─', '─', '│', '│'),
BorderStyle::Rounded => ('╭', '╮', '╰', '╯', '─', '─', '│', '│'),
BorderStyle::Double => ('╔', '╗', '╚', '╝', '═', '═', '║', '║'),
BorderStyle::Thick => ('┏', '┓', '┗', '┛', '━', '━', '┃', '┃'),
BorderStyle::Custom {
top_left,
top_right,
bottom_left,
bottom_right,
top,
bottom,
left,
right,
} => (
*top_left,
*top_right,
*bottom_left,
*bottom_right,
*top,
*bottom,
*left,
*right,
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_inner_defaults_to_one_cell_border_only() {
let block = Block::new("title");
assert_eq!(block.inner(Rect::new(0, 0, 10, 6)), Rect::new(1, 1, 8, 4));
}
#[test]
fn block_builder_aliases_match_old_shape() {
let block = Block::bordered()
.title("demo")
.border_style(Style::new().fg(Color::CYAN));
assert_eq!(block.title, "demo");
assert_eq!(block.borders, BorderStyle::Plain);
assert_eq!(block.border_color, Color::CYAN);
}
}
impl<'a> Widget for Block<'a> {
fn render(&self, buffer: &mut Buffer, area: Rect) {
if area.width < 2 || area.height < 2 {
return;
}
if let Some(bg) = self.bg {
buffer.fill(area, ' ', Color::WHITE, Some(bg));
}
if self.borders == BorderStyle::None {
return;
}
let (tl, tr, bl, br, top, bot, l, r) = self.chars_for_border(&self.borders);
for x in (area.x + 1) as usize..(area.right() - 1) as usize {
buffer.set(
x,
area.y as usize,
crate::core::buffer::Cell {
ch: top,
fg: self.border_color,
bg: self.bg,
bold: false,
italic: false,
underlined: false,
},
);
buffer.set(
x,
(area.bottom() - 1) as usize,
crate::core::buffer::Cell {
ch: bot,
fg: self.border_color,
bg: self.bg,
bold: false,
italic: false,
underlined: false,
},
);
}
for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
buffer.set(
area.x as usize,
y,
crate::core::buffer::Cell {
ch: l,
fg: self.border_color,
bg: self.bg,
bold: false,
italic: false,
underlined: false,
},
);
buffer.set(
(area.right() - 1) as usize,
y,
crate::core::buffer::Cell {
ch: r,
fg: self.border_color,
bg: self.bg,
bold: false,
italic: false,
underlined: false,
},
);
}
buffer.set(
area.x as usize,
area.y as usize,
crate::core::buffer::Cell {
ch: tl,
fg: self.border_color,
bg: self.bg,
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
(area.right() - 1) as usize,
area.y as usize,
crate::core::buffer::Cell {
ch: tr,
fg: self.border_color,
bg: self.bg,
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
area.x as usize,
(area.bottom() - 1) as usize,
crate::core::buffer::Cell {
ch: bl,
fg: self.border_color,
bg: self.bg,
bold: true,
italic: false,
underlined: false,
},
);
buffer.set(
(area.right() - 1) as usize,
(area.bottom() - 1) as usize,
crate::core::buffer::Cell {
ch: br,
fg: self.border_color,
bg: self.bg,
bold: true,
italic: false,
underlined: false,
},
);
if !self.title.is_empty() {
let title_text = format!(" {} ", self.title);
let max_title_w = area.width.saturating_sub(4) as usize;
let display: String = title_text.chars().take(max_title_w).collect();
let title_x = (area.x + 2) as usize;
buffer.set_str_bold(
title_x,
area.y as usize,
&display,
self.border_color.brighten(0.25),
self.bg,
);
}
if let Some(right_title) = self.title_right {
let rt = format!(" {} ", right_title);
let max_title_w = area.width.saturating_sub(4) as usize;
let display: String = rt.chars().take(max_title_w).collect();
let rx = (area.right() as usize).saturating_sub(display.len() + 2);
buffer.set_str_bold(
rx,
area.y as usize,
&display,
self.border_color.brighten(0.18),
self.bg,
);
}
}
}