#![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)]
pub struct Palette {
pub page: Color,
pub raised: Color,
pub overlay: Color,
pub well: Option<Color>,
pub bevel_light: Color,
pub bevel_dark: Color,
}
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
}
}
mod glyph {
pub(crate) const HORIZONTAL: &str = "─";
pub(crate) const VERTICAL: &str = "│";
pub(crate) const TOP_LEFT: &str = "┌";
pub(crate) const TOP_RIGHT: &str = "┐";
pub(crate) const BOTTOM_LEFT: &str = "└";
pub(crate) const BOTTOM_RIGHT: &str = "┘";
}
pub fn paint_bevel(buf: &mut Buffer, area: Rect, bevel: Bevel, palette: &Palette) {
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(glyph::HORIZONTAL).set_fg(light);
}
for y in y0..=y1 {
buf[(x0, y)].set_symbol(glyph::VERTICAL).set_fg(light);
}
for x in x0..=x1 {
buf[(x, y1)].set_symbol(glyph::HORIZONTAL).set_fg(dark);
}
for y in y0..=y1 {
buf[(x1, y)].set_symbol(glyph::VERTICAL).set_fg(dark);
}
buf[(x0, y0)].set_symbol(glyph::TOP_LEFT).set_fg(light);
buf[(x1, y0)].set_symbol(glyph::TOP_RIGHT).set_fg(dark);
buf[(x0, y1)].set_symbol(glyph::BOTTOM_LEFT).set_fg(dark);
buf[(x1, y1)].set_symbol(glyph::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 => {
paint_bevel(buf, area, bevel, palette);
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),
}
}
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(), glyph::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(), glyph::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 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));
}
}