Skip to main content

i3status_rs/themes/
color.rs

1//! Color specification
2//!
3//! Colors can be specified in any of these forms:
4//!
5//! - `none` or an empty string for no color.
6//! - `auto` to let the output determine the color.
7//! - `#RRGGBB[AA]`. The alpha byte ranges from `00` (transparent) to `FF` (opaque).
8//!   Alpha is optional and defaults to `FF`.
9//! - `hsv:H:S:V[:A]` for an HSVA color. Hue is in degrees; saturation, value,
10//!   and optional alpha are percentages from 0 to 100. Alpha defaults to 100.
11//! - `x:name` to look up a color in `~/.Xresources`. For example,
12//!   `x:background` uses the value of `*background`.
13//! - `sway:name` to use a color from the active sway bar configuration. For
14//!   example, `sway:urgent_workspace_bg` uses the bar's urgent workspace
15//!   background color. See [`sway`] for more details.
16
17pub mod sway;
18pub mod xresources;
19
20use crate::errors::*;
21use serde::de::{self, Deserializer, Visitor};
22use serde::{Deserialize, Serialize, Serializer};
23use smart_default::SmartDefault;
24use std::fmt;
25use std::ops::Add;
26use std::str::FromStr;
27
28/// An RGBA color (red, green, blue, alpha).
29#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
30pub struct Rgba {
31    pub r: u8,
32    pub g: u8,
33    pub b: u8,
34    pub a: u8,
35}
36
37impl Rgba {
38    /// Create a new RGBA color.
39    ///
40    /// `r`: red component (0 to 255).
41    ///
42    /// `g`: green component (0 to 255).
43    ///
44    /// `b`: blue component (0 to 255).
45    ///
46    /// `a`: alpha component (0 to 255).
47    pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
48        Self { r, g, b, a }
49    }
50
51    /// Create a new RGBA color from the `hex` value.
52    ///
53    /// ```let cyan = Rgba::from_hex(0xffffff);```
54    pub fn from_hex(hex: u32) -> Self {
55        let [r, g, b, a] = hex.to_be_bytes();
56        Self { r, g, b, a }
57    }
58}
59
60impl Add for Rgba {
61    type Output = Self;
62    fn add(self, rhs: Self) -> Self::Output {
63        Rgba::new(
64            self.r.saturating_add(rhs.r),
65            self.g.saturating_add(rhs.g),
66            self.b.saturating_add(rhs.b),
67            self.a.saturating_add(rhs.a),
68        )
69    }
70}
71
72/// An HSVA color (hue, saturation, value, alpha).
73#[derive(Copy, Clone, Debug, Default)]
74pub struct Hsva {
75    pub h: f64,
76    pub s: f64,
77    pub v: f64,
78    pub a: u8,
79}
80
81impl Hsva {
82    /// Create a new HSVA color.
83    ///
84    /// `h`: hue component (0 to 360)
85    ///
86    /// `s`: saturation component (0 to 1)
87    ///
88    /// `v`: value component (0 to 1)
89    ///
90    /// `a`: alpha component (0 to 255).
91    pub fn new(h: f64, s: f64, v: f64, a: u8) -> Self {
92        Self { h, s, v, a }
93    }
94}
95
96impl PartialEq for Hsva {
97    fn eq(&self, other: &Self) -> bool {
98        approx(self.h, other.h)
99            && approx(self.s, other.s)
100            && approx(self.v, other.v)
101            && self.a == other.a
102    }
103}
104
105impl From<Rgba> for Hsva {
106    fn from(rgba: Rgba) -> Self {
107        let r = rgba.r as f64 / 255.0;
108        let g = rgba.g as f64 / 255.0;
109        let b = rgba.b as f64 / 255.0;
110
111        let min = r.min(g.min(b));
112        let max = r.max(g.max(b));
113        let delta = max - min;
114
115        let v = max;
116        let s = match max > 1e-3 {
117            true => delta / max,
118            false => 0.0,
119        };
120        let h = match delta == 0.0 {
121            true => 0.0,
122            false => {
123                if r == max {
124                    (g - b) / delta
125                } else if g == max {
126                    2.0 + (b - r) / delta
127                } else {
128                    4.0 + (r - g) / delta
129                }
130            }
131        };
132        let h2 = ((h * 60.0) + 360.0) % 360.0;
133
134        Self::new(h2, s, v, rgba.a)
135    }
136}
137
138impl From<Hsva> for Rgba {
139    fn from(hsva: Hsva) -> Self {
140        let range = (hsva.h / 60.0) as u8;
141        let c = hsva.v * hsva.s;
142        let x = c * (1.0 - (((hsva.h / 60.0) % 2.0) - 1.0).abs());
143        let m = hsva.v - c;
144
145        let cm_scaled = ((c + m) * 255.0) as u8;
146        let xm_scaled = ((x + m) * 255.0) as u8;
147        let m_scaled = (m * 255.0) as u8;
148
149        match range {
150            0 => Self::new(cm_scaled, xm_scaled, m_scaled, hsva.a),
151            1 => Self::new(xm_scaled, cm_scaled, m_scaled, hsva.a),
152            2 => Self::new(m_scaled, cm_scaled, xm_scaled, hsva.a),
153            3 => Self::new(m_scaled, xm_scaled, cm_scaled, hsva.a),
154            4 => Self::new(xm_scaled, m_scaled, cm_scaled, hsva.a),
155            _ => Self::new(cm_scaled, m_scaled, xm_scaled, hsva.a),
156        }
157    }
158}
159
160impl Add for Hsva {
161    type Output = Self;
162    fn add(self, rhs: Self) -> Self::Output {
163        Hsva::new(
164            (self.h + rhs.h) % 360.,
165            (self.s + rhs.s).clamp(0., 1.),
166            (self.v + rhs.v).clamp(0., 1.),
167            self.a.saturating_add(rhs.a),
168        )
169    }
170}
171
172pub fn approx(a: f64, b: f64) -> bool {
173    if a == b {
174        return true;
175    }
176    let eps = 1e-2;
177    let abs_a = a.abs();
178    let abs_b = b.abs();
179    let diff = (abs_a - abs_b).abs();
180    if a == 0.0 || b == 0.0 || abs_a + abs_b < f64::EPSILON {
181        diff < eps * f64::EPSILON
182    } else {
183        diff / (abs_a + abs_b).min(f64::MAX) < eps
184    }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, SmartDefault)]
188pub enum Color {
189    #[default]
190    None,
191    Auto,
192    Rgba(Rgba),
193    Hsva(Hsva),
194}
195
196impl Color {
197    pub fn skip_ser(&self) -> bool {
198        matches!(self, Self::None | Self::Auto)
199    }
200}
201
202impl Add for Color {
203    type Output = Color;
204    fn add(self, rhs: Self) -> Self::Output {
205        match (self, rhs) {
206            // Do nothing
207            (x, Self::None | Self::Auto) | (Self::None | Self::Auto, x) => x,
208            // Hsva + Hsva => Hsva
209            (Color::Hsva(hsva1), Color::Hsva(hsva2)) => Color::Hsva(hsva1 + hsva2),
210            // Rgba + Rgba => Rgba
211            (Color::Rgba(rgba1), Color::Rgba(rgba2)) => Color::Rgba(rgba1 + rgba2),
212            // Hsva + Rgba => Hsva
213            // Rgba + Hsva => Hsva
214            (Color::Hsva(hsva), Color::Rgba(rgba)) | (Color::Rgba(rgba), Color::Hsva(hsva)) => {
215                Color::Hsva(hsva + rgba.into())
216            }
217        }
218    }
219}
220
221impl FromStr for Color {
222    type Err = Error;
223
224    fn from_str(color: &str) -> Result<Self, Self::Err> {
225        Ok(if color == "none" || color.is_empty() {
226            Color::None
227        } else if color == "auto" {
228            Color::Auto
229        } else if color.starts_with("hsv:") {
230            let err_msg = || format!("'{color}' is not a valid HSVA color");
231            let color = color.split_at(4).1;
232            let mut components = color.split(':').map(|x| x.parse::<f64>().or_error(err_msg));
233            let h = components.next().or_error(err_msg)??;
234            let s = components.next().or_error(err_msg)??;
235            let v = components.next().or_error(err_msg)??;
236            let a = components.next().unwrap_or(Ok(100.))?;
237            Color::Hsva(Hsva::new(h, s / 100., v / 100., (a / 100. * 255.) as u8))
238        } else if color.starts_with("x:") {
239            let name = color.split_at(2).1;
240            xresources::get_color(name)?
241                .or_error(|| format!("color '{name}' not defined in ~/.Xresources"))?
242                .parse()
243                .or_error(|| format!("invalid color definition '{name}'"))?
244        } else if color.starts_with("sway:") {
245            let name = color.split_at(5).1;
246            sway::get_color(name)?
247                .parse()
248                .or_error(|| format!("invalid color definition '{name}'"))?
249        } else {
250            let err_msg = || format!("'{color}' is not a valid RGBA color");
251            let rgb = color.get(1..7).or_error(err_msg)?;
252            let a = color.get(7..9).unwrap_or("FF");
253            Color::Rgba(Rgba::from_hex(
254                (u32::from_str_radix(rgb, 16).or_error(err_msg)? << 8)
255                    + u32::from_str_radix(a, 16).or_error(err_msg)?,
256            ))
257        })
258    }
259}
260
261impl Serialize for Color {
262    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
263    where
264        S: Serializer,
265    {
266        let format_rgba =
267            |rgba: Rgba| format!("#{:02X}{:02X}{:02X}{:02X}", rgba.r, rgba.g, rgba.b, rgba.a);
268        match *self {
269            Self::None | Self::Auto => serializer.serialize_none(),
270            Self::Rgba(rgba) => serializer.serialize_str(&format_rgba(rgba)),
271            Self::Hsva(hsva) => serializer.serialize_str(&format_rgba(hsva.into())),
272        }
273    }
274}
275
276impl<'de> Deserialize<'de> for Color {
277    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
278    where
279        D: Deserializer<'de>,
280    {
281        struct ColorVisitor;
282
283        impl Visitor<'_> for ColorVisitor {
284            type Value = Color;
285
286            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
287                formatter.write_str("color")
288            }
289
290            fn visit_str<E>(self, s: &str) -> Result<Color, E>
291            where
292                E: de::Error,
293            {
294                s.parse().serde_error()
295            }
296        }
297
298        deserializer.deserialize_any(ColorVisitor)
299    }
300}