#![forbid(unsafe_code)]
use makeover_layout::{Bevel, Depth, Edge, Fill};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
pub use makeover_layout;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Fidelity {
Ansi16,
Ansi256,
#[default]
TrueColor,
}
impl Fidelity {
#[must_use]
pub fn detect() -> Self {
let colorterm = std::env::var("COLORTERM").unwrap_or_default();
if colorterm.contains("truecolor") || colorterm.contains("24bit") {
return Self::TrueColor;
}
let term = std::env::var("TERM").unwrap_or_default();
if term.contains("256color") || term.contains("direct") {
return Self::Ansi256;
}
if term.is_empty() {
return Self::TrueColor;
}
Self::Ansi16
}
#[must_use]
pub const fn separates_depth(self) -> bool {
!matches!(self, Self::Ansi16)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Palette {
pub page: Color,
pub raised: Color,
pub overlay: Color,
pub well: Option<Color>,
pub bevel_light: Color,
pub bevel_dark: Color,
pub fidelity: Fidelity,
}
impl Palette {
#[must_use]
pub const fn fill(&self, fill: Fill) -> Option<Color> {
match fill {
Fill::Page => Some(self.page),
Fill::Raised => Some(self.raised),
Fill::Overlay => Some(self.overlay),
Fill::Well => self.well,
_ => None,
}
}
#[must_use]
pub const fn edge(&self, edge: Edge) -> Color {
match edge {
Edge::Light => self.bevel_light,
Edge::Dark => self.bevel_dark,
}
}
#[must_use]
pub fn shows(fill: Color, behind: Color) -> bool {
fill != behind
}
#[must_use]
pub fn two_tone(&self) -> bool {
self.bevel_light != self.bevel_dark
}
#[must_use]
pub const fn needs_glyph_depth(&self) -> bool {
!self.fidelity.separates_depth()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GlyphSet {
pub(crate) top: &'static str,
pub(crate) bottom: &'static str,
pub(crate) left: &'static str,
pub(crate) right: &'static str,
pub(crate) top_left: &'static str,
pub(crate) top_right: &'static str,
pub(crate) bottom_left: &'static str,
pub(crate) bottom_right: &'static str,
pub(crate) split_corners: bool,
}
pub(crate) const BEVEL: GlyphSet = GlyphSet {
top: "▀",
bottom: "▄",
left: "▌",
right: "▐",
top_left: "▛",
top_right: "▀",
bottom_left: "▄",
bottom_right: "▟",
split_corners: true,
};
pub(crate) const LIGHT: GlyphSet = GlyphSet {
top: "─",
bottom: "─",
left: "│",
right: "│",
top_left: "┌",
top_right: "┐",
bottom_left: "└",
bottom_right: "┘",
split_corners: false,
};
pub(crate) const DOUBLE: GlyphSet = GlyphSet {
top: "═",
bottom: "═",
left: "║",
right: "║",
top_left: "╔",
top_right: "╗",
bottom_left: "╚",
bottom_right: "╝",
split_corners: false,
};
pub fn paint_bevel(buf: &mut Buffer, area: Rect, bevel: Bevel, palette: &Palette) {
paint_bevel_with(buf, area, bevel, palette, set_for(palette, None));
}
fn set_for(palette: &Palette, depth: Option<Depth>) -> GlyphSet {
if !palette.needs_glyph_depth() {
return BEVEL;
}
match depth {
Some(Depth::Raised) => DOUBLE,
_ => LIGHT,
}
}
fn paint_bevel_with(buf: &mut Buffer, area: Rect, bevel: Bevel, palette: &Palette, set: GlyphSet) {
if area.width < 2 || area.height < 2 {
return;
}
let (top_left, bottom_right) = bevel.edges();
let light = palette.edge(top_left);
let dark = palette.edge(bottom_right);
let (x0, y0) = (area.x, area.y);
let (x1, y1) = (area.right() - 1, area.bottom() - 1);
for x in x0..=x1 {
buf[(x, y0)].set_symbol(set.top).set_fg(light);
}
for y in y0..=y1 {
buf[(x0, y)].set_symbol(set.left).set_fg(light);
}
for x in x0..=x1 {
buf[(x, y1)].set_symbol(set.bottom).set_fg(dark);
}
for y in y0..=y1 {
buf[(x1, y)].set_symbol(set.right).set_fg(dark);
}
buf[(x0, y0)].set_symbol(set.top_left).set_fg(light);
buf[(x1, y1)].set_symbol(set.bottom_right).set_fg(dark);
if set.split_corners {
buf[(x1, y0)]
.set_symbol(set.top_right)
.set_fg(light)
.set_bg(dark);
buf[(x0, y1)]
.set_symbol(set.bottom_left)
.set_fg(dark)
.set_bg(light);
} else {
buf[(x1, y0)].set_symbol(set.top_right).set_fg(dark);
buf[(x0, y1)].set_symbol(set.bottom_left).set_fg(dark);
}
}
pub fn frame(buf: &mut Buffer, area: Rect, depth: Depth, palette: &Palette) -> Rect {
if area.is_empty() {
return area;
}
let behind = buf[(area.x, area.y)].bg;
if let Some(color) = depth.fill().and_then(|f| palette.fill(f))
&& Palette::shows(color, behind)
{
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
buf[(x, y)].set_bg(color);
}
}
}
match depth.bevel() {
Some(bevel) if area.width >= 2 && area.height >= 2 => {
let set = set_for(palette, Some(depth));
paint_bevel_with(buf, area, bevel, palette, set);
Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2)
}
_ => area,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn palette(well: Option<Color>) -> Palette {
Palette {
page: Color::Indexed(7),
raised: Color::Indexed(15),
overlay: Color::Indexed(8),
well,
bevel_light: Color::Indexed(15),
bevel_dark: Color::Indexed(0),
fidelity: Fidelity::TrueColor,
}
}
fn buffer() -> Buffer {
Buffer::empty(Rect::new(0, 0, 6, 4))
}
#[test]
fn a_well_that_cannot_be_coloured_is_still_drawn() {
let p = palette(None);
let mut buf = buffer();
frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Well, &p);
assert_eq!(buf[(0, 0)].symbol(), BEVEL.top_left);
assert_eq!(buf[(0, 0)].bg, Color::Reset);
}
#[test]
fn a_fill_that_matches_its_surroundings_is_not_painted() {
let p = palette(Some(Color::Indexed(7)));
let mut buf = buffer();
for y in 0..4 {
for x in 0..6 {
buf[(x, y)].set_bg(Color::Indexed(7));
}
}
frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Well, &p);
assert!(!Palette::shows(Color::Indexed(7), Color::Indexed(7)));
assert_eq!(buf[(5, 3)].symbol(), BEVEL.bottom_right);
}
#[test]
fn a_visible_fill_is_painted() {
let p = palette(Some(Color::Indexed(4)));
let mut buf = buffer();
frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Well, &p);
assert_eq!(buf[(2, 2)].bg, Color::Indexed(4));
}
#[test]
fn the_light_falls_from_the_top_left() {
let p = palette(None);
let mut buf = buffer();
paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised, &p);
assert_eq!(buf[(0, 0)].fg, p.bevel_light); assert_eq!(buf[(3, 0)].fg, p.bevel_light); assert_eq!(buf[(0, 2)].fg, p.bevel_light); assert_eq!(buf[(5, 3)].fg, p.bevel_dark); assert_eq!(buf[(3, 3)].fg, p.bevel_dark); assert_eq!(buf[(5, 2)].fg, p.bevel_dark); }
#[test]
fn the_shared_corners_carry_both_tones_when_the_glyph_can_split() {
let p = palette(None);
let mut buf = buffer();
paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised, &p);
let top_right = &buf[(5, 0)];
assert_eq!(top_right.fg, p.bevel_light);
assert_eq!(top_right.bg, p.bevel_dark);
let bottom_left = &buf[(0, 3)];
assert_eq!(bottom_left.fg, p.bevel_dark);
assert_eq!(bottom_left.bg, p.bevel_light);
}
#[test]
fn box_drawing_corners_stay_dark_and_match_the_immediate_renderer() {
let p = Palette {
fidelity: Fidelity::Ansi16,
..palette(None)
};
let mut buf = buffer();
paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised, &p);
assert_eq!(buf[(5, 0)].symbol(), LIGHT.top_right);
assert_eq!(buf[(5, 0)].fg, p.bevel_dark);
assert_eq!(buf[(5, 0)].bg, Color::Reset, "a stroke has no second tone");
assert_eq!(buf[(0, 3)].fg, p.bevel_dark);
}
#[test]
fn a_bevel_draws_an_even_outline_and_leaves_the_middle_alone() {
let p = palette(None);
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 4));
paint_bevel(&mut buf, Rect::new(0, 0, 5, 4), Bevel::Raised, &p);
let rows: Vec<String> = (0..4)
.map(|y| (0..5).map(|x| buf[(x, y)].symbol()).collect())
.collect();
assert_eq!(rows, vec!["▛▀▀▀▀", "▌ ▐", "▌ ▐", "▄▄▄▄▟"]);
}
#[test]
fn pressing_swaps_the_lit_side() {
let p = palette(None);
let mut buf = buffer();
paint_bevel(&mut buf, Rect::new(0, 0, 6, 4), Bevel::Raised.pressed(), &p);
assert_eq!(buf[(0, 0)].fg, p.bevel_dark);
}
#[test]
fn a_sixteen_colour_terminal_can_lose_the_second_tone() {
let flat = Palette {
bevel_dark: Color::Indexed(15),
..palette(None)
};
assert!(!flat.two_tone());
assert!(palette(None).two_tone());
}
#[test]
fn an_edge_costs_a_cell_on_every_side() {
let p = palette(None);
let mut buf = buffer();
let inner = frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Raised, &p);
assert_eq!(inner, Rect::new(1, 1, 4, 2));
let same = frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Flat, &p);
assert_eq!(same, Rect::new(0, 0, 6, 4));
}
#[test]
fn sixteen_colours_carries_depth_in_the_glyphs_instead() {
let p = Palette {
fidelity: Fidelity::Ansi16,
..palette(None)
};
assert!(p.needs_glyph_depth());
let mut raised = buffer();
frame(&mut raised, Rect::new(0, 0, 6, 4), Depth::Raised, &p);
let mut well = buffer();
frame(&mut well, Rect::new(0, 0, 6, 4), Depth::Well, &p);
assert_eq!(raised[(0, 0)].symbol(), DOUBLE.top_left);
assert_eq!(well[(0, 0)].symbol(), LIGHT.top_left);
assert_ne!(raised[(0, 0)].symbol(), well[(0, 0)].symbol());
}
#[test]
fn above_sixteen_colours_the_glyphs_stay_out_of_it() {
for f in [Fidelity::Ansi256, Fidelity::TrueColor] {
let p = Palette {
fidelity: f,
..palette(Some(Color::Indexed(4)))
};
assert!(!p.needs_glyph_depth());
let mut buf = buffer();
frame(&mut buf, Rect::new(0, 0, 6, 4), Depth::Raised, &p);
assert_eq!(
buf[(0, 0)].symbol(),
BEVEL.top_left,
"{f:?} got a heavier frame"
);
assert_ne!(buf[(0, 0)].symbol(), DOUBLE.top_left);
}
}
#[test]
fn colour_alone_separates_raised_from_well_where_it_can() {
let p = palette(Some(Color::Indexed(4)));
let mut raised = buffer();
frame(&mut raised, Rect::new(0, 0, 6, 4), Depth::Raised, &p);
let mut well = buffer();
frame(&mut well, Rect::new(0, 0, 6, 4), Depth::Well, &p);
assert_eq!(raised[(0, 0)].symbol(), well[(0, 0)].symbol());
assert_eq!(raised[(0, 0)].fg, p.bevel_light);
assert_eq!(well[(0, 0)].fg, p.bevel_dark);
}
#[test]
fn detection_defaults_generously_and_only_downgrades_on_evidence() {
assert!(Fidelity::default().separates_depth());
assert!(Fidelity::TrueColor.separates_depth());
assert!(Fidelity::Ansi256.separates_depth());
assert!(!Fidelity::Ansi16.separates_depth());
}
#[test]
fn a_region_too_small_for_an_edge_is_left_alone() {
let p = palette(None);
let mut buf = buffer();
let inner = frame(&mut buf, Rect::new(0, 0, 1, 1), Depth::Raised, &p);
assert_eq!(inner, Rect::new(0, 0, 1, 1));
}
}