use std::collections::HashMap;
use ratatui_core::buffer::{Buffer, Cell};
use ratatui_core::style::{Color, Modifier};
use crate::color::{ColorDepth, Rgb};
use crate::geometry::Padding;
use crate::theme::{Paint, PropValue, StyleProps};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CellStyle {
pub fg: Option<Rgb>,
pub bg: Option<Rgb>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub dim: bool,
}
impl CellStyle {
#[must_use]
pub fn fg(color: Rgb) -> Self {
Self { fg: Some(color), ..Self::default() }
}
#[must_use]
pub fn on(mut self, color: Rgb) -> Self {
self.bg = Some(color);
self
}
#[must_use]
pub fn with_bold(mut self, bold: bool) -> Self {
self.bold = bold;
self
}
#[cfg(test)]
pub(crate) fn apply(self, cell: &mut Cell) {
self.paint().apply(cell);
}
pub(crate) fn paint(self) -> CellPaint {
let mut modifier = Modifier::empty();
modifier.set(Modifier::BOLD, self.bold);
modifier.set(Modifier::ITALIC, self.italic);
modifier.set(Modifier::UNDERLINED, self.underline);
modifier.set(Modifier::DIM, self.dim);
CellPaint { fg: self.fg.map(to_color), bg: self.bg.map(to_color), modifier }
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CellPaint {
fg: Option<Color>,
bg: Option<Color>,
modifier: Modifier,
}
impl CellPaint {
pub(crate) fn apply(self, cell: &mut Cell) {
if let Some(fg) = self.fg {
cell.fg = fg;
}
if let Some(bg) = self.bg {
cell.bg = bg;
}
cell.modifier = self.modifier;
}
}
pub(crate) fn to_color(color: Rgb) -> Color {
Color::Rgb(color.r, color.g, color.b)
}
pub(crate) fn reduce(buf: &mut Buffer, depth: ColorDepth, ground: Rgb) {
match depth {
ColorDepth::TrueColor => {}
ColorDepth::Ansi256 => reduce_with(buf, Rgb::to_ansi256, Rgb::to_ansi256_text, true),
ColorDepth::Ansi16 => {
let text = |text: Rgb, bg| text.to_ansi16_text(bg, ground);
reduce_with(buf, |tone| tone.to_ansi16_on(ground), text, false);
}
}
}
fn is_half_block(symbol: &str) -> bool {
matches!(symbol, "▀" | "▄")
}
fn reduce_with(buf: &mut Buffer, tone: impl Fn(Rgb) -> u8, text: impl Fn(Rgb, Rgb) -> u8, half_blocks_fill: bool) {
let mut tones: HashMap<Rgb, u8> = HashMap::new();
let mut texts: HashMap<(Rgb, Rgb), u8> = HashMap::new();
let mut last: Option<(CellColours, (Color, Color))> = None;
for cell in &mut buf.content {
let (bg, fg) = (rgb(cell.bg), rgb(cell.fg));
if bg.is_none() && fg.is_none() {
continue;
}
let symbol = cell.symbol();
let glyph =
fg.is_some() && bg.is_some() && !symbol.trim().is_empty() && !(half_blocks_fill && is_half_block(symbol));
let key = (cell.fg, cell.bg, glyph);
if let Some((seen, (fg, bg))) = last
&& seen == key
{
(cell.fg, cell.bg) = (fg, bg);
continue;
}
if let Some(fg) = fg {
let index = match bg {
Some(bg) if glyph => *texts.entry((fg, bg)).or_insert_with(|| text(fg, bg)),
_ => *tones.entry(fg).or_insert_with(|| tone(fg)),
};
cell.fg = Color::Indexed(index);
}
if let Some(bg) = bg {
cell.bg = Color::Indexed(*tones.entry(bg).or_insert_with(|| tone(bg)));
}
last = Some((key, (cell.fg, cell.bg)));
}
}
type CellColours = (Color, Color, bool);
fn rgb(color: Color) -> Option<Rgb> {
match color {
Color::Rgb(r, g, b) => Some(Rgb::new(r, g, b)),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WidgetStyle {
props: StyleProps,
phase: f32,
}
impl WidgetStyle {
pub(crate) fn new(props: StyleProps, phase: f32) -> Self {
Self { props, phase }
}
#[must_use]
pub(crate) fn without(mut self, key: &str) -> Self {
self.props.remove(key);
self
}
#[must_use]
pub fn color(&self, key: &str) -> Option<Rgb> {
self.props.paint(key).map(|paint: Paint| paint.at(self.phase))
}
#[must_use]
pub fn flag(&self, key: &str) -> bool {
self.props.flag(key)
}
#[must_use]
pub fn cells(&self, key: &str) -> Option<u16> {
self.props.cells(key)
}
#[must_use]
pub fn word(&self, key: &str) -> Option<&'static str> {
self.props.word(key)
}
#[must_use]
pub fn padding(&self) -> Padding {
match self.props.get("padding") {
Some(PropValue::Pair(v, h)) => Padding::symmetric(v, h),
Some(PropValue::Cells(n)) => Padding::all(n),
_ => Padding::default(),
}
}
#[must_use]
pub fn text(&self) -> CellStyle {
CellStyle {
fg: self.color("fg"),
bg: self.color("bg"),
bold: self.flag("bold"),
italic: self.flag("italic"),
underline: self.flag("underline"),
dim: self.flag("dim"),
}
}
#[must_use]
pub fn is_animated(&self) -> bool {
self.props.is_animated()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::ThemeRegistry;
#[test]
fn applies_colours_and_modifiers() {
let mut cell = Cell::default();
CellStyle::fg(Rgb::new(255, 0, 0)).on(Rgb::new(0, 0, 0)).with_bold(true).apply(&mut cell);
assert_eq!(cell.fg, Color::Rgb(255, 0, 0));
assert!(cell.modifier.contains(Modifier::BOLD));
CellStyle::fg(Rgb::new(0, 0, 255)).apply(&mut cell);
assert_eq!(cell.fg, Color::Rgb(0, 0, 255));
assert_eq!(cell.bg, Color::Rgb(0, 0, 0), "a style without a background keeps the one there");
assert!(!cell.modifier.contains(Modifier::BOLD));
}
#[test]
fn a_frame_is_reduced_to_its_palette_once_painted() {
let ground = Rgb::new(12, 12, 14);
let painted = || {
let mut buf = Buffer::empty(ratatui_core::layout::Rect::new(0, 0, 3, 1));
CellStyle::fg(Rgb::new(255, 0, 0)).on(Rgb::new(0, 0, 0)).apply(&mut buf.content[0]);
buf.content[0].set_symbol("a");
buf.content[1].set_bg(Color::Indexed(4));
buf.content[2].set_bg(Color::Rgb(128, 128, 128));
buf
};
let mut buf = painted();
reduce(&mut buf, ColorDepth::TrueColor, ground);
assert_eq!(buf, painted(), "true colour is sent as painted");
reduce(&mut buf, ColorDepth::Ansi256, ground);
assert_eq!((buf.content[0].fg, buf.content[0].bg), (Color::Indexed(196), Color::Indexed(16)));
assert_eq!(buf.content[1].bg, Color::Indexed(4), "a palette colour is left as it is");
assert_eq!(buf.content[2].bg, Color::Indexed(244));
let mut buf = painted();
reduce(&mut buf, ColorDepth::Ansi16, ground);
assert_eq!((buf.content[0].fg, buf.content[0].bg), (Color::Indexed(9), Color::Indexed(0)));
assert_eq!(buf.content[2].bg, Color::Indexed(8));
}
#[test]
fn a_half_block_in_256_colours_is_two_fills_not_text() {
let (top, bottom) = (Rgb::new(120, 120, 120), Rgb::new(124, 124, 124));
let painted = |symbol: &str| {
let mut buf = Buffer::empty(ratatui_core::layout::Rect::new(0, 0, 1, 1));
CellStyle::fg(top).on(bottom).apply(&mut buf.content[0]);
buf.content[0].set_symbol(symbol);
buf
};
let mut buf = painted("▀");
reduce(&mut buf, ColorDepth::Ansi256, Rgb::new(12, 12, 14));
assert_eq!(buf.content[0].fg, Color::Indexed(top.to_ansi256()), "the upper half is its nearest entry");
assert_eq!(buf.content[0].bg, Color::Indexed(bottom.to_ansi256()));
let mut buf = painted("a");
reduce(&mut buf, ColorDepth::Ansi256, Rgb::new(12, 12, 14));
assert_ne!(buf.content[0].fg, Color::Indexed(top.to_ansi256()), "a letter is still kept readable");
}
#[test]
fn resolves_theme_properties() {
let (theme, _) = ThemeRegistry::builtin().resolve_or_default("monochrome");
let style = WidgetStyle::new(theme.style("button", Some("primary"), &[]), 0.0);
assert_ne!(style.text().bg, theme.color("accent"));
assert_eq!(style.text().fg, theme.color("accent"));
assert!(style.text().bold);
assert_eq!(style.padding(), Padding::symmetric(0, 2));
}
}