use crate::dom::{BorderStyle, Style};
use crate::render::cli_boxes::{BoxChars, CustomChars, named as named_box};
use crate::render::colorize::{ColorLevel, Kind, colorize, dim as dim_modifier};
use crate::render::grid::Grid;
fn style_piece(
segment: &str,
fg: Option<&str>,
bg: Option<&str>,
dim: bool,
level: ColorLevel,
) -> String {
let styled = colorize(segment, fg, Kind::Fg, level);
let styled = colorize(&styled, bg, Kind::Bg, level);
if dim {
dim_modifier(&styled, level)
} else {
styled
}
}
fn resolve_box(style: &BorderStyle) -> BoxChars {
match style {
BorderStyle::Named(name) => named_box(name).unwrap_or_else(|| {
named_box("single").unwrap()
}),
BorderStyle::Custom {
top_left,
top,
top_right,
right,
bottom_right,
bottom,
bottom_left,
left,
} => BoxChars::from_custom(CustomChars {
top_left: top_left.clone(),
top: top.clone(),
top_right: top_right.clone(),
right: right.clone(),
bottom_right: bottom_right.clone(),
bottom: bottom.clone(),
bottom_left: bottom_left.clone(),
left: left.clone(),
}),
}
}
pub fn render_border(
x: i32,
y: i32,
width: u16,
height: u16,
style: &Style,
grid: &mut Grid,
level: ColorLevel,
) {
let Some(ref border_style) = style.border_style else {
return; };
let bx = resolve_box(border_style);
let top_color = style
.border_top_color
.as_deref()
.or(style.border_color.as_deref());
let bottom_color = style
.border_bottom_color
.as_deref()
.or(style.border_color.as_deref());
let left_color = style
.border_left_color
.as_deref()
.or(style.border_color.as_deref());
let right_color = style
.border_right_color
.as_deref()
.or(style.border_color.as_deref());
let dim_top = style
.border_top_dim_color
.or(style.border_dim_color)
.unwrap_or(false);
let dim_bottom = style
.border_bottom_dim_color
.or(style.border_dim_color)
.unwrap_or(false);
let dim_left = style
.border_left_dim_color
.or(style.border_dim_color)
.unwrap_or(false);
let dim_right = style
.border_right_dim_color
.or(style.border_dim_color)
.unwrap_or(false);
let top_bg = style
.border_top_background_color
.as_deref()
.or(style.border_background_color.as_deref());
let bottom_bg = style
.border_bottom_background_color
.as_deref()
.or(style.border_background_color.as_deref());
let left_bg = style
.border_left_background_color
.as_deref()
.or(style.border_background_color.as_deref());
let right_bg = style
.border_right_background_color
.as_deref()
.or(style.border_background_color.as_deref());
let show_top = style.border_top != Some(false);
let show_bottom = style.border_bottom != Some(false);
let show_left = style.border_left != Some(false);
let show_right = style.border_right != Some(false);
let w = width as i32;
let h = height as i32;
let content_width = w - if show_left { 1 } else { 0 } - if show_right { 1 } else { 0 };
if show_top {
let mut top_str = String::new();
if show_left {
top_str.push_str(&bx.top_left);
}
for _ in 0..content_width.max(0) {
top_str.push_str(&bx.top);
}
if show_right {
top_str.push_str(&bx.top_right);
}
let top_str = style_piece(&top_str, top_color, top_bg, dim_top, level);
grid.write(x, y, &top_str);
}
let mut vert_height = h;
if show_top {
vert_height -= 1;
}
if show_bottom {
vert_height -= 1;
}
if show_left && vert_height > 0 {
let one = style_piece(&bx.left, left_color, left_bg, dim_left, level);
let left_str = std::iter::repeat_n(one.as_str(), vert_height as usize)
.collect::<Vec<_>>()
.join("\n");
let offset_y = if show_top { 1 } else { 0 };
grid.write(x, y + offset_y, &left_str);
}
if show_right && vert_height > 0 {
let one = style_piece(&bx.right, right_color, right_bg, dim_right, level);
let right_str = std::iter::repeat_n(one.as_str(), vert_height as usize)
.collect::<Vec<_>>()
.join("\n");
let offset_y = if show_top { 1 } else { 0 };
grid.write(x + w - 1, y + offset_y, &right_str);
}
if show_bottom {
let mut bot_str = String::new();
if show_left {
bot_str.push_str(&bx.bottom_left);
}
for _ in 0..content_width.max(0) {
bot_str.push_str(&bx.bottom);
}
if show_right {
bot_str.push_str(&bx.bottom_right);
}
let bot_str = style_piece(&bot_str, bottom_color, bottom_bg, dim_bottom, level);
grid.write(x, y + h - 1, &bot_str);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dom::Style;
use crate::render::grid::Grid;
fn style_with_border(name: &str) -> Style {
Style {
border_style: Some(BorderStyle::Named(name.to_owned())),
..Style::default()
}
}
fn render_to_string(rows: usize, cols: usize, style: &Style, w: u16, h: u16) -> String {
let mut g = Grid::new(rows, cols);
render_border(0, 0, w, h, style, &mut g, ColorLevel::Truecolor);
g.get().0
}
#[test]
fn single_border_10x3() {
let s = style_with_border("single");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "┌────────┐\n│ │\n└────────┘");
}
#[test]
fn double_border_10x3() {
let s = style_with_border("double");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "╔════════╗\n║ ║\n╚════════╝");
}
#[test]
fn round_border_10x3() {
let s = style_with_border("round");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "╭────────╮\n│ │\n╰────────╯");
}
#[test]
fn bold_border_10x3() {
let s = style_with_border("bold");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "┏━━━━━━━━┓\n┃ ┃\n┗━━━━━━━━┛");
}
#[test]
fn classic_border_10x3() {
let s = style_with_border("classic");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "+--------+\n| |\n+--------+");
}
#[test]
fn single_double_border_10x3() {
let s = style_with_border("singleDouble");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "╓────────╖\n║ ║\n╙────────╜");
}
#[test]
fn double_single_border_10x3() {
let s = style_with_border("doubleSingle");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "╒════════╕\n│ │\n╘════════╛");
}
#[test]
fn arrow_border_10x3() {
let s = style_with_border("arrow");
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "↘↓↓↓↓↓↓↓↓↙\n→ ←\n↗↑↑↑↑↑↑↑↑↖");
}
#[test]
fn single_no_top_10x3() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_top: Some(false),
..Style::default()
};
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "│ │\n│ │\n└────────┘");
}
#[test]
fn single_no_left_10x3() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_left: Some(false),
..Style::default()
};
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "─────────┐\n │\n─────────┘");
}
#[test]
fn single_no_right_10x3() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_right: Some(false),
..Style::default()
};
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "┌─────────\n│\n└─────────");
}
#[test]
fn single_no_bottom_10x3() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_bottom: Some(false),
..Style::default()
};
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "┌────────┐\n│ │\n│ │");
}
#[test]
fn no_border_style_noop() {
let s = Style::default();
let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "\n\n");
}
#[test]
fn unknown_named_falls_back_to_single() {
let s = style_with_border("singel"); let out = render_to_string(3, 10, &s, 10, 3);
assert_eq!(out, "┌────────┐\n│ │\n└────────┘");
}
fn render_styled(rows: usize, cols: usize, style: &Style, w: u16, h: u16) -> String {
let mut g = Grid::new(rows, cols);
render_border(0, 0, w, h, style, &mut g, ColorLevel::Truecolor);
g.get().0
}
#[test]
fn plain_border_identity_no_sgr() {
let s = style_with_border("single");
let out = render_styled(3, 10, &s, 10, 3);
assert_eq!(out, "┌────────┐\n│ │\n└────────┘");
assert!(!out.contains('\x1b'), "plain border must emit no SGR bytes");
}
fn render_border_at(level: ColorLevel) -> String {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_color: Some("#ff8800".to_owned()),
..Style::default()
};
let mut g = Grid::new(3, 10);
render_border(0, 0, 10, 3, &s, &mut g, level);
g.get().0
}
#[test]
fn hex_border_level_none_emits_no_sgr() {
let out = render_border_at(ColorLevel::None);
assert_eq!(out, "┌────────┐\n│ │\n└────────┘");
assert!(
!out.contains('\x1b'),
"level 0: a hex-colored border must emit NO SGR (plain chars)"
);
}
#[test]
fn hex_border_level_basic_downgrades_to_16() {
let out = render_border_at(ColorLevel::Basic);
assert_eq!(
out,
"\x1b[93m┌────────┐\x1b[39m\n\x1b[93m│\x1b[39m \x1b[93m│\x1b[39m\n\x1b[93m└────────┘\x1b[39m"
);
}
#[test]
fn hex_border_level_ansi256_downgrades_to_256() {
let out = render_border_at(ColorLevel::Ansi256);
assert_eq!(
out,
"\x1b[38;5;214m┌────────┐\x1b[39m\n\x1b[38;5;214m│\x1b[39m \x1b[38;5;214m│\x1b[39m\n\x1b[38;5;214m└────────┘\x1b[39m"
);
}
#[test]
fn hex_border_level_truecolor_verbatim() {
let out = render_border_at(ColorLevel::Truecolor);
assert_eq!(
out,
"\x1b[38;2;255;136;0m┌────────┐\x1b[39m\n\x1b[38;2;255;136;0m│\x1b[39m \x1b[38;2;255;136;0m│\x1b[39m\n\x1b[38;2;255;136;0m└────────┘\x1b[39m"
);
}
#[test]
fn per_edge_color_resolution_cascade() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_color: Some("red".to_owned()), border_top_color: Some("green".to_owned()), ..Style::default()
};
let out = render_styled(3, 10, &s, 10, 3);
assert_eq!(
out,
"\x1b[32m┌────────┐\x1b[39m\n\x1b[31m│\x1b[39m \x1b[31m│\x1b[39m\n\x1b[31m└────────┘\x1b[39m"
);
}
#[test]
fn per_edge_background_resolution_cascade() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_background_color: Some("red".to_owned()), border_top_background_color: Some("blue".to_owned()), ..Style::default()
};
let out = render_styled(3, 10, &s, 10, 3);
assert_eq!(
out,
"\x1b[44m┌────────┐\x1b[49m\n\x1b[41m│\x1b[49m \x1b[41m│\x1b[49m\n\x1b[41m└────────┘\x1b[49m"
);
}
#[test]
fn each_vertical_bar_wrapped_independently() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_left_color: Some("green".to_owned()),
..Style::default()
};
let out = render_styled(4, 10, &s, 10, 4);
assert_eq!(
out,
"┌────────┐\n\x1b[32m│\x1b[39m │\n\x1b[32m│\x1b[39m │\n└────────┘"
);
}
#[test]
fn dim_and_color_composition_order_bytes() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_top_color: Some("green".to_owned()),
border_top_dim_color: Some(true),
..Style::default()
};
let out = render_styled(3, 10, &s, 10, 3);
let top = out.lines().next().unwrap();
assert_eq!(top, "\x1b[2m\x1b[32m┌────────┐\x1b[39m\x1b[22m");
}
#[test]
fn dim_cascade_general_applies_to_all_edges() {
let s = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_dim_color: Some(true),
..Style::default()
};
let out = render_styled(3, 10, &s, 10, 3);
assert_eq!(
out,
"\x1b[2m┌────────┐\x1b[22m\n\x1b[2m│\x1b[22m \x1b[2m│\x1b[22m\n\x1b[2m└────────┘\x1b[22m"
);
}
#[test]
fn dim_cascade_resolution_style_based() {
let per_edge = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_top_dim_color: Some(true),
..Style::default()
};
let out = render_styled(3, 10, &per_edge, 10, 3);
assert_eq!(out, "\x1b[2m┌────────┐\x1b[22m\n│ │\n└────────┘");
let per_edge_wins = Style {
border_style: Some(BorderStyle::Named("single".to_owned())),
border_dim_color: Some(true),
border_top_dim_color: Some(false),
..Style::default()
};
let out = render_styled(3, 10, &per_edge_wins, 10, 3);
assert_eq!(
out,
"┌────────┐\n\x1b[2m│\x1b[22m \x1b[2m│\x1b[22m\n\x1b[2m└────────┘\x1b[22m"
);
}
}