Skip to main content

floem_picker/
color.rs

1//! SolidColor type — the public color representation for floem-picker.
2//!
3//! Stores RGBA as f64 values in 0.0–1.0 range. Uses direct math for color
4//! space conversions and hex parsing/formatting.
5
6use std::fmt;
7use std::str::FromStr;
8
9use crate::math;
10
11/// RGBA color with components in the 0.0–1.0 range.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct SolidColor {
14    r: f64,
15    g: f64,
16    b: f64,
17    a: f64,
18}
19
20impl SolidColor {
21    /// Red component (0.0–1.0).
22    pub fn r(&self) -> f64 {
23        self.r
24    }
25    /// Green component (0.0–1.0).
26    pub fn g(&self) -> f64 {
27        self.g
28    }
29    /// Blue component (0.0–1.0).
30    pub fn b(&self) -> f64 {
31        self.b
32    }
33    /// Alpha component (0.0–1.0).
34    pub fn a(&self) -> f64 {
35        self.a
36    }
37
38    /// All four components as a tuple (r, g, b, a), each 0.0–1.0.
39    pub fn rgba(&self) -> (f64, f64, f64, f64) {
40        (self.r, self.g, self.b, self.a)
41    }
42}
43
44impl Default for SolidColor {
45    fn default() -> Self {
46        Self {
47            r: 0.5,
48            g: 0.5,
49            b: 0.5,
50            a: 1.0,
51        }
52    }
53}
54
55impl SolidColor {
56    /// Create from 0–255 RGB values with full opacity.
57    pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
58        Self {
59            r: r as f64 / 255.0,
60            g: g as f64 / 255.0,
61            b: b as f64 / 255.0,
62            a: 1.0,
63        }
64    }
65
66    /// Convert to 0–255 RGB tuple.
67    pub fn to_rgb(&self) -> (u8, u8, u8) {
68        (
69            (self.r * 255.0).round() as u8,
70            (self.g * 255.0).round() as u8,
71            (self.b * 255.0).round() as u8,
72        )
73    }
74
75    /// Parse a hex string (with or without `#`, 3, 6, or 8 chars).
76    ///
77    /// 8-char hex is interpreted as RRGGBBAA. 3 and 6-char hex default to full opacity.
78    pub fn from_hex(hex: &str) -> Option<Self> {
79        let stripped = hex.trim_start_matches('#');
80        if !stripped.chars().all(|c| c.is_ascii_hexdigit()) {
81            return None;
82        }
83        match stripped.len() {
84            3 => {
85                let r = u8::from_str_radix(&stripped[0..1], 16).ok()?;
86                let g = u8::from_str_radix(&stripped[1..2], 16).ok()?;
87                let b = u8::from_str_radix(&stripped[2..3], 16).ok()?;
88                Some(Self {
89                    r: (r * 17) as f64 / 255.0,
90                    g: (g * 17) as f64 / 255.0,
91                    b: (b * 17) as f64 / 255.0,
92                    a: 1.0,
93                })
94            }
95            6 => {
96                let r = u8::from_str_radix(&stripped[0..2], 16).ok()?;
97                let g = u8::from_str_radix(&stripped[2..4], 16).ok()?;
98                let b = u8::from_str_radix(&stripped[4..6], 16).ok()?;
99                Some(Self {
100                    r: r as f64 / 255.0,
101                    g: g as f64 / 255.0,
102                    b: b as f64 / 255.0,
103                    a: 1.0,
104                })
105            }
106            8 => {
107                let r = u8::from_str_radix(&stripped[0..2], 16).ok()?;
108                let g = u8::from_str_radix(&stripped[2..4], 16).ok()?;
109                let b = u8::from_str_radix(&stripped[4..6], 16).ok()?;
110                let a = u8::from_str_radix(&stripped[6..8], 16).ok()?;
111                Some(Self {
112                    r: r as f64 / 255.0,
113                    g: g as f64 / 255.0,
114                    b: b as f64 / 255.0,
115                    a: a as f64 / 255.0,
116                })
117            }
118            _ => None,
119        }
120    }
121
122    /// Format as uppercase hex (no `#` prefix).
123    ///
124    /// Returns 6 chars (RRGGBB) when alpha is 1.0.
125    /// Returns 8 chars (RRGGBBAA) when alpha is less than 1.0.
126    pub fn to_hex(&self) -> String {
127        let (r, g, b) = self.to_rgb();
128        let a = (self.a * 255.0).round() as u8;
129        if a == 255 {
130            format!("{:02X}{:02X}{:02X}", r, g, b)
131        } else {
132            format!("{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
133        }
134    }
135
136    /// Create from HSB/HSV values (all 0.0–1.0).
137    pub fn from_hsb(h: f64, s: f64, b: f64, a: f64) -> Self {
138        let (r, g, bl) = math::hsb_to_rgb(h, s, b);
139        Self { r, g, b: bl, a }
140    }
141
142    /// Convert to HSB (all 0.0–1.0). Returns (h, s, b).
143    pub fn to_hsb(&self) -> (f64, f64, f64) {
144        math::rgb_to_hsb(self.r, self.g, self.b)
145    }
146
147    /// Create from HSL values (all 0.0–1.0).
148    pub fn from_hsl(h: f64, s: f64, l: f64, a: f64) -> Self {
149        let (hb, sb, vb) = math::hsl_to_hsb(h, s, l);
150        let (r, g, bl) = math::hsb_to_rgb(hb, sb, vb);
151        Self { r, g, b: bl, a }
152    }
153
154    /// Convert to HSL (all 0.0–1.0). Returns (h, s, l).
155    pub fn to_hsl(&self) -> (f64, f64, f64) {
156        let (h, s, v) = math::rgb_to_hsb(self.r, self.g, self.b);
157        math::hsb_to_hsl(h, s, v)
158    }
159
160    /// Create from f64 RGBA. Values are clamped to 0.0–1.0.
161    pub fn from_rgba(r: f64, g: f64, b: f64, a: f64) -> Self {
162        Self {
163            r: r.clamp(0.0, 1.0),
164            g: g.clamp(0.0, 1.0),
165            b: b.clamp(0.0, 1.0),
166            a: a.clamp(0.0, 1.0),
167        }
168    }
169}
170
171impl fmt::Display for SolidColor {
172    /// Formats as `#RRGGBB` or `#RRGGBBAA` (when alpha < 1.0).
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        write!(f, "#{}", self.to_hex())
175    }
176}
177
178impl FromStr for SolidColor {
179    type Err = String;
180
181    /// Parses a hex color string (with or without `#`, 3/6/8 hex chars).
182    fn from_str(s: &str) -> Result<Self, Self::Err> {
183        SolidColor::from_hex(s).ok_or_else(|| format!("invalid hex color: {s}"))
184    }
185}