use ratatui::style::Color;
pub const FOCUS_DARKEN: u16 = 10;
pub const FOCUS_LIGHTEN: u16 = 10;
pub const HOVER_DARKEN: u16 = 20;
pub const HOVER_LIGHTEN: u16 = 20;
pub const FIELD_FOCUS_LIGHTEN: u16 = 4;
pub const FIELD_HOVER_LIGHTEN: u16 = 8;
pub const DISABLED_DIM: u16 = 50;
#[must_use]
pub const fn resolve_rgb(color: Color) -> Option<(u8, u8, u8)> {
match color {
Color::Rgb(r, g, b) => Some((r, g, b)),
Color::Black => Some((0, 0, 0)),
Color::Red => Some((128, 0, 0)),
Color::Green => Some((0, 128, 0)),
Color::Yellow => Some((128, 128, 0)),
Color::Blue => Some((0, 0, 128)),
Color::Magenta => Some((128, 0, 128)),
Color::Cyan => Some((0, 128, 128)),
Color::Gray => Some((192, 192, 192)),
Color::DarkGray => Some((128, 128, 128)),
Color::LightRed => Some((255, 0, 0)),
Color::LightGreen => Some((0, 255, 0)),
Color::LightYellow => Some((255, 255, 0)),
Color::LightBlue => Some((0, 0, 255)),
Color::LightMagenta => Some((255, 0, 255)),
Color::LightCyan => Some((0, 255, 255)),
Color::White => Some((255, 255, 255)),
Color::Indexed(_) | Color::Reset => None,
}
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
reason = "`c * keep / 100` with c <= 255 and keep <= 100 is <= 255, so the `as u8` cast cannot truncate"
)]
pub const fn darken(color: Color, amount: u16) -> Color {
match resolve_rgb(color) {
Some((r, g, b)) => {
let amount = if amount > 100 { 100 } else { amount };
let keep = 100 - amount;
Color::Rgb(
((r as u16 * keep + 50) / 100) as u8,
((g as u16 * keep + 50) / 100) as u8,
((b as u16 * keep + 50) / 100) as u8,
)
}
None => color,
}
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
reason = "`c*(100-amount)/100 + 255*amount/100` is a weighted average of c and 255, both <= 255, so it stays <= 255 and the `as u8` cast cannot truncate"
)]
pub const fn lighten(color: Color, amount: u16) -> Color {
match resolve_rgb(color) {
Some((r, g, b)) => {
let amount = if amount > 100 { 100 } else { amount };
let keep = 100 - amount;
Color::Rgb(
((r as u16 * keep + 255 * amount + 50) / 100) as u8,
((g as u16 * keep + 255 * amount + 50) / 100) as u8,
((b as u16 * keep + 255 * amount + 50) / 100) as u8,
)
}
None => color,
}
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
reason = "each channel is a weighted average of two u8s, so it stays within 0..=255 and the `as u8` cast cannot truncate"
)]
pub const fn dim(color: Color, toward: Color, amount: u16) -> Color {
match (resolve_rgb(color), resolve_rgb(toward)) {
(Some((r, g, b)), Some((tr, tg, tb))) => {
let amount = if amount > 100 { 100 } else { amount };
let keep = 100 - amount;
Color::Rgb(
((r as u16 * keep + tr as u16 * amount + 50) / 100) as u8,
((g as u16 * keep + tg as u16 * amount + 50) / 100) as u8,
((b as u16 * keep + tb as u16 * amount + 50) / 100) as u8,
)
}
_ => color,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percentages_above_one_hundred_clamp_to_the_endpoint() {
let color = Color::Rgb(20, 40, 60);
let toward = Color::Rgb(100, 120, 140);
assert_eq!(darken(color, 101), Color::Rgb(0, 0, 0));
assert_eq!(lighten(color, u16::MAX), Color::Rgb(255, 255, 255));
assert_eq!(dim(color, toward, 101), toward);
}
#[test]
fn named_colors_derive_visible_state_changes() {
assert_eq!(
darken(Color::LightBlue, 10),
darken(Color::Rgb(0, 0, 255), 10)
);
assert_ne!(darken(Color::LightBlue, 10), Color::LightBlue);
assert_eq!(
dim(Color::Red, Color::Black, 50),
dim(Color::Rgb(128, 0, 0), Color::Rgb(0, 0, 0), 50)
);
}
#[test]
fn unresolvable_colors_pass_through() {
assert_eq!(darken(Color::Reset, 10), Color::Reset);
assert_eq!(lighten(Color::Indexed(42), 10), Color::Indexed(42));
assert_eq!(
dim(Color::Rgb(1, 2, 3), Color::Reset, 50),
Color::Rgb(1, 2, 3)
);
}
}