Skip to main content

mcraw_tui/
gradient.rs

1use ratatui::style::Color;
2
3/// Multi-stop gradient for warm tones (Exposure slider, progress bars, borders).
4/// #1A0505 (deep soil) → #3A1A15 (dark bark) → #6B3A2A (amber shadow)
5///   → #B57A35 (warm amber) → #E8A035 (golden amber).
6pub const GRADIENT_WARM: &[(Color, f32); 5] = &[
7    (Color::Rgb(0x1A, 0x05, 0x05), 0.0),
8    (Color::Rgb(0x3A, 0x1A, 0x15), 0.25),
9    (Color::Rgb(0x6B, 0x3A, 0x2A), 0.50),
10    (Color::Rgb(0xB5, 0x7A, 0x35), 0.75),
11    (Color::Rgb(0xE8, 0xA0, 0x35), 1.0),
12];
13
14/// Multi-stop gradient for cool tones (White Balance slider, scope backgrounds).
15/// #0A1A25 (deep river) → #1A3A45 (deep water) → #4D8A8A (teal)
16///   → #6DAEAE (mist) → #E8E4D9 (parchment).
17pub const GRADIENT_COOL: &[(Color, f32); 5] = &[
18    (Color::Rgb(0x0A, 0x1A, 0x25), 0.0),
19    (Color::Rgb(0x1A, 0x3A, 0x45), 0.25),
20    (Color::Rgb(0x4D, 0x8A, 0x8A), 0.50),
21    (Color::Rgb(0x6D, 0xAE, 0xAE), 0.75),
22    (Color::Rgb(0xE8, 0xE4, 0xD9), 1.0),
23];
24
25/// Linear interpolation between two RGB colors.
26/// `t=0` returns `a`, `t=1` returns `b`. `t` is clamped to [0, 1].
27/// If either color is not `Color::Rgb`, returns `a` unchanged.
28pub fn lerp_color(a: Color, b: Color, t: f32) -> Color {
29    let t = t.clamp(0.0, 1.0);
30    match (a, b) {
31        (Color::Rgb(ar, ag, ab), Color::Rgb(br, bg, bb)) => Color::Rgb(
32            (ar as f32 + (br as f32 - ar as f32) * t) as u8,
33            (ag as f32 + (bg as f32 - ag as f32) * t) as u8,
34            (ab as f32 + (bb as f32 - ab as f32) * t) as u8,
35        ),
36        _ => a,
37    }
38}
39
40/// Interpolate across a multi-stop gradient at position `t` (0.0 — 1.0).
41///
42/// `stops` must be sorted by position. Returns the nearest stop color if `t`
43/// falls before the first or after the last stop.
44pub fn multi_stop_color(stops: &[(Color, f32)], t: f32) -> Color {
45    let t = t.clamp(0.0, 1.0);
46    if stops.is_empty() {
47        return Color::Rgb(0, 0, 0);
48    }
49    if stops.len() == 1 {
50        return stops[0].0;
51    }
52    for i in 0..stops.len() - 1 {
53        if t >= stops[i].1 && t <= stops[i + 1].1 {
54            let seg_len = stops[i + 1].1 - stops[i].1;
55            let seg_t = if seg_len == 0.0 { 0.0 } else { (t - stops[i].1) / seg_len };
56            return lerp_color(stops[i].0, stops[i + 1].0, seg_t);
57        }
58    }
59    stops.last().unwrap().0
60}
61
62/// Pre-compute a horizontal row of gradient colors.
63///
64/// Returns one `Color` per cell for the given `width`. Useful for caching
65/// per-cell border or progress-bar colors across consecutive renders.
66pub fn gradient_horizontal(width: u16, stops: &[(Color, f32)]) -> Vec<Color> {
67    if width == 0 {
68        return Vec::new();
69    }
70    (0..width)
71        .map(|i| {
72            let t = i as f32 / (width - 1) as f32;
73            multi_stop_color(stops, t)
74        })
75        .collect()
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn lerp_identity() {
84        let a = Color::Rgb(10, 20, 30);
85        let b = Color::Rgb(100, 200, 250);
86        assert_eq!(lerp_color(a, b, 0.0), a);
87        assert_eq!(lerp_color(a, b, 1.0), b);
88    }
89
90    #[test]
91    fn lerp_midpoint() {
92        let a = Color::Rgb(0, 0, 0);
93        let b = Color::Rgb(100, 200, 0);
94        assert_eq!(lerp_color(a, b, 0.5), Color::Rgb(50, 100, 0));
95    }
96
97    #[test]
98    fn lerp_clamps_t() {
99        let a = Color::Rgb(0, 0, 0);
100        let b = Color::Rgb(100, 0, 0);
101        assert_eq!(lerp_color(a, b, -0.5), a);
102        assert_eq!(lerp_color(a, b, 1.5), b);
103    }
104
105    #[test]
106    fn lerp_non_rgb_fallback() {
107        let a = Color::Red;
108        let b = Color::Rgb(100, 0, 0);
109        assert_eq!(lerp_color(a, b, 0.5), a);
110    }
111
112    #[test]
113    fn multi_stop_single() {
114        let stops = &[(Color::Rgb(50, 50, 50), 0.0)];
115        assert_eq!(multi_stop_color(stops, 0.0), Color::Rgb(50, 50, 50));
116        assert_eq!(multi_stop_color(stops, 0.5), Color::Rgb(50, 50, 50));
117    }
118
119    #[test]
120    fn multi_stop_warm_midpoint() {
121        let c = multi_stop_color(GRADIENT_WARM, 0.5);
122        // At t=0.5: lands exactly on stop 2 (#6B3A2A)
123        assert_eq!(c, Color::Rgb(0x6B, 0x3A, 0x2A));
124    }
125
126    #[test]
127    fn gradient_horizontal_empty() {
128        assert!(gradient_horizontal(0, GRADIENT_WARM).is_empty());
129    }
130
131    #[test]
132    fn gradient_horizontal_single() {
133        let colors = gradient_horizontal(1, GRADIENT_WARM);
134        assert_eq!(colors.len(), 1);
135        // When width=1, t=NaN → falls to last stop
136        assert_eq!(colors[0], GRADIENT_WARM.last().unwrap().0);
137    }
138
139    #[test]
140    fn gradient_horizontal_width_matches() {
141        let colors = gradient_horizontal(10, GRADIENT_WARM);
142        assert_eq!(colors.len(), 10);
143        assert_eq!(colors[0], GRADIENT_WARM[0].0);
144        assert_eq!(colors[9], GRADIENT_WARM.last().unwrap().0);
145    }
146}