use crate::macros::for_all_rgb_colors;
#[cfg(feature = "lilygo-epd47")]
use embedded_graphics::pixelcolor::Gray4;
use embedded_graphics::pixelcolor::{
Bgr555, Bgr565, Bgr666, Bgr888, BinaryColor, Rgb555, Rgb565, Rgb666, Rgb888, RgbColor,
};
use ratatui_core::style::Color;
#[derive(Clone, Copy)]
pub struct ColorTheme {
pub foreground: Rgb888,
pub background: Rgb888,
pub white: Rgb888,
pub black: Rgb888,
pub red: Rgb888,
pub green: Rgb888,
pub yellow: Rgb888,
pub blue: Rgb888,
pub magenta: Rgb888,
pub cyan: Rgb888,
pub light_red: Rgb888,
pub light_green: Rgb888,
pub light_yellow: Rgb888,
pub light_blue: Rgb888,
pub light_magenta: Rgb888,
pub light_cyan: Rgb888,
pub gray: Rgb888,
pub dark_gray: Rgb888,
}
impl Default for ColorTheme {
fn default() -> Self {
Self::ansi()
}
}
impl ColorTheme {
pub const fn ansi() -> Self {
Self {
foreground: Rgb888::WHITE,
background: Rgb888::BLACK,
white: Rgb888::WHITE,
black: Rgb888::BLACK,
red: Rgb888::RED,
green: Rgb888::GREEN,
yellow: Rgb888::YELLOW,
blue: Rgb888::BLUE,
magenta: Rgb888::MAGENTA,
cyan: Rgb888::CYAN,
light_red: Rgb888::new(Rgb888::MAX_R, Rgb888::MAX_G / 2, Rgb888::MAX_B / 2),
light_green: Rgb888::new(Rgb888::MAX_R / 2, Rgb888::MAX_G, Rgb888::MAX_B / 2),
light_yellow: Rgb888::new(Rgb888::MAX_R, Rgb888::MAX_G, Rgb888::MAX_B / 2),
light_blue: Rgb888::new(Rgb888::MAX_R / 2, Rgb888::MAX_G / 2, Rgb888::MAX_B),
light_magenta: Rgb888::new(Rgb888::MAX_R, Rgb888::MAX_G / 2, Rgb888::MAX_B),
light_cyan: Rgb888::new(Rgb888::MAX_R / 2, Rgb888::MAX_G, Rgb888::MAX_B),
gray: Rgb888::new(Rgb888::MAX_R / 2, Rgb888::MAX_G / 2, Rgb888::MAX_B / 2),
dark_gray: Rgb888::new(170, 170, 170),
}
}
pub const fn tokyo_night() -> Self {
Self {
foreground: Rgb888::new(0xa9, 0xb1, 0xd6), background: Rgb888::new(0x1a, 0x1b, 0x26), white: Rgb888::new(0xc0, 0xca, 0xf5), black: Rgb888::new(0x41, 0x48, 0x68), red: Rgb888::new(0xf7, 0x76, 0x8e), green: Rgb888::new(0x73, 0xda, 0xca), yellow: Rgb888::new(0xe0, 0xaf, 0x68), blue: Rgb888::new(0x7a, 0xa2, 0xf7), magenta: Rgb888::new(0xbb, 0x9a, 0xf7), cyan: Rgb888::new(0x7d, 0xcf, 0xff), light_red: Rgb888::new(0xf7, 0x76, 0x8e), light_green: Rgb888::new(0x73, 0xda, 0xca), light_yellow: Rgb888::new(0xe0, 0xaf, 0x68), light_blue: Rgb888::new(0x7a, 0xa2, 0xf7), light_magenta: Rgb888::new(0xbb, 0x9a, 0xf7), light_cyan: Rgb888::new(0x7d, 0xcf, 0xff), gray: Rgb888::new(0xc0, 0xca, 0xf5), dark_gray: Rgb888::new(0x41, 0x48, 0x68), }
}
pub(crate) fn resolve(&self, color: Color, color_type: TermColorType) -> Rgb888 {
match color {
Color::Reset => match color_type {
TermColorType::Foreground => self.foreground,
TermColorType::Background => self.background,
},
Color::White => self.white,
Color::Black => self.black,
Color::Red => self.red,
Color::Green => self.green,
Color::Yellow => self.yellow,
Color::Blue => self.blue,
Color::Magenta => self.magenta,
Color::Cyan => self.cyan,
Color::LightRed => self.light_red,
Color::LightGreen => self.light_green,
Color::LightYellow => self.light_yellow,
Color::LightBlue => self.light_blue,
Color::LightMagenta => self.light_magenta,
Color::LightCyan => self.light_cyan,
Color::Gray => self.gray,
Color::DarkGray => self.dark_gray,
Color::Rgb(r, g, b) => Rgb888::new(r, g, b),
Color::Indexed(_) => Rgb888::BLACK,
}
}
}
#[derive(Clone, Copy)]
pub enum TermColorType {
Foreground,
Background,
}
#[derive(Clone, Copy)]
pub struct TermColor<'a>(pub Color, pub TermColorType, pub &'a ColorTheme);
impl<'a> TermColor<'a> {
pub fn new(color: Color, color_type: TermColorType, theme: &'a ColorTheme) -> Self {
Self(color, color_type, theme)
}
fn to_rgb888(self) -> Rgb888 {
self.2.resolve(self.0, self.1)
}
}
macro_rules! impl_from_term_color {
(
$color_type:ident
) => {
impl<'a> From<TermColor<'a>> for $color_type {
fn from(color: TermColor<'a>) -> Self {
color.to_rgb888().into()
}
}
};
}
for_all_rgb_colors!(impl_from_term_color);
impl<'a> From<TermColor<'a>> for BinaryColor {
fn from(color: TermColor<'a>) -> Self {
match color.to_rgb888() {
rgb if rgb == Rgb888::BLACK => BinaryColor::Off,
rgb if rgb == Rgb888::WHITE => BinaryColor::On,
_ => match color.1 {
TermColorType::Foreground => BinaryColor::On,
TermColorType::Background => BinaryColor::Off,
},
}
}
}
fn dim_u8(v: u8) -> u8 {
v >> 1
}
pub fn dim_color<C>(color: C) -> C
where
C: Into<Rgb888> + From<Rgb888>,
{
let rgb: Rgb888 = color.into();
Rgb888::new(dim_u8(rgb.r()), dim_u8(rgb.g()), dim_u8(rgb.b())).into()
}
#[cfg(feature = "epd-weact")]
impl<'a> From<TermColor<'a>> for weact_studio_epd::Color {
fn from(color: TermColor<'a>) -> Self {
BinaryColor::from(color).into()
}
}
#[cfg(feature = "epd-weact")]
impl<'a> From<TermColor<'a>> for weact_studio_epd::TriColor {
fn from(color: TermColor<'a>) -> Self {
let rgb = color.to_rgb888();
match rgb {
rgb if rgb == Rgb888::WHITE => weact_studio_epd::TriColor::White,
rgb if rgb == Rgb888::BLACK => weact_studio_epd::TriColor::Black,
rgb if rgb == Rgb888::RED => weact_studio_epd::TriColor::Red,
_ => match color.1 {
TermColorType::Foreground => weact_studio_epd::TriColor::Black,
TermColorType::Background => weact_studio_epd::TriColor::White,
},
}
}
}
#[cfg(feature = "epd-waveshare")]
impl From<TermColor<'_>> for epd_waveshare::color::Color {
fn from(color: TermColor) -> Self {
match BinaryColor::from(color) {
BinaryColor::Off => epd_waveshare::color::Color::Black,
BinaryColor::On => epd_waveshare::color::Color::White,
}
}
}
#[cfg(feature = "epd-waveshare")]
impl From<TermColor<'_>> for epd_waveshare::color::TriColor {
fn from(color: TermColor) -> Self {
match color.0 {
Color::White => epd_waveshare::color::TriColor::White,
Color::Black => epd_waveshare::color::TriColor::Black,
Color::Red => epd_waveshare::color::TriColor::Chromatic,
_ => match color.1 {
TermColorType::Foreground => epd_waveshare::color::TriColor::Black,
TermColorType::Background => epd_waveshare::color::TriColor::White,
},
}
}
}
#[cfg(feature = "lilygo-epd47")]
impl<'a> From<TermColor<'a>> for Gray4 {
fn from(color: TermColor<'a>) -> Self {
let rgb: Rgb888 = color.into();
rgb.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use Color::*;
use TermColorType::*;
use paste::paste;
use rstest::rstest;
const TEST_THEME: ColorTheme = ColorTheme::ansi();
fn themed(color_type: TermColorType, color_from: Color) -> TermColor<'static> {
TermColor::new(color_from, color_type, &TEST_THEME)
}
macro_rules! into_eg_color {
($color_type:ident) => {
paste! {
#[rstest]
#[case(Foreground, Reset, $color_type::WHITE)]
#[case(Background, Reset, $color_type::BLACK)]
#[case(Foreground, White, $color_type::WHITE)]
#[case(Background, White, $color_type::WHITE)]
#[case(Foreground, Black, $color_type::BLACK)]
#[case(Background, Black, $color_type::BLACK)]
#[case(Foreground, Red, $color_type::RED)]
#[case(Background, Red, $color_type::RED)]
#[case(Foreground, Yellow, $color_type::YELLOW)]
#[case(Background, Yellow, $color_type::YELLOW)]
#[case(Foreground, Magenta, $color_type::MAGENTA)]
#[case(Background, Magenta, $color_type::MAGENTA)]
#[case(Foreground, Cyan, $color_type::CYAN)]
#[case(Background, Cyan, $color_type::CYAN)]
#[case(Foreground, LightRed, Rgb888::new(255, 127, 127).into())]
#[case(Background, LightRed, Rgb888::new(255, 127, 127).into())]
#[case(Foreground, LightGreen, Rgb888::new(127, 255, 127).into())]
#[case(Background, LightGreen, Rgb888::new(127, 255, 127).into())]
#[case(Foreground, LightYellow, Rgb888::new(255, 255, 127).into())]
#[case(Background, LightYellow, Rgb888::new(255, 255, 127).into())]
#[case(Foreground, LightBlue, Rgb888::new(127, 127, 255).into())]
#[case(Background, LightBlue, Rgb888::new(127, 127, 255).into())]
#[case(Foreground, LightMagenta, Rgb888::new(255, 127, 255).into())]
#[case(Background, LightMagenta, Rgb888::new(255, 127, 255).into())]
#[case(Foreground, LightCyan, Rgb888::new(127, 255, 255).into())]
#[case(Background, LightCyan, Rgb888::new(127, 255, 255).into())]
#[case(Foreground, Gray, Rgb888::new(127, 127, 127).into())]
#[case(Background, Gray, Rgb888::new(127, 127, 127).into())]
#[case(Foreground, DarkGray, Rgb888::new(170, 170, 170).into())]
#[case(Background, DarkGray, Rgb888::new(170, 170, 170).into())]
#[case(Foreground, Rgb(50, 100, 200), Rgb888::new(50, 100, 200).into())]
#[case(Background, Rgb(50, 100, 200), Rgb888::new(50, 100, 200).into())]
#[case(Foreground, Rgb(123, 23, 3), Rgb888::new(123, 23, 3).into())]
#[case(Background, Rgb(123, 23, 3), Rgb888::new(123, 23, 3).into())]
fn [<into_ $color_type:lower>] (
#[case] color_type: TermColorType,
#[case] color_from: Color,
#[case] color_into: $color_type
) {
let output: $color_type = themed(color_type, color_from).into();
assert_eq!(output, color_into);
}
}
};
}
for_all_rgb_colors!(into_eg_color);
#[rstest]
#[case(Foreground, Black, BinaryColor::Off)]
#[case(Background, Black, BinaryColor::Off)]
#[case(Foreground, White, BinaryColor::On)]
#[case(Background, White, BinaryColor::On)]
#[case(Background, Reset, BinaryColor::Off)]
#[case(Foreground, Reset, BinaryColor::On)]
fn into_binary_color(
#[case] color_type: TermColorType,
#[case] color_from: Color,
#[case] color_into: BinaryColor,
) {
let output: BinaryColor = themed(color_type, color_from).into();
assert_eq!(output, color_into);
}
#[cfg(feature = "epd-weact")]
#[rstest]
#[case(Foreground, Black, weact_studio_epd::Color::Black)]
#[case(Background, Black, weact_studio_epd::Color::Black)]
#[case(Foreground, White, weact_studio_epd::Color::White)]
#[case(Background, White, weact_studio_epd::Color::White)]
fn into_weact_color(
#[case] color_type: TermColorType,
#[case] color_from: Color,
#[case] color_into: weact_studio_epd::Color,
) {
let output: weact_studio_epd::Color = themed(color_type, color_from).into();
assert_eq!(output, color_into);
}
#[cfg(feature = "epd-weact")]
#[rstest]
#[case(Foreground, Black, weact_studio_epd::TriColor::Black)]
#[case(Background, Black, weact_studio_epd::TriColor::Black)]
#[case(Foreground, White, weact_studio_epd::TriColor::White)]
#[case(Background, White, weact_studio_epd::TriColor::White)]
#[case(Foreground, Red, weact_studio_epd::TriColor::Red)]
#[case(Background, Red, weact_studio_epd::TriColor::Red)]
fn into_weact_tricolor(
#[case] color_type: TermColorType,
#[case] color_from: Color,
#[case] color_into: weact_studio_epd::TriColor,
) {
let output: weact_studio_epd::TriColor = themed(color_type, color_from).into();
assert_eq!(output, color_into);
}
#[cfg(feature = "epd-waveshare")]
#[rstest]
#[case(Foreground, Black, epd_waveshare::color::Color::Black)]
#[case(Background, Black, epd_waveshare::color::Color::Black)]
#[case(Foreground, White, epd_waveshare::color::Color::White)]
#[case(Background, White, epd_waveshare::color::Color::White)]
fn into_waveshare_color(
#[case] color_type: TermColorType,
#[case] color_from: Color,
#[case] color_into: epd_waveshare::color::Color,
) {
let output: epd_waveshare::color::Color = themed(color_type, color_from).into();
assert_eq!(output, color_into);
}
#[cfg(feature = "epd-waveshare")]
#[rstest]
#[case(Foreground, Black, epd_waveshare::color::TriColor::Black)]
#[case(Background, Black, epd_waveshare::color::TriColor::Black)]
#[case(Foreground, White, epd_waveshare::color::TriColor::White)]
#[case(Background, White, epd_waveshare::color::TriColor::White)]
#[case(Foreground, Red, epd_waveshare::color::TriColor::Chromatic)]
#[case(Background, Red, epd_waveshare::color::TriColor::Chromatic)]
fn into_wavesharet_tricolor(
#[case] color_type: TermColorType,
#[case] color_from: Color,
#[case] color_into: epd_waveshare::color::TriColor,
) {
let output: epd_waveshare::color::TriColor = themed(color_type, color_from).into();
assert_eq!(output, color_into);
}
}