git_iris/theme/
gradient.rs

1//! Gradient color interpolation.
2
3use super::color::ThemeColor;
4
5/// A color gradient defined by a series of color stops.
6#[derive(Debug, Clone, PartialEq)]
7pub struct Gradient {
8    /// The colors that make up this gradient.
9    stops: Vec<ThemeColor>,
10}
11
12impl Gradient {
13    /// Create a new gradient from a list of color stops.
14    ///
15    /// # Panics
16    /// Panics if `stops` is empty.
17    #[must_use]
18    pub fn new(stops: Vec<ThemeColor>) -> Self {
19        assert!(!stops.is_empty(), "gradient must have at least one color");
20        Self { stops }
21    }
22
23    /// Get the interpolated color at position `t` (0.0 to 1.0).
24    #[must_use]
25    #[allow(
26        clippy::cast_precision_loss,
27        clippy::cast_possible_truncation,
28        clippy::cast_sign_loss,
29        clippy::as_conversions
30    )]
31    pub fn at(&self, t: f32) -> ThemeColor {
32        let t = t.clamp(0.0, 1.0);
33
34        if self.stops.len() == 1 {
35            return self.stops[0];
36        }
37
38        let scaled = t * (self.stops.len() - 1) as f32;
39        let idx = scaled.floor() as usize;
40        let local_t = scaled - scaled.floor();
41
42        if idx >= self.stops.len() - 1 {
43            self.stops[self.stops.len() - 1]
44        } else {
45            self.stops[idx].lerp(&self.stops[idx + 1], local_t)
46        }
47    }
48
49    /// Get the number of color stops in this gradient.
50    #[must_use]
51    pub fn len(&self) -> usize {
52        self.stops.len()
53    }
54
55    /// Check if the gradient has only one color.
56    #[must_use]
57    pub fn is_empty(&self) -> bool {
58        self.stops.is_empty()
59    }
60
61    /// Generate a smooth gradient with `n` evenly spaced colors.
62    #[must_use]
63    #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
64    pub fn generate(&self, n: usize) -> Vec<ThemeColor> {
65        if n == 0 {
66            return vec![];
67        }
68        if n == 1 {
69            return vec![self.stops[0]];
70        }
71
72        (0..n)
73            .map(|i| {
74                let t = i as f32 / (n - 1) as f32;
75                self.at(t)
76            })
77            .collect()
78    }
79}
80
81impl Default for Gradient {
82    fn default() -> Self {
83        Self {
84            stops: vec![ThemeColor::FALLBACK],
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_gradient_endpoints() {
95        let gradient = Gradient::new(vec![
96            ThemeColor::new(0, 0, 0),
97            ThemeColor::new(255, 255, 255),
98        ]);
99
100        assert_eq!(gradient.at(0.0), ThemeColor::new(0, 0, 0));
101        assert_eq!(gradient.at(1.0), ThemeColor::new(255, 255, 255));
102    }
103
104    #[test]
105    fn test_gradient_midpoint() {
106        let gradient = Gradient::new(vec![
107            ThemeColor::new(0, 0, 0),
108            ThemeColor::new(255, 255, 255),
109        ]);
110
111        let mid = gradient.at(0.5);
112        assert_eq!(mid, ThemeColor::new(127, 127, 127));
113    }
114
115    #[test]
116    fn test_gradient_multi_stop() {
117        let gradient = Gradient::new(vec![
118            ThemeColor::new(255, 0, 0), // red
119            ThemeColor::new(0, 255, 0), // green
120            ThemeColor::new(0, 0, 255), // blue
121        ]);
122
123        assert_eq!(gradient.at(0.0), ThemeColor::new(255, 0, 0));
124        assert_eq!(gradient.at(0.5), ThemeColor::new(0, 255, 0));
125        assert_eq!(gradient.at(1.0), ThemeColor::new(0, 0, 255));
126    }
127
128    #[test]
129    fn test_generate() {
130        let gradient = Gradient::new(vec![
131            ThemeColor::new(0, 0, 0),
132            ThemeColor::new(255, 255, 255),
133        ]);
134
135        let colors = gradient.generate(3);
136        assert_eq!(colors.len(), 3);
137        assert_eq!(colors[0], ThemeColor::new(0, 0, 0));
138        assert_eq!(colors[1], ThemeColor::new(127, 127, 127));
139        assert_eq!(colors[2], ThemeColor::new(255, 255, 255));
140    }
141}