1use 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 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
312fn 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 (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 let hue = RgbHue::from_degrees(h_deg);
346 from_hsva(Hsva::new(hue, s, v, 1.0))
347}