use crate::layout::Rect;
use crate::render::Cell;
use crate::style::Color;
use crate::widget::RenderContext;
#[derive(Clone, Copy, Debug)]
pub struct BorderChars {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
}
impl BorderChars {
pub const SINGLE: Self = Self {
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
horizontal: '─',
vertical: '│',
};
pub const ROUNDED: Self = Self {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
};
pub const DOUBLE: Self = Self {
top_left: '╔',
top_right: '╗',
bottom_left: '╚',
bottom_right: '╝',
horizontal: '═',
vertical: '║',
};
pub const BOLD: Self = Self {
top_left: '┏',
top_right: '┓',
bottom_left: '┗',
bottom_right: '┛',
horizontal: '━',
vertical: '┃',
};
pub const ASCII: Self = Self {
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
horizontal: '-',
vertical: '|',
};
}
impl Default for BorderChars {
fn default() -> Self {
Self::SINGLE
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct BorderStyle {
pub chars: BorderChars,
pub color: Option<Color>,
pub bg: Option<Color>,
}
impl BorderStyle {
pub fn new(color: Color) -> Self {
Self {
chars: BorderChars::SINGLE,
color: Some(color),
bg: None,
}
}
pub fn rounded(mut self) -> Self {
self.chars = BorderChars::ROUNDED;
self
}
pub fn double(mut self) -> Self {
self.chars = BorderChars::DOUBLE;
self
}
pub fn bold(mut self) -> Self {
self.chars = BorderChars::BOLD;
self
}
pub fn ascii(mut self) -> Self {
self.chars = BorderChars::ASCII;
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
}
pub fn render_border(ctx: &mut RenderContext, area: Rect, color: Color) {
render_border_with_style(ctx, area, BorderStyle::new(color));
}
pub fn render_rounded_border(ctx: &mut RenderContext, area: Rect, color: Color) {
render_border_with_style(ctx, area, BorderStyle::new(color).rounded());
}
pub fn render_border_with_style(ctx: &mut RenderContext, area: Rect, style: BorderStyle) {
if area.width < 2 || area.height < 2 {
return;
}
let chars = style.chars;
let fg = style.color;
let bg = style.bg;
for x in area.x..area.x + area.width {
let ch = if x == area.x {
chars.top_left
} else if x == area.x + area.width - 1 {
chars.top_right
} else {
chars.horizontal
};
let mut cell = Cell::new(ch);
cell.fg = fg;
cell.bg = bg;
ctx.buffer.set(x, area.y, cell);
}
for x in area.x..area.x + area.width {
let ch = if x == area.x {
chars.bottom_left
} else if x == area.x + area.width - 1 {
chars.bottom_right
} else {
chars.horizontal
};
let mut cell = Cell::new(ch);
cell.fg = fg;
cell.bg = bg;
ctx.buffer.set(x, area.y + area.height - 1, cell);
}
for y in area.y + 1..area.y + area.height - 1 {
let mut left = Cell::new(chars.vertical);
left.fg = fg;
left.bg = bg;
ctx.buffer.set(area.x, y, left);
let mut right = Cell::new(chars.vertical);
right.fg = fg;
right.bg = bg;
ctx.buffer.set(area.x + area.width - 1, y, right);
}
}
pub fn fill_bg(ctx: &mut RenderContext, area: Rect, color: Color) {
for y in area.y..area.y + area.height {
for x in area.x..area.x + area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(color);
ctx.buffer.set(x, y, cell);
}
}
}
pub fn fill_inner_bg(ctx: &mut RenderContext, area: Rect, color: Color) {
if area.width <= 2 || area.height <= 2 {
return;
}
let inner = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2);
fill_bg(ctx, inner, color);
}
pub fn draw_hline(ctx: &mut RenderContext, x: u16, y: u16, width: u16, color: Color) {
for dx in 0..width {
let mut cell = Cell::new('─');
cell.fg = Some(color);
ctx.buffer.set(x + dx, y, cell);
}
}
pub fn draw_vline(ctx: &mut RenderContext, x: u16, y: u16, height: u16, color: Color) {
for dy in 0..height {
let mut cell = Cell::new('│');
cell.fg = Some(color);
ctx.buffer.set(x, y + dy, cell);
}
}
pub fn draw_separator(ctx: &mut RenderContext, area: Rect, y: u16, color: Color) {
if y <= area.y || y >= area.y + area.height - 1 {
return;
}
let mut left = Cell::new('├');
left.fg = Some(color);
ctx.buffer.set(area.x, y, left);
for x in area.x + 1..area.x + area.width - 1 {
let mut h = Cell::new('─');
h.fg = Some(color);
ctx.buffer.set(x, y, h);
}
let mut right = Cell::new('┤');
right.fg = Some(color);
ctx.buffer.set(area.x + area.width - 1, y, right);
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum TitlePosition {
#[default]
Start,
Center,
End,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum BorderEdge {
#[default]
Top,
Bottom,
Left,
Right,
}
#[derive(Clone, Debug)]
pub struct BorderTitle {
pub text: String,
pub edge: BorderEdge,
pub position: TitlePosition,
pub fg: Option<Color>,
pub bg: Option<Color>,
pub pad_start: u16,
pub pad_end: u16,
pub offset: i16,
}
impl BorderTitle {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
edge: BorderEdge::Top,
position: TitlePosition::Start,
fg: None,
bg: None,
pad_start: 1,
pad_end: 1,
offset: 0,
}
}
pub fn edge(mut self, edge: BorderEdge) -> Self {
self.edge = edge;
self
}
pub fn position(mut self, pos: TitlePosition) -> Self {
self.position = pos;
self
}
pub fn top(mut self) -> Self {
self.edge = BorderEdge::Top;
self
}
pub fn bottom(mut self) -> Self {
self.edge = BorderEdge::Bottom;
self
}
pub fn left(mut self) -> Self {
self.edge = BorderEdge::Left;
self
}
pub fn right(mut self) -> Self {
self.edge = BorderEdge::Right;
self
}
pub fn start(mut self) -> Self {
self.position = TitlePosition::Start;
self
}
pub fn center(mut self) -> Self {
self.position = TitlePosition::Center;
self
}
pub fn end(mut self) -> Self {
self.position = TitlePosition::End;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn padding(mut self, pad: u16) -> Self {
self.pad_start = pad;
self.pad_end = pad;
self
}
pub fn pad_start(mut self, pad: u16) -> Self {
self.pad_start = pad;
self
}
pub fn pad_end(mut self, pad: u16) -> Self {
self.pad_end = pad;
self
}
pub fn offset(mut self, offset: i16) -> Self {
self.offset = offset;
self
}
pub fn width(&self) -> u16 {
crate::utils::unicode::display_width(&self.text) as u16 + self.pad_start + self.pad_end
}
}
pub fn draw_border_title(ctx: &mut RenderContext, area: Rect, title: &BorderTitle) {
if area.width < 3 || area.height < 2 {
return;
}
let text_width = crate::utils::unicode::display_width(&title.text) as u16;
let total_width = text_width + title.pad_start + title.pad_end;
match title.edge {
BorderEdge::Top | BorderEdge::Bottom => {
let available = area.width.saturating_sub(2); if total_width > available {
return;
}
let y = if title.edge == BorderEdge::Top {
area.y
} else {
area.y + area.height - 1
};
let base_x = match title.position {
TitlePosition::Start => area.x + 1,
TitlePosition::Center => area.x + 1 + (available.saturating_sub(total_width)) / 2,
TitlePosition::End => area.x + area.width - 1 - total_width,
};
let x = (base_x as i16 + title.offset).max(area.x as i16 + 1) as u16;
for dx in 0..title.pad_start {
let mut cell = Cell::new(' ');
cell.fg = title.fg;
cell.bg = title.bg;
ctx.buffer.set(x + dx, y, cell);
}
let text_x = x + title.pad_start;
for (i, ch) in title.text.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = title.fg;
cell.bg = title.bg;
ctx.buffer.set(text_x + i as u16, y, cell);
}
let end_x = text_x + text_width;
for dx in 0..title.pad_end {
let mut cell = Cell::new(' ');
cell.fg = title.fg;
cell.bg = title.bg;
ctx.buffer.set(end_x + dx, y, cell);
}
}
BorderEdge::Left | BorderEdge::Right => {
let available = area.height.saturating_sub(2); let text_len = title.text.chars().count() as u16;
let total_height = text_len + title.pad_start + title.pad_end;
if total_height > available {
return;
}
let x = if title.edge == BorderEdge::Left {
area.x
} else {
area.x + area.width - 1
};
let base_y = match title.position {
TitlePosition::Start => area.y + 1,
TitlePosition::Center => area.y + 1 + (available.saturating_sub(total_height)) / 2,
TitlePosition::End => area.y + area.height - 1 - total_height,
};
let y = (base_y as i16 + title.offset).max(area.y as i16 + 1) as u16;
for dy in 0..title.pad_start {
let mut cell = Cell::new(' ');
cell.fg = title.fg;
cell.bg = title.bg;
ctx.buffer.set(x, y + dy, cell);
}
let text_y = y + title.pad_start;
for (i, ch) in title.text.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = title.fg;
cell.bg = title.bg;
ctx.buffer.set(x, text_y + i as u16, cell);
}
let end_y = text_y + text_len;
for dy in 0..title.pad_end {
let mut cell = Cell::new(' ');
cell.fg = title.fg;
cell.bg = title.bg;
ctx.buffer.set(x, end_y + dy, cell);
}
}
}
}
pub fn draw_border_titles(ctx: &mut RenderContext, area: Rect, titles: &[BorderTitle]) {
for title in titles {
draw_border_title(ctx, area, title);
}
}
pub fn draw_title(ctx: &mut RenderContext, area: Rect, text: &str, color: Color) {
draw_border_title(ctx, area, &BorderTitle::new(text).fg(color));
}
pub fn draw_title_right(ctx: &mut RenderContext, area: Rect, text: &str, color: Color) {
draw_border_title(ctx, area, &BorderTitle::new(text).end().fg(color));
}
pub fn draw_title_center(ctx: &mut RenderContext, area: Rect, text: &str, color: Color) {
draw_border_title(ctx, area, &BorderTitle::new(text).center().fg(color));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::Buffer;
#[test]
fn test_border_chars() {
assert_eq!(BorderChars::SINGLE.top_left, '┌');
assert_eq!(BorderChars::ROUNDED.top_left, '╭');
assert_eq!(BorderChars::DOUBLE.top_left, '╔');
}
#[test]
fn test_border_style() {
let style = BorderStyle::new(Color::WHITE).rounded();
assert_eq!(style.chars.top_left, '╭');
}
#[test]
fn test_render_border() {
let mut buffer = Buffer::new(10, 5);
let area = Rect::new(0, 0, 10, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
assert_eq!(buffer.get(0, 0).unwrap().symbol, '┌');
assert_eq!(buffer.get(9, 0).unwrap().symbol, '┐');
assert_eq!(buffer.get(0, 4).unwrap().symbol, '└');
assert_eq!(buffer.get(9, 4).unwrap().symbol, '┘');
assert_eq!(buffer.get(5, 0).unwrap().symbol, '─');
assert_eq!(buffer.get(0, 2).unwrap().symbol, '│');
}
#[test]
fn test_render_rounded_border() {
let mut buffer = Buffer::new(10, 5);
let area = Rect::new(0, 0, 10, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_rounded_border(&mut ctx, area, Color::WHITE);
assert_eq!(buffer.get(0, 0).unwrap().symbol, '╭');
assert_eq!(buffer.get(9, 0).unwrap().symbol, '╮');
}
#[test]
fn test_fill_bg() {
let mut buffer = Buffer::new(10, 5);
let area = Rect::new(0, 0, 10, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
fill_bg(&mut ctx, area, Color::RED);
assert_eq!(buffer.get(5, 2).unwrap().bg, Some(Color::RED));
}
#[test]
fn test_draw_separator() {
let mut buffer = Buffer::new(10, 5);
let area = Rect::new(0, 0, 10, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
draw_separator(&mut ctx, area, 2, Color::WHITE);
assert_eq!(buffer.get(0, 2).unwrap().symbol, '├');
assert_eq!(buffer.get(9, 2).unwrap().symbol, '┤');
}
#[test]
fn test_border_title_builder() {
let title = BorderTitle::new("Test")
.top()
.start()
.fg(Color::BLUE)
.padding(2);
assert_eq!(title.text, "Test");
assert_eq!(title.edge, BorderEdge::Top);
assert_eq!(title.position, TitlePosition::Start);
assert_eq!(title.fg, Some(Color::BLUE));
assert_eq!(title.pad_start, 2);
assert_eq!(title.pad_end, 2);
}
#[test]
fn test_border_title_width() {
let title = BorderTitle::new("Hello").padding(1);
assert_eq!(title.width(), 7); }
#[test]
fn test_draw_border_title_start() {
let mut buffer = Buffer::new(20, 5);
let area = Rect::new(0, 0, 20, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
draw_border_title(&mut ctx, area, &BorderTitle::new("Title").padding(1));
assert_eq!(buffer.get(1, 0).unwrap().symbol, ' ');
assert_eq!(buffer.get(2, 0).unwrap().symbol, 'T');
assert_eq!(buffer.get(3, 0).unwrap().symbol, 'i');
assert_eq!(buffer.get(6, 0).unwrap().symbol, 'e');
assert_eq!(buffer.get(7, 0).unwrap().symbol, ' ');
}
#[test]
fn test_draw_border_title_end() {
let mut buffer = Buffer::new(20, 5);
let area = Rect::new(0, 0, 20, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
draw_border_title(&mut ctx, area, &BorderTitle::new("End").end().padding(1));
assert_eq!(buffer.get(14, 0).unwrap().symbol, ' ');
assert_eq!(buffer.get(15, 0).unwrap().symbol, 'E');
assert_eq!(buffer.get(16, 0).unwrap().symbol, 'n');
assert_eq!(buffer.get(17, 0).unwrap().symbol, 'd');
}
#[test]
fn test_draw_border_title_center() {
let mut buffer = Buffer::new(20, 5);
let area = Rect::new(0, 0, 20, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
draw_border_title(&mut ctx, area, &BorderTitle::new("Hi").center().padding(1));
assert_eq!(buffer.get(9, 0).unwrap().symbol, 'H');
assert_eq!(buffer.get(10, 0).unwrap().symbol, 'i');
}
#[test]
fn test_draw_border_title_bottom() {
let mut buffer = Buffer::new(20, 5);
let area = Rect::new(0, 0, 20, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
draw_border_title(
&mut ctx,
area,
&BorderTitle::new("Bottom").bottom().padding(1),
);
assert_eq!(buffer.get(2, 4).unwrap().symbol, 'B');
assert_eq!(buffer.get(7, 4).unwrap().symbol, 'm');
}
#[test]
fn test_draw_multiple_titles() {
let mut buffer = Buffer::new(30, 5);
let area = Rect::new(0, 0, 30, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
draw_border_titles(
&mut ctx,
area,
&[
BorderTitle::new("Left").start(),
BorderTitle::new("Right").end(),
],
);
assert_eq!(buffer.get(2, 0).unwrap().symbol, 'L');
assert_eq!(buffer.get(23, 0).unwrap().symbol, 'R');
}
#[test]
fn test_draw_title_convenience() {
let mut buffer = Buffer::new(20, 5);
let area = Rect::new(0, 0, 20, 5);
let mut ctx = RenderContext::new(&mut buffer, area);
render_border(&mut ctx, area, Color::WHITE);
draw_title(&mut ctx, area, "Test", Color::BLUE);
assert_eq!(buffer.get(2, 0).unwrap().symbol, 'T');
assert_eq!(buffer.get(2, 0).unwrap().fg, Some(Color::BLUE));
}
}