Skip to main content

rosace_theme/
color.rs

1//! Color primitives: `Color` (f32 RGBA), `ColorScheme`, and `Palette`.
2
3/// A color value with red, green, blue, and alpha channels, each in `[0.0, 1.0]`.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Color {
6    pub r: f32,
7    pub g: f32,
8    pub b: f32,
9    pub a: f32,
10}
11
12impl Color {
13    /// Creates an opaque color from RGB components in `[0.0, 1.0]`.
14    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
15        Self { r, g, b, a: 1.0 }
16    }
17
18    /// Creates a color from RGBA components in `[0.0, 1.0]`.
19    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
20        Self { r, g, b, a }
21    }
22
23    /// Creates an opaque color from a 24-bit hex value (`0xRRGGBB`).
24    pub fn from_hex(hex: u32) -> Self {
25        let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
26        let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
27        let b = (hex & 0xFF) as f32 / 255.0;
28        Self::rgb(r, g, b)
29    }
30
31    /// Returns a copy of this color with the alpha channel replaced by `a`.
32    pub fn with_alpha(self, a: f32) -> Self {
33        Self { a, ..self }
34    }
35
36    /// Linearly interpolates between `self` and `other` by factor `t` (0 → self, 1 → other).
37    pub fn lerp(self, other: Self, t: f32) -> Self {
38        Self {
39            r: self.r + (other.r - self.r) * t,
40            g: self.g + (other.g - self.g) * t,
41            b: self.b + (other.b - self.b) * t,
42            a: self.a + (other.a - self.a) * t,
43        }
44    }
45
46    /// Fully opaque white.
47    pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
48    /// Fully opaque black.
49    pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
50    /// Fully transparent black.
51    pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
52}
53
54/// The semantic color roles used throughout a theme (Material Design 3 inspired).
55#[derive(Debug, Clone)]
56pub struct ColorScheme {
57    pub primary: Color,
58    pub on_primary: Color,
59    pub primary_container: Color,
60    pub on_primary_container: Color,
61    pub secondary: Color,
62    pub on_secondary: Color,
63    pub surface: Color,
64    pub on_surface: Color,
65    pub surface_variant: Color,
66    pub background: Color,
67    pub on_background: Color,
68    pub error: Color,
69    pub on_error: Color,
70    pub outline: Color,
71    pub shadow: Color,
72}
73
74// ---------------------------------------------------------------------------
75// Tests
76// ---------------------------------------------------------------------------
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn color_rgb_sets_alpha_to_one() {
84        let c = Color::rgb(0.5, 0.5, 0.5);
85        assert_eq!(c.a, 1.0);
86    }
87
88    #[test]
89    fn color_from_hex_parses_correctly() {
90        // 0x6750A4 → r=103, g=80, b=164
91        let c = Color::from_hex(0x6750A4);
92        let expected_r = 103.0_f32 / 255.0;
93        let expected_g = 80.0_f32 / 255.0;
94        let expected_b = 164.0_f32 / 255.0;
95        assert!((c.r - expected_r).abs() < 1e-5, "r mismatch: {}", c.r);
96        assert!((c.g - expected_g).abs() < 1e-5, "g mismatch: {}", c.g);
97        assert!((c.b - expected_b).abs() < 1e-5, "b mismatch: {}", c.b);
98        assert_eq!(c.a, 1.0);
99    }
100
101    #[test]
102    fn color_from_hex_white() {
103        let c = Color::from_hex(0xFFFFFF);
104        assert!((c.r - 1.0).abs() < 1e-5);
105        assert!((c.g - 1.0).abs() < 1e-5);
106        assert!((c.b - 1.0).abs() < 1e-5);
107    }
108
109    #[test]
110    fn color_lerp_midpoint() {
111        let a = Color::rgb(0.0, 0.0, 0.0);
112        let b = Color::rgb(1.0, 1.0, 1.0);
113        let mid = a.lerp(b, 0.5);
114        assert!((mid.r - 0.5).abs() < 1e-6);
115        assert!((mid.g - 0.5).abs() < 1e-6);
116        assert!((mid.b - 0.5).abs() < 1e-6);
117        assert!((mid.a - 1.0).abs() < 1e-6);
118    }
119
120    #[test]
121    fn color_lerp_t0_returns_self() {
122        let a = Color::rgb(0.2, 0.4, 0.6);
123        let b = Color::rgb(1.0, 1.0, 1.0);
124        let result = a.lerp(b, 0.0);
125        assert!((result.r - a.r).abs() < 1e-6);
126        assert!((result.g - a.g).abs() < 1e-6);
127        assert!((result.b - a.b).abs() < 1e-6);
128    }
129
130    #[test]
131    fn color_lerp_t1_returns_other() {
132        let a = Color::rgb(0.0, 0.0, 0.0);
133        let b = Color::rgb(0.3, 0.6, 0.9);
134        let result = a.lerp(b, 1.0);
135        assert!((result.r - b.r).abs() < 1e-6);
136        assert!((result.g - b.g).abs() < 1e-6);
137        assert!((result.b - b.b).abs() < 1e-6);
138    }
139
140    #[test]
141    fn color_with_alpha() {
142        let c = Color::WHITE.with_alpha(0.5);
143        assert_eq!(c.r, 1.0);
144        assert_eq!(c.g, 1.0);
145        assert_eq!(c.b, 1.0);
146        assert!((c.a - 0.5).abs() < 1e-6);
147    }
148
149    #[test]
150    fn color_constants() {
151        assert_eq!(Color::WHITE.a, 1.0);
152        assert_eq!(Color::BLACK.r, 0.0);
153        assert_eq!(Color::TRANSPARENT.a, 0.0);
154    }
155}