Skip to main content

flowsurface_data/config/
theme.rs

1/// Most of the stuff here is exact copy of some of the code from
2/// <https://github.com/iced-rs/iced/blob/master/core/src/theme/palette.rs> &
3/// <https://github.com/squidowl/halloy/blob/main/data/src/appearance/theme.rs>
4/// All credits and thanks to the authors of [`Halloy`] and [`iced_core`]
5use iced_core::{
6    Color,
7    theme::{Custom, Palette},
8};
9use palette::{
10    FromColor, Hsva, RgbHue,
11    rgb::{Rgb, Rgba},
12};
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone)]
16pub struct Theme(pub iced_core::Theme);
17
18#[derive(Serialize, Deserialize)]
19struct SerTheme {
20    name: String,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    palette: Option<Palette>,
23}
24
25impl Default for Theme {
26    fn default() -> Self {
27        Self(iced_core::Theme::Custom(default_theme().into()))
28    }
29}
30
31impl From<Theme> for iced_core::Theme {
32    fn from(val: Theme) -> Self {
33        val.0
34    }
35}
36
37pub fn default_theme() -> Custom {
38    Custom::new(
39        "Flowsurface".to_string(),
40        Palette {
41            background: Color::from_rgb8(24, 22, 22),
42            text: Color::from_rgb8(197, 201, 197),
43            primary: Color::from_rgb8(200, 200, 200),
44            success: Color::from_rgb8(81, 205, 160),
45            danger: Color::from_rgb8(192, 80, 77),
46            warning: Color::from_rgb8(238, 216, 139),
47        },
48    )
49}
50
51impl Serialize for Theme {
52    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
53    where
54        S: serde::Serializer,
55    {
56        if let iced_core::Theme::Custom(custom) = &self.0 {
57            let is_default_theme = custom.to_string() == "Flowsurface";
58            let ser_theme = SerTheme {
59                name: if is_default_theme {
60                    "flowsurface"
61                } else {
62                    "custom"
63                }
64                .to_string(),
65                palette: if is_default_theme {
66                    None
67                } else {
68                    Some(self.0.palette())
69                },
70            };
71            ser_theme.serialize(serializer)
72        } else {
73            let theme_str = match self.0 {
74                iced_core::Theme::Ferra => "ferra",
75                iced_core::Theme::Dark => "dark",
76                iced_core::Theme::Light => "light",
77                iced_core::Theme::Dracula => "dracula",
78                iced_core::Theme::Nord => "nord",
79                iced_core::Theme::SolarizedLight => "solarized_light",
80                iced_core::Theme::SolarizedDark => "solarized_dark",
81                iced_core::Theme::GruvboxLight => "gruvbox_light",
82                iced_core::Theme::GruvboxDark => "gruvbox_dark",
83                iced_core::Theme::CatppuccinLatte => "catppuccino_latte",
84                iced_core::Theme::CatppuccinFrappe => "catppuccino_frappe",
85                iced_core::Theme::CatppuccinMacchiato => "catppuccino_macchiato",
86                iced_core::Theme::CatppuccinMocha => "catppuccino_mocha",
87                iced_core::Theme::TokyoNight => "tokyo_night",
88                iced_core::Theme::TokyoNightStorm => "tokyo_night_storm",
89                iced_core::Theme::TokyoNightLight => "tokyo_night_light",
90                iced_core::Theme::KanagawaWave => "kanagawa_wave",
91                iced_core::Theme::KanagawaDragon => "kanagawa_dragon",
92                iced_core::Theme::KanagawaLotus => "kanagawa_lotus",
93                iced_core::Theme::Moonfly => "moonfly",
94                iced_core::Theme::Nightfly => "nightfly",
95                iced_core::Theme::Oxocarbon => "oxocarbon",
96                _ => unreachable!(),
97            };
98            theme_str.serialize(serializer)
99        }
100    }
101}
102
103impl<'de> Deserialize<'de> for Theme {
104    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105    where
106        D: serde::Deserializer<'de>,
107    {
108        let value =
109            serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?;
110
111        if let Some(s) = value.as_str() {
112            let theme = match s {
113                "ferra" => iced_core::Theme::Ferra,
114                "dark" => iced_core::Theme::Dark,
115                "light" => iced_core::Theme::Light,
116                "dracula" => iced_core::Theme::Dracula,
117                "nord" => iced_core::Theme::Nord,
118                "solarized_light" => iced_core::Theme::SolarizedLight,
119                "solarized_dark" => iced_core::Theme::SolarizedDark,
120                "gruvbox_light" => iced_core::Theme::GruvboxLight,
121                "gruvbox_dark" => iced_core::Theme::GruvboxDark,
122                "catppuccino_latte" => iced_core::Theme::CatppuccinLatte,
123                "catppuccino_frappe" => iced_core::Theme::CatppuccinFrappe,
124                "catppuccino_macchiato" => iced_core::Theme::CatppuccinMacchiato,
125                "catppuccino_mocha" => iced_core::Theme::CatppuccinMocha,
126                "tokyo_night" => iced_core::Theme::TokyoNight,
127                "tokyo_night_storm" => iced_core::Theme::TokyoNightStorm,
128                "tokyo_night_light" => iced_core::Theme::TokyoNightLight,
129                "kanagawa_wave" => iced_core::Theme::KanagawaWave,
130                "kanagawa_dragon" => iced_core::Theme::KanagawaDragon,
131                "kanagawa_lotus" => iced_core::Theme::KanagawaLotus,
132                "moonfly" => iced_core::Theme::Moonfly,
133                "nightfly" => iced_core::Theme::Nightfly,
134                "oxocarbon" => iced_core::Theme::Oxocarbon,
135                "flowsurface" => Theme::default().0,
136                _ => {
137                    return Err(serde::de::Error::custom(format!("Invalid theme: {}", s)));
138                }
139            };
140            return Ok(Theme(theme));
141        }
142
143        let serialized = SerTheme::deserialize(value).map_err(serde::de::Error::custom)?;
144
145        let theme = match serialized.name.as_str() {
146            "flowsurface" => Theme::default().0,
147            "custom" => {
148                if let Some(palette) = serialized.palette {
149                    iced_core::Theme::Custom(Custom::new("Custom".to_string(), palette).into())
150                } else {
151                    return Err(serde::de::Error::custom(
152                        "Custom theme missing palette data",
153                    ));
154                }
155            }
156            _ => return Err(serde::de::Error::custom("Invalid theme")),
157        };
158
159        Ok(Theme(theme))
160    }
161}
162
163pub fn hex_to_color(hex: &str) -> Option<Color> {
164    if hex.len() == 7 || hex.len() == 9 {
165        let hash = &hex[0..1];
166        let r = u8::from_str_radix(&hex[1..3], 16);
167        let g = u8::from_str_radix(&hex[3..5], 16);
168        let b = u8::from_str_radix(&hex[5..7], 16);
169        let a = (hex.len() == 9)
170            .then(|| u8::from_str_radix(&hex[7..9], 16).ok())
171            .flatten();
172
173        return match (hash, r, g, b, a) {
174            ("#", Ok(r), Ok(g), Ok(b), None) => Some(Color {
175                r: f32::from(r) / 255.0,
176                g: f32::from(g) / 255.0,
177                b: f32::from(b) / 255.0,
178                a: 1.0,
179            }),
180            ("#", Ok(r), Ok(g), Ok(b), Some(a)) => Some(Color {
181                r: f32::from(r) / 255.0,
182                g: f32::from(g) / 255.0,
183                b: f32::from(b) / 255.0,
184                a: f32::from(a) / 255.0,
185            }),
186            _ => None,
187        };
188    }
189
190    None
191}
192
193pub fn color_to_hex(color: Color) -> String {
194    use std::fmt::Write;
195
196    let mut hex = String::with_capacity(9);
197
198    let [r, g, b, a] = color.into_rgba8();
199
200    let _ = write!(&mut hex, "#");
201    let _ = write!(&mut hex, "{r:02X}");
202    let _ = write!(&mut hex, "{g:02X}");
203    let _ = write!(&mut hex, "{b:02X}");
204
205    if a < u8::MAX {
206        let _ = write!(&mut hex, "{a:02X}");
207    }
208
209    hex
210}
211
212pub fn from_hsva(color: Hsva) -> Color {
213    to_color(palette::Srgba::from_color(color))
214}
215
216fn to_color(rgba: Rgba) -> Color {
217    Color {
218        r: rgba.color.red,
219        g: rgba.color.green,
220        b: rgba.color.blue,
221        a: rgba.alpha,
222    }
223}
224
225pub fn to_hsva(color: Color) -> Hsva {
226    Hsva::from_color(to_rgba(color))
227}
228
229fn to_rgb(color: Color) -> Rgb {
230    Rgb {
231        red: color.r,
232        green: color.g,
233        blue: color.b,
234        ..Rgb::default()
235    }
236}
237
238fn to_rgba(color: Color) -> Rgba {
239    Rgba {
240        alpha: color.a,
241        color: to_rgb(color),
242    }
243}
244
245pub fn darken(color: Color, amount: f32) -> Color {
246    let mut hsl = to_hsl(color);
247
248    hsl.l = if hsl.l - amount < 0.0 {
249        0.0
250    } else {
251        hsl.l - amount
252    };
253
254    from_hsl(hsl)
255}
256
257pub fn lighten(color: Color, amount: f32) -> Color {
258    let mut hsl = to_hsl(color);
259
260    hsl.l = if hsl.l + amount > 1.0 {
261        1.0
262    } else {
263        hsl.l + amount
264    };
265
266    from_hsl(hsl)
267}
268
269fn to_hsl(color: Color) -> Hsl {
270    let x_max = color.r.max(color.g).max(color.b);
271    let x_min = color.r.min(color.g).min(color.b);
272    let c = x_max - x_min;
273    let l = x_max.midpoint(x_min);
274
275    let h = if c == 0.0 {
276        0.0
277    } else if x_max == color.r {
278        60.0 * ((color.g - color.b) / c).rem_euclid(6.0)
279    } else if x_max == color.g {
280        60.0 * (((color.b - color.r) / c) + 2.0)
281    } else {
282        // x_max == color.b
283        60.0 * (((color.r - color.g) / c) + 4.0)
284    };
285
286    let s = if l == 0.0 || l == 1.0 {
287        0.0
288    } else {
289        (x_max - l) / l.min(1.0 - l)
290    };
291
292    Hsl {
293        h,
294        s,
295        l,
296        a: color.a,
297    }
298}
299
300pub fn is_dark(color: Color) -> bool {
301    let brightness = (color.r * 299.0 + color.g * 587.0 + color.b * 114.0) / 1000.0;
302    brightness < 0.5
303}
304
305struct Hsl {
306    h: f32,
307    s: f32,
308    l: f32,
309    a: f32,
310}
311
312// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
313fn from_hsl(hsl: Hsl) -> Color {
314    let c = (1.0 - (2.0 * hsl.l - 1.0).abs()) * hsl.s;
315    let h = hsl.h / 60.0;
316    let x = c * (1.0 - (h.rem_euclid(2.0) - 1.0).abs());
317
318    let (r1, g1, b1) = if h < 1.0 {
319        (c, x, 0.0)
320    } else if h < 2.0 {
321        (x, c, 0.0)
322    } else if h < 3.0 {
323        (0.0, c, x)
324    } else if h < 4.0 {
325        (0.0, x, c)
326    } else if h < 5.0 {
327        (x, 0.0, c)
328    } else {
329        // h < 6.0
330        (c, 0.0, x)
331    };
332
333    let m = hsl.l - (c / 2.0);
334
335    Color {
336        r: r1 + m,
337        g: g1 + m,
338        b: b1 + m,
339        a: hsl.a,
340    }
341}
342
343pub fn from_hsv_degrees(h_deg: f32, s: f32, v: f32) -> Color {
344    // Hue in degrees [0,360), s,v in [0,1]
345    let hue = RgbHue::from_degrees(h_deg);
346    from_hsva(Hsva::new(hue, s, v, 1.0))
347}