#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BorderSet {
pub vertical: char,
pub horizontal: char,
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub tee_up: char,
pub tee_down: char,
pub tee_left: char,
pub tee_right: char,
pub cross: char,
}
impl BorderSet {
pub const ASCII: Self = Self {
vertical: '|',
horizontal: '-',
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
tee_up: '+',
tee_down: '+',
tee_left: '+',
tee_right: '+',
cross: '+',
};
pub const ROUNDED: Self = Self {
vertical: '│',
horizontal: '─',
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
tee_up: '┴',
tee_down: '┬',
tee_left: '┤',
tee_right: '├',
cross: '┼',
};
pub const SQUARE: Self = Self {
vertical: '│',
horizontal: '─',
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
tee_up: '┴',
tee_down: '┬',
tee_left: '┤',
tee_right: '├',
cross: '┼',
};
pub const DOUBLE: Self = Self {
vertical: '║',
horizontal: '═',
top_left: '╔',
top_right: '╗',
bottom_left: '╚',
bottom_right: '╝',
tee_up: '╩',
tee_down: '╦',
tee_left: '╣',
tee_right: '╠',
cross: '╬',
};
pub const HEAVY: Self = Self {
vertical: '┃',
horizontal: '━',
top_left: '┏',
top_right: '┓',
bottom_left: '┗',
bottom_right: '┛',
tee_up: '┻',
tee_down: '┳',
tee_left: '┫',
tee_right: '┣',
cross: '╋',
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BorderType {
#[default]
Square,
Ascii,
Rounded,
Double,
Heavy,
Custom(BorderSet),
}
impl BorderType {
pub fn to_border_set(&self) -> BorderSet {
match self {
BorderType::Square => BorderSet::SQUARE,
BorderType::Ascii => BorderSet::ASCII,
BorderType::Rounded => BorderSet::ROUNDED,
BorderType::Double => BorderSet::DOUBLE,
BorderType::Heavy => BorderSet::HEAVY,
BorderType::Custom(set) => *set,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_is_ascii_only() {
let set = BorderSet::ASCII;
let chars = [
set.vertical,
set.horizontal,
set.top_left,
set.top_right,
set.bottom_left,
set.bottom_right,
set.tee_up,
set.tee_down,
set.tee_left,
set.tee_right,
set.cross,
];
assert!(chars.iter().all(|c| c.is_ascii()));
}
#[test]
fn square_has_box_drawing() {
let set = BorderSet::SQUARE;
assert_eq!(set.horizontal, '─');
assert_eq!(set.vertical, '│');
assert_eq!(set.cross, '┼');
}
#[test]
fn rounded_has_round_corners() {
let set = BorderSet::ROUNDED;
assert_eq!(set.top_left, '╭');
assert_eq!(set.top_right, '╮');
assert_eq!(set.bottom_left, '╰');
assert_eq!(set.bottom_right, '╯');
assert_eq!(set.horizontal, '─');
assert_eq!(set.vertical, '│');
}
#[test]
fn double_has_double_lines() {
let set = BorderSet::DOUBLE;
assert_eq!(set.horizontal, '═');
assert_eq!(set.vertical, '║');
assert_eq!(set.top_left, '╔');
assert_eq!(set.top_right, '╗');
assert_eq!(set.bottom_left, '╚');
assert_eq!(set.bottom_right, '╝');
assert_eq!(set.cross, '╬');
}
#[test]
fn heavy_has_heavy_lines() {
let set = BorderSet::HEAVY;
assert_eq!(set.horizontal, '━');
assert_eq!(set.vertical, '┃');
assert_eq!(set.top_left, '┏');
assert_eq!(set.top_right, '┓');
assert_eq!(set.bottom_left, '┗');
assert_eq!(set.bottom_right, '┛');
assert_eq!(set.cross, '╋');
}
#[test]
fn all_border_sets_have_11_fields() {
for set in [
BorderSet::ASCII,
BorderSet::ROUNDED,
BorderSet::SQUARE,
BorderSet::DOUBLE,
BorderSet::HEAVY,
] {
let chars = [
set.vertical,
set.horizontal,
set.top_left,
set.top_right,
set.bottom_left,
set.bottom_right,
set.tee_up,
set.tee_down,
set.tee_left,
set.tee_right,
set.cross,
];
assert_eq!(chars.len(), 11);
assert_ne!(set.horizontal, set.vertical);
}
}
#[test]
fn box_drawing_sets_have_distinct_corners() {
for set in [
BorderSet::ROUNDED,
BorderSet::SQUARE,
BorderSet::DOUBLE,
BorderSet::HEAVY,
] {
let corners = [
set.top_left,
set.top_right,
set.bottom_left,
set.bottom_right,
];
for (i, a) in corners.iter().enumerate() {
for (j, b) in corners.iter().enumerate() {
if i != j {
assert_ne!(a, b, "corners {i} and {j} should differ");
}
}
}
}
}
#[test]
fn ascii_set_reuses_plus_for_junctions() {
let set = BorderSet::ASCII;
assert_eq!(set.top_left, '+');
assert_eq!(set.top_right, '+');
assert_eq!(set.bottom_left, '+');
assert_eq!(set.bottom_right, '+');
assert_eq!(set.tee_up, '+');
assert_eq!(set.tee_down, '+');
assert_eq!(set.tee_left, '+');
assert_eq!(set.tee_right, '+');
assert_eq!(set.cross, '+');
}
#[test]
fn border_type_to_border_set_roundtrip() {
assert_eq!(BorderType::Square.to_border_set(), BorderSet::SQUARE);
assert_eq!(BorderType::Ascii.to_border_set(), BorderSet::ASCII);
assert_eq!(BorderType::Rounded.to_border_set(), BorderSet::ROUNDED);
assert_eq!(BorderType::Double.to_border_set(), BorderSet::DOUBLE);
assert_eq!(BorderType::Heavy.to_border_set(), BorderSet::HEAVY);
}
#[test]
fn border_type_default_is_square() {
assert_eq!(BorderType::default(), BorderType::Square);
}
#[test]
fn border_type_custom_uses_provided_set() {
let custom = BorderSet {
vertical: '!',
horizontal: '-',
top_left: '/',
top_right: '\\',
bottom_left: '\\',
bottom_right: '/',
tee_up: '+',
tee_down: '+',
tee_left: '+',
tee_right: '+',
cross: '*',
};
assert_eq!(BorderType::Custom(custom).to_border_set(), custom);
}
#[test]
fn borders_none_is_zero() {
assert!(Borders::NONE.is_empty());
assert_eq!(Borders::NONE.bits(), 0);
}
#[test]
fn borders_all_contains_all_sides() {
assert!(Borders::ALL.contains(Borders::TOP));
assert!(Borders::ALL.contains(Borders::RIGHT));
assert!(Borders::ALL.contains(Borders::BOTTOM));
assert!(Borders::ALL.contains(Borders::LEFT));
}
#[test]
fn borders_individual_bits_are_distinct() {
let sides = [Borders::TOP, Borders::RIGHT, Borders::BOTTOM, Borders::LEFT];
for (i, a) in sides.iter().enumerate() {
for (j, b) in sides.iter().enumerate() {
if i != j {
assert!(!a.contains(*b), "side {i} should not contain side {j}");
}
}
}
}
#[test]
fn borders_union_and_intersection() {
let top_left = Borders::TOP | Borders::LEFT;
assert!(top_left.contains(Borders::TOP));
assert!(top_left.contains(Borders::LEFT));
assert!(!top_left.contains(Borders::RIGHT));
let top_right = Borders::TOP | Borders::RIGHT;
let intersection = top_left & top_right;
assert!(intersection.contains(Borders::TOP));
assert!(!intersection.contains(Borders::LEFT));
assert!(!intersection.contains(Borders::RIGHT));
}
#[test]
fn borders_default_is_none() {
assert_eq!(Borders::default(), Borders::NONE);
}
#[test]
fn non_ascii_sets_have_no_ascii_chars() {
for set in [
BorderSet::ROUNDED,
BorderSet::SQUARE,
BorderSet::DOUBLE,
BorderSet::HEAVY,
] {
let chars = [
set.vertical,
set.horizontal,
set.top_left,
set.top_right,
set.bottom_left,
set.bottom_right,
set.tee_up,
set.tee_down,
set.tee_left,
set.tee_right,
set.cross,
];
assert!(
chars.iter().all(|c| !c.is_ascii()),
"non-ASCII border set should have no ASCII chars"
);
}
}
#[test]
fn border_set_tees_are_consistent() {
let set = BorderSet::SQUARE;
let tees = [set.tee_up, set.tee_down, set.tee_left, set.tee_right];
for (i, a) in tees.iter().enumerate() {
for (j, b) in tees.iter().enumerate() {
if i != j {
assert_ne!(a, b, "tees {i} and {j} should differ");
}
}
}
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Borders: u8 {
const NONE = 0b0000;
const TOP = 0b0001;
const RIGHT = 0b0010;
const BOTTOM = 0b0100;
const LEFT = 0b1000;
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
}
}