use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, BorderType, Borders};
use crate::sampler::PressureLevel;
pub const ACCENT: Color = Color::Rgb(45, 225, 194); pub const AMBER: Color = Color::Rgb(255, 180, 84);
pub const RED: Color = Color::Rgb(255, 92, 87);
pub const GREEN: Color = Color::Rgb(74, 214, 109);
pub const TEXT: Color = Color::Rgb(205, 214, 217);
pub const DIM: Color = Color::Rgb(90, 105, 110); pub const BG_CELL: Color = Color::Rgb(18, 32, 36); pub const BASE: Color = Color::Rgb(10, 14, 18);
const GRAD_FROM: (u8, u8, u8) = (14, 58, 58); const GRAD_TO: (u8, u8, u8) = (45, 225, 194);
pub fn gradient(t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
let lerp = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * t) as u8;
Color::Rgb(
lerp(GRAD_FROM.0, GRAD_TO.0),
lerp(GRAD_FROM.1, GRAD_TO.1),
lerp(GRAD_FROM.2, GRAD_TO.2),
)
}
pub fn blend(from: Color, to: Color, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
let ch = |c: Color| match c {
Color::Rgb(r, g, b) => (r, g, b),
other => panic!("blend expects Color::Rgb, got {other:?}"),
};
let (fr, fg, fb) = ch(from);
let (tr, tg, tb) = ch(to);
let lerp = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * t) as u8;
Color::Rgb(lerp(fr, tr), lerp(fg, tg), lerp(fb, tb))
}
const MIN_RAMP: f32 = 0.5;
pub fn ramp(color: Color, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
let floored = if t > 0.0 { t.max(MIN_RAMP) } else { 0.0 };
blend(BASE, color, floored)
}
pub fn temp_color(c: f32) -> Color {
if c >= 95.0 { RED } else if c >= 85.0 { AMBER } else { ACCENT }
}
pub fn pressure_color(p: PressureLevel) -> Color {
match p {
PressureLevel::Normal => GREEN,
PressureLevel::Warn => AMBER,
PressureLevel::Critical => RED,
}
}
pub fn panel_block(title: &str, focused: bool) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(if focused { ACCENT } else { DIM }))
.title(format!(" {title} "))
.title_style(Style::default().fg(TEXT))
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::Color;
#[test]
fn gradient_is_monotonic_and_clamped() {
let ch = |c: Color| match c { Color::Rgb(r, g, b) => (r, g, b), _ => panic!() };
let (r0, g0, b0) = ch(gradient(0.0));
let (r1, g1, b1) = ch(gradient(1.0));
assert_eq!((r1, g1, b1), (45, 225, 194));
assert!(g0 < g1 && b0 < b1 && r0 <= r1);
assert_eq!(gradient(-1.0), gradient(0.0));
assert_eq!(gradient(2.0), gradient(1.0));
}
#[test]
fn temp_thresholds() {
assert_eq!(temp_color(60.0), ACCENT);
assert_eq!(temp_color(85.0), AMBER);
assert_eq!(temp_color(95.0), RED);
}
#[test]
fn blend_hits_both_endpoints_and_the_midpoint() {
let a = Color::Rgb(0, 0, 0);
let b = Color::Rgb(100, 200, 50);
assert_eq!(blend(a, b, 0.0), a);
assert_eq!(blend(a, b, 1.0), b);
assert_eq!(blend(a, b, 0.5), Color::Rgb(50, 100, 25));
}
#[test]
fn blend_clamps_out_of_range_t() {
let a = Color::Rgb(10, 20, 30);
let b = Color::Rgb(200, 210, 220);
assert_eq!(blend(a, b, -1.0), a);
assert_eq!(blend(a, b, 5.0), b);
}
#[test]
fn ramp_hits_base_at_zero_and_the_full_colour_at_one() {
assert_eq!(ramp(ACCENT, 0.0), BASE);
assert_eq!(ramp(ACCENT, 1.0), ACCENT);
}
#[test]
fn ramp_is_dimmer_at_low_factors_than_high_ones() {
let ch = |c: Color| match c { Color::Rgb(r, g, b) => (r, g, b), _ => panic!() };
let (r_dim, g_dim, b_dim) = ch(ramp(ACCENT, 0.2));
let (r_bright, g_bright, b_bright) = ch(ramp(ACCENT, 1.0));
assert!(r_dim <= r_bright && g_dim < g_bright && b_dim < b_bright);
}
#[test]
fn ramp_floors_the_lowest_nonzero_bar_away_from_base() {
let ch = |c: Color| match c { Color::Rgb(r, g, b) => (i32::from(r), i32::from(g), i32::from(b)), _ => panic!() };
let dimmest = ramp(ACCENT, 0.01);
assert_ne!(dimmest, BASE);
let (r0, g0, b0) = ch(BASE);
let (r1, g1, b1) = ch(dimmest);
let dist = (r1 - r0).abs() + (g1 - g0).abs() + (b1 - b0).abs();
assert!(dist > 20, "lowest non-zero bar colour {dimmest:?} too close to BASE {BASE:?}");
assert_eq!(ramp(ACCENT, 0.0), BASE);
}
#[test]
fn ramp_stays_within_a_colours_own_hue_not_teal() {
let dim_red = ramp(RED, 0.5);
let dim_accent = ramp(ACCENT, 0.5);
assert_ne!(dim_red, dim_accent);
}
}