#![forbid(unsafe_code)]
use makeover_layout::{Bevel, Depth, Edge, Fill};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
#[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,
}
}
#[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) horizontal: &'static str,
pub(crate) vertical: &'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) const LIGHT: GlyphSet = GlyphSet {
horizontal: "─",
vertical: "│",
top_left: "┌",
top_right: "┐",
bottom_left: "└",
bottom_right: "┘",
};
pub(crate) const DOUBLE: GlyphSet = GlyphSet {
horizontal: "═",
vertical: "║",
top_left: "╔",
top_right: "╗",
bottom_left: "╚",
bottom_right: "╝",
};
pub fn paint_bevel(buf: &mut Buffer, area: Rect, bevel: Bevel, palette: &Palette) {
paint_bevel_with(buf, area, bevel, palette, 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.horizontal).set_fg(light);
}
for y in y0..=y1 {
buf[(x0, y)].set_symbol(set.vertical).set_fg(light);
}
for x in x0..=x1 {
buf[(x, y1)].set_symbol(set.horizontal).set_fg(dark);
}
for y in y0..=y1 {
buf[(x1, y)].set_symbol(set.vertical).set_fg(dark);
}
buf[(x0, y0)].set_symbol(set.top_left).set_fg(light);
buf[(x1, y0)].set_symbol(set.top_right).set_fg(dark);
buf[(x0, y1)].set_symbol(set.bottom_left).set_fg(dark);
buf[(x1, y1)].set_symbol(set.bottom_right).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 = match (palette.needs_glyph_depth(), depth) {
(true, Depth::Raised) => DOUBLE,
_ => LIGHT,
};
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(), LIGHT.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(), LIGHT.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_and_corners_go_dark() {
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[(5, 0)].fg, p.bevel_dark);
assert_eq!(buf[(0, 3)].fg, p.bevel_dark);
}
#[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(),
LIGHT.top_left,
"{f:?} got a heavier frame"
);
}
}
#[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));
}
}