use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::stage::{Canvas, Caption};
pub(crate) const BACKDROP: usize = 80;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rgb(pub(crate) f64, pub(crate) f64, pub(crate) f64);
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self(r as f64, g as f64, b as f64)
}
pub(crate) fn blend(self, under: Rgb, alpha: f64) -> Rgb {
let mix = |a: f64, b: f64| a * alpha + b * (1.0 - alpha);
Rgb(
mix(self.0, under.0),
mix(self.1, under.1),
mix(self.2, under.2),
)
}
pub(crate) fn luma(self) -> f64 {
(0.2126 * self.0 + 0.7152 * self.1 + 0.0722 * self.2) / 255.0
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "Terminal quantization truncates channels clamped to 0..=255."
)]
pub fn bytes(self) -> [u8; 3] {
let channel = |v: f64| v.clamp(0.0, 255.0) as u8;
[channel(self.0), channel(self.1), channel(self.2)]
}
}
impl From<(u8, u8, u8)> for Rgb {
fn from((r, g, b): (u8, u8, u8)) -> Self {
Self::new(r, g, b)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Palette {
pub fg: Rgb,
pub bg: Rgb,
}
impl Palette {
pub(crate) fn legible(self, accent: Rgb) -> Rgb {
let luma = accent.luma();
if self.bg.luma() < 0.5 || luma <= 0.4 {
accent
} else {
let dim = 0.4 / luma;
Rgb(accent.0 * dim, accent.1 * dim, accent.2 * dim)
}
}
pub(crate) fn ink_on(self, cell: Rgb) -> Rgb {
let (darker, lighter) = if self.fg.luma() < self.bg.luma() {
(self.fg, self.bg)
} else {
(self.bg, self.fg)
};
if cell.luma() > 0.5 { darker } else { lighter }
}
pub(crate) fn grey(self, alpha: f64) -> Rgb {
self.fg.blend(self.bg, alpha)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Motion {
pub mark: bool,
pub letters: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct Ink {
pub glyph: Option<char>,
pub fg: Option<Rgb>,
pub bg: Option<Rgb>,
pub bold: bool,
pub underline: Option<Rgb>,
}
impl Ink {
pub(crate) const fn of(glyph: char) -> Ink {
Ink {
glyph: Some(glyph),
fg: None,
bg: None,
bold: false,
underline: None,
}
}
pub(crate) const fn fg(self, fg: Rgb) -> Ink {
Ink {
fg: Some(fg),
..self
}
}
pub(crate) const fn bg(self, bg: Rgb) -> Ink {
Ink {
bg: Some(bg),
..self
}
}
pub(crate) const fn bold(self, bold: bool) -> Ink {
Ink { bold, ..self }
}
pub(crate) const fn underline(self, color: Rgb) -> Ink {
Ink {
underline: Some(color),
..self
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Row {
text: String,
cells: Vec<(usize, Ink)>,
backdrop: Vec<Rgb>,
}
impl Row {
pub(crate) fn paint(caption: &Caption, canvas: Canvas, motion: Motion) -> Self {
let mut row = Self::default();
if motion.mark && !canvas.mark.is_empty() {
for ink in canvas.mark {
row.put(ink, "•");
}
row.put(Ink::default(), " ");
}
if !motion.letters {
for letter in caption.iter() {
row.put(Ink::default(), letter);
}
return row;
}
let mut letters = canvas.letters.into_iter();
for letter in caption.iter() {
row.put(letters.next().unwrap_or_default(), letter);
}
for ink in canvas.tail {
row.put(ink, " ");
}
row.backdrop = canvas.backdrop;
row.backdrop.truncate(BACKDROP);
row
}
fn put(&mut self, ink: Ink, beneath: &str) {
match ink
.glyph
.filter(|glyph| glyph.width() == Some(beneath.width()))
{
Some(glyph) => self.text.push(glyph),
None => self.text.push_str(beneath),
}
self.cells
.push((self.text.len(), Ink { glyph: None, ..ink }));
}
pub fn cells(&self) -> impl Iterator<Item = (&str, Ink)> {
self.cells.iter().scan(0, |start, &(end, ink)| {
let symbol = &self.text[*start..end];
*start = end;
Some((symbol, ink))
})
}
pub fn backdrop(&self) -> &[Rgb] {
&self.backdrop
}
}