use core::ops::{BitOr, BitOrAssign};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ThemeSlot(pub u8);
impl ThemeSlot {
pub const FG: Self = Self(0);
pub const BG: Self = Self(1);
pub const RECEDE_FG: Self = Self(2);
pub const RECEDE_BG: Self = Self(3);
pub const ACCENT: Self = Self(4);
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Colour {
#[default]
Default,
Rgb(u8, u8, u8),
Indexed(u8),
Theme(ThemeSlot),
}
impl Colour {
pub const FG: Self = Self::Theme(ThemeSlot::FG);
pub const BG: Self = Self::Theme(ThemeSlot::BG);
pub const RECEDE_FG: Self = Self::Theme(ThemeSlot::RECEDE_FG);
pub const RECEDE_BG: Self = Self::Theme(ThemeSlot::RECEDE_BG);
pub const ACCENT: Self = Self::Theme(ThemeSlot::ACCENT);
pub const fn hex(rgb: u32) -> Self {
Self::Rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
}
pub const fn encode(self) -> u32 {
match self {
Self::Default => 0,
Self::Rgb(r, g, b) => 0x0100_0000 | ((r as u32) << 16) | ((g as u32) << 8) | b as u32,
Self::Indexed(index) => 0x0200_0000 | index as u32,
Self::Theme(slot) => 0x0300_0000 | slot.0 as u32,
}
}
pub const fn decode(word: u32) -> Self {
match word >> 24 {
1 => Self::Rgb((word >> 16) as u8, (word >> 8) as u8, word as u8),
2 => Self::Indexed(word as u8),
3 => Self::Theme(ThemeSlot(word as u8)),
_ => Self::Default,
}
}
}
pub const fn recede(rgb: u32, toward: u32, percent: u16) -> u32 {
let percent = if percent > 100 { 100 } else { percent } as u32;
blend_channel(rgb, toward, percent, 16)
| blend_channel(rgb, toward, percent, 8)
| blend_channel(rgb, toward, percent, 0)
}
const fn blend_channel(rgb: u32, toward: u32, percent: u32, shift: u32) -> u32 {
let from = (rgb >> shift) & 0xff;
let to = (toward >> shift) & 0xff;
((from * (100 - percent) + to * percent) / 100) << shift
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Attrs(pub u16);
impl Attrs {
pub const NONE: Self = Self(0);
pub const BOLD: Self = Self(1);
pub const DIM: Self = Self(1 << 1);
pub const ITALIC: Self = Self(1 << 2);
pub const UNDERLINE: Self = Self(1 << 3);
pub const REVERSE: Self = Self(1 << 4);
pub const STRIKETHROUGH: Self = Self(1 << 5);
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
}
impl BitOr for Attrs {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for Attrs {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Style {
pub fg: Colour,
pub bg: Colour,
pub attrs: Attrs,
}
impl Style {
pub const DEFAULT: Self = Self {
fg: Colour::Default,
bg: Colour::Default,
attrs: Attrs::NONE,
};
pub const fn new() -> Self {
Self::DEFAULT
}
pub const fn fg(colour: Colour) -> Self {
Self {
fg: colour,
..Self::DEFAULT
}
}
pub const fn with_fg(mut self, colour: Colour) -> Self {
self.fg = colour;
self
}
pub const fn with_bg(mut self, colour: Colour) -> Self {
self.bg = colour;
self
}
pub const fn with_attrs(mut self, attrs: Attrs) -> Self {
self.attrs = Attrs(self.attrs.0 | attrs.0);
self
}
pub const fn bold(self) -> Self {
self.with_attrs(Attrs::BOLD)
}
pub const fn dim(self) -> Self {
self.with_attrs(Attrs::DIM)
}
pub const fn italic(self) -> Self {
self.with_attrs(Attrs::ITALIC)
}
pub const fn underline(self) -> Self {
self.with_attrs(Attrs::UNDERLINE)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct Rgba {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Rgba {
pub const TRANSPARENT: Self = Self::new(0, 0, 0, 0);
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::new(r, g, b, 255)
}
pub const fn hex(rgb: u32) -> Self {
Self::rgb((rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8)
}
pub const fn with_alpha(mut self, a: u8) -> Self {
self.a = a;
self
}
pub const fn to_word(self) -> u32 {
u32::from_le_bytes([self.r, self.g, self.b, self.a])
}
pub const fn from_word(word: u32) -> Self {
let [r, g, b, a] = word.to_le_bytes();
Self { r, g, b, a }
}
pub fn over(self, below: Self, coverage: u8) -> Self {
let alpha = u32::from(self.a) * u32::from(coverage) / 255;
if alpha == 255 {
return self;
}
if alpha == 0 {
return below;
}
let below_alpha = u32::from(below.a) * (255 - alpha) / 255;
let out_alpha = alpha + below_alpha;
let channel = |top: u8, bottom: u8| {
((u32::from(top) * alpha + u32::from(bottom) * below_alpha + out_alpha / 2) / out_alpha)
as u8
};
Self {
r: channel(self.r, below.r),
g: channel(self.g, below.g),
b: channel(self.b, below.b),
a: out_alpha as u8,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn receding_blends_toward_the_background_and_keeps_the_hue() {
assert_eq!(recede(0xff8000, 0x000000, 55), 0x723900);
assert_eq!(recede(0x123456, 0xffffff, 0), 0x123456);
assert_eq!(recede(0x123456, 0xffffff, 100), 0xffffff);
assert_eq!(recede(0x123456, 0xffffff, 250), 0xffffff, "clamped");
}
#[test]
fn colours_round_trip_through_their_buffer_words() {
let mut colours = alloc::vec![Colour::Default];
for value in [0u8, 1, 127, 128, 254, 255] {
colours.push(Colour::Indexed(value));
colours.push(Colour::Theme(ThemeSlot(value)));
colours.push(Colour::Rgb(value, 255 - value, value / 2));
}
for colour in colours {
assert_eq!(Colour::decode(colour.encode()), colour, "{colour:?}");
}
assert_eq!(Colour::Rgb(0x12, 0x34, 0x56).encode(), 0x0112_3456);
assert_eq!(Colour::Indexed(9).encode(), 0x0200_0009);
assert_eq!(Colour::ACCENT.encode(), 0x0300_0004);
assert_eq!(Colour::hex(0xabcdef), Colour::Rgb(0xab, 0xcd, 0xef));
assert_eq!(Colour::decode(0x02ff_ff07), Colour::Indexed(7));
assert_eq!(Colour::decode(0x0412_3456), Colour::Default);
assert_eq!(Colour::decode(0x00ab_cdef), Colour::Default);
for word in [0u32, 0x0100_0000, 0x01ff_ffff, 0x0200_00ff, 0x0300_0004] {
assert_eq!(Colour::decode(word).encode(), word);
}
let pixel = Rgba::new(1, 2, 3, 4);
assert_eq!(Rgba::from_word(pixel.to_word()), pixel);
assert_eq!(pixel.to_word().to_le_bytes(), [1, 2, 3, 4]);
}
}