1use std::borrow::Cow;
2
3use crate::utils::{lightness, mix};
4use iced_widget::core::{
5 Color, color,
6 theme::{Base, Mode, Style},
7};
8
9#[allow(clippy::cast_precision_loss)]
10macro_rules! from_argb {
11 ($hex:expr) => {{
12 let hex = $hex as u32;
13
14 let a = ((hex & 0xff000000) >> 24) as f32 / 255.0;
15 let r = (hex & 0x00ff0000) >> 16;
16 let g = (hex & 0x0000ff00) >> 8;
17 let b = (hex & 0x000000ff);
18
19 ::iced_widget::core::color!(r as u8, g as u8, b as u8, a)
20 }};
21}
22
23#[allow(clippy::large_enum_variant)]
24#[derive(Debug, Clone, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[cfg_attr(feature = "serde", serde(from = "Custom", into = "Custom"))]
27pub enum Theme {
28 Dark,
29 Light,
30 Custom(Custom),
31}
32
33impl Theme {
34 pub const ALL: &'static [Self] = &[Self::Dark, Self::Light];
35
36 pub fn new(name: impl Into<Cow<'static, str>>, colorscheme: ColorScheme) -> Self {
37 Self::Custom(Custom {
38 name: name.into(),
39 is_dark: lightness(colorscheme.surface.color) <= 0.5,
40 colorscheme,
41 })
42 }
43
44 pub const fn new_const(name: &'static str, colorscheme: ColorScheme) -> Self {
45 Self::Custom(Custom {
46 name: Cow::Borrowed(name),
47 is_dark: lightness(colorscheme.surface.color) <= 0.5,
48 colorscheme,
49 })
50 }
51
52 pub fn name(&self) -> &str {
53 match self {
54 Self::Dark => "Dark",
55 Self::Light => "Light",
56 Self::Custom(custom) => &custom.name,
57 }
58 }
59
60 pub fn is_dark(&self) -> bool {
61 match self {
62 Self::Dark => true,
63 Self::Light => false,
64 Self::Custom(custom) => custom.is_dark,
65 }
66 }
67
68 pub fn colors(&self) -> ColorScheme {
69 match self {
70 Self::Dark => ColorScheme::DARK,
71 Self::Light => ColorScheme::LIGHT,
72 Self::Custom(custom) => custom.colorscheme,
73 }
74 }
75}
76
77impl std::fmt::Display for Theme {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "{}", self.name())
80 }
81}
82
83impl Base for Theme {
84 fn default(preference: Mode) -> Self {
85 match preference {
86 Mode::None | Mode::Dark => Self::Dark,
87 Mode::Light => Self::Light,
88 }
89 }
90
91 fn mode(&self) -> Mode {
92 if self.is_dark() {
93 Mode::Dark
94 } else {
95 Mode::Light
96 }
97 }
98
99 fn base(&self) -> Style {
100 Style {
101 background_color: self.colors().surface.color,
102 text_color: self.colors().surface.text,
103 }
104 }
105
106 fn palette(&self) -> Option<iced_widget::theme::Palette> {
107 let colors = self.colors();
108
109 Some(iced_widget::theme::Palette {
110 background: colors.surface.color,
111 text: colors.surface.text,
112 primary: colors.primary.color,
113 success: colors.primary.container,
114 warning: mix(from_argb!(0xffffff00), colors.primary.color, 0.25),
115 danger: colors.error.color,
116 })
117 }
118
119 fn name(&self) -> &str {
120 self.name()
121 }
122}
123
124#[cfg(feature = "animate")]
125impl iced_anim::Animate for Theme {
126 fn components() -> usize {
127 ColorScheme::components()
128 }
129
130 fn update(&mut self, components: &mut impl Iterator<Item = f32>) {
131 let mut colorscheme = self.colors();
132 colorscheme.update(components);
133 *self = Self::Custom(Custom {
134 name: "Animating Theme".into(),
135 is_dark: lightness(colorscheme.surface.color) <= 0.5,
136 colorscheme,
137 });
138 }
139
140 fn distance_to(&self, end: &Self) -> Vec<f32> {
141 self.colors().distance_to(&end.colors())
142 }
143
144 fn lerp(&mut self, start: &Self, end: &Self, progress: f32) {
145 let mut colorscheme = self.colors();
146 colorscheme.lerp(&start.colors(), &end.colors(), progress);
147 *self = Self::Custom(Custom {
148 name: "Animating Theme".into(),
149 is_dark: lightness(colorscheme.surface.color) <= 0.5,
150 colorscheme,
151 });
152 }
153}
154
155#[derive(Debug, PartialEq)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
158pub struct Custom {
159 pub name: Cow<'static, str>,
161 pub is_dark: bool,
163 #[cfg_attr(feature = "serde", serde(flatten))]
165 pub colorscheme: ColorScheme,
166}
167
168impl From<Custom> for Theme {
169 fn from(custom: Custom) -> Self {
170 Self::Custom(custom)
171 }
172}
173
174impl From<Theme> for Custom {
175 fn from(theme: Theme) -> Self {
176 match theme {
177 Theme::Custom(custom) => custom,
178 theme => Self {
179 name: theme.name().to_owned().into(),
180 is_dark: theme.is_dark(),
181 colorscheme: theme.colors(),
182 },
183 }
184 }
185}
186
187impl Clone for Custom {
188 fn clone(&self) -> Self {
189 Self {
190 name: self.name.clone(),
191 is_dark: self.is_dark,
192 colorscheme: self.colorscheme,
193 }
194 }
195
196 fn clone_from(&mut self, source: &Self) {
197 self.name.clone_from(&source.name);
198 self.is_dark = source.is_dark;
199 self.colorscheme = source.colorscheme;
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq)]
208#[cfg_attr(feature = "animate", derive(iced_anim::Animate))]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub struct ColorScheme {
211 pub primary: ColorQuartet,
213 pub secondary: ColorQuartet,
215 pub tertiary: ColorQuartet,
217 pub error: ColorQuartet,
219 pub surface: Surface,
221 pub inverse: Inverse,
223 pub outline: Outline,
225 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
227 pub shadow: Color,
228 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
230 pub scrim: Color,
231}
232
233#[allow(clippy::cast_precision_loss)]
234impl ColorScheme {
235 const DARK: Self = Self {
236 primary: ColorQuartet {
237 color: color!(0xd0bcff),
238 text: color!(0x381e72),
239 container: color!(0x4f378b),
240 container_text: color!(0xeaddff),
241 },
242 secondary: ColorQuartet {
243 color: color!(0xccc2dc),
244 text: color!(0x332d41),
245 container: color!(0x4a4458),
246 container_text: color!(0xe8def8),
247 },
248 tertiary: ColorQuartet {
249 color: color!(0xefb8c8),
250 text: color!(0x492532),
251 container: color!(0x633b48),
252 container_text: color!(0xffd8e4),
253 },
254 error: ColorQuartet {
255 color: color!(0xf2b8b5),
256 text: color!(0x601410),
257 container: color!(0x8c1d18),
258 container_text: color!(0xf9dedc),
259 },
260 surface: Surface {
261 color: color!(0x141218),
262 text: color!(0xe6e0e9),
263 text_variant: color!(0xcac4d0),
264 container: SurfaceContainer {
265 lowest: color!(0x0f0d13),
266 low: color!(0x1d1b20),
267 base: color!(0x211f26),
268 high: color!(0x2b2930),
269 highest: color!(0x36343b),
270 },
271 },
272 inverse: Inverse {
273 inverse_surface: color!(0xe6e0e9),
274 inverse_surface_text: color!(0x322f35),
275 inverse_primary: color!(0x6750a4),
276 },
277 outline: Outline {
278 color: color!(0x938f99),
279 variant: color!(0x49454f),
280 },
281 shadow: color!(0x000000),
282 scrim: from_argb!(0x4d000000),
283 };
284
285 const LIGHT: Self = Self {
286 primary: ColorQuartet {
287 color: color!(0x6750a4),
288 text: color!(0xffffff),
289 container: color!(0xeaddff),
290 container_text: color!(0x21005d),
291 },
292 secondary: ColorQuartet {
293 color: color!(0x625b71),
294 text: color!(0xffffff),
295 container: color!(0xe8def8),
296 container_text: color!(0x1d192b),
297 },
298 tertiary: ColorQuartet {
299 color: color!(0x7d5260),
300 text: color!(0xffffff),
301 container: color!(0xffd8e4),
302 container_text: color!(0x31111d),
303 },
304 error: ColorQuartet {
305 color: color!(0xb3261e),
306 text: color!(0xffffff),
307 container: color!(0xf9dedc),
308 container_text: color!(0x410e0b),
309 },
310 surface: Surface {
311 color: color!(0xfef7ff),
312 text: color!(0x1d1b20),
313 text_variant: color!(0x49454f),
314 container: SurfaceContainer {
315 lowest: color!(0xffffff),
316 low: color!(0xf7f2fa),
317 base: color!(0xf3edf7),
318 high: color!(0xece6f0),
319 highest: color!(0xe6e0e9),
320 },
321 },
322 inverse: Inverse {
323 inverse_surface: color!(0x322f35),
324 inverse_surface_text: color!(0xf5eff7),
325 inverse_primary: color!(0xd0bcff),
326 },
327 outline: Outline {
328 color: color!(0x79747e),
329 variant: color!(0xcac4d0),
330 },
331 shadow: color!(0x000000),
332 scrim: from_argb!(0x4d000000),
333 };
334}
335
336impl ColorScheme {
337 pub fn interpolate(from: Self, to: Self, amount: f32) -> Self {
338 Self {
339 primary: interpolate_color_quartet(from.primary, to.primary, amount),
340 secondary: interpolate_color_quartet(from.secondary, to.secondary, amount),
341 tertiary: interpolate_color_quartet(from.tertiary, to.tertiary, amount),
342 error: interpolate_color_quartet(from.error, to.error, amount),
343 surface: interpolate_surface(from.surface, to.surface, amount),
344 inverse: interpolate_inverse(from.inverse, to.inverse, amount),
345 outline: interpolate_outline(from.outline, to.outline, amount),
346 shadow: interpolate_color(from.shadow, to.shadow, amount),
347 scrim: interpolate_color(from.scrim, to.scrim, amount),
348 }
349 }
350}
351
352fn interpolate_color_quartet(from: ColorQuartet, to: ColorQuartet, amount: f32) -> ColorQuartet {
353 ColorQuartet {
354 color: interpolate_color(from.color, to.color, amount),
355 text: interpolate_color(from.text, to.text, amount),
356 container: interpolate_color(from.container, to.container, amount),
357 container_text: interpolate_color(from.container_text, to.container_text, amount),
358 }
359}
360
361fn interpolate_surface(from: Surface, to: Surface, amount: f32) -> Surface {
362 Surface {
363 color: interpolate_color(from.color, to.color, amount),
364 text: interpolate_color(from.text, to.text, amount),
365 text_variant: interpolate_color(from.text_variant, to.text_variant, amount),
366 container: interpolate_surface_container(from.container, to.container, amount),
367 }
368}
369
370fn interpolate_surface_container(
371 from: SurfaceContainer,
372 to: SurfaceContainer,
373 amount: f32,
374) -> SurfaceContainer {
375 SurfaceContainer {
376 lowest: interpolate_color(from.lowest, to.lowest, amount),
377 low: interpolate_color(from.low, to.low, amount),
378 base: interpolate_color(from.base, to.base, amount),
379 high: interpolate_color(from.high, to.high, amount),
380 highest: interpolate_color(from.highest, to.highest, amount),
381 }
382}
383
384fn interpolate_inverse(from: Inverse, to: Inverse, amount: f32) -> Inverse {
385 Inverse {
386 inverse_surface: interpolate_color(from.inverse_surface, to.inverse_surface, amount),
387 inverse_surface_text: interpolate_color(
388 from.inverse_surface_text,
389 to.inverse_surface_text,
390 amount,
391 ),
392 inverse_primary: interpolate_color(from.inverse_primary, to.inverse_primary, amount),
393 }
394}
395
396fn interpolate_outline(from: Outline, to: Outline, amount: f32) -> Outline {
397 Outline {
398 color: interpolate_color(from.color, to.color, amount),
399 variant: interpolate_color(from.variant, to.variant, amount),
400 }
401}
402
403fn interpolate_color(from: Color, to: Color, amount: f32) -> Color {
404 if amount <= 0.0 {
405 return from;
406 }
407
408 if amount >= 1.0 {
409 return to;
410 }
411
412 Color {
413 r: interpolate_component(from.r, to.r, amount),
414 g: interpolate_component(from.g, to.g, amount),
415 b: interpolate_component(from.b, to.b, amount),
416 a: interpolate_component(from.a, to.a, amount),
417 }
418}
419
420fn interpolate_component(from: f32, to: f32, amount: f32) -> f32 {
421 from + (to - from) * amount
422}
423
424#[cfg(test)]
425#[path = "../../tests/design/theme.rs"]
426mod color_scheme_tests;
427
428#[derive(Debug, Clone, Copy, PartialEq)]
429#[cfg_attr(feature = "animate", derive(iced_anim::Animate))]
430#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
431pub struct ColorQuartet {
432 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
433 pub color: Color,
434 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
435 pub text: Color,
436 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
437 pub container: Color,
438 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
439 pub container_text: Color,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq)]
443#[cfg_attr(feature = "animate", derive(iced_anim::Animate))]
444#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
445pub struct Surface {
446 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
447 pub color: Color,
448 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
449 pub text: Color,
450 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
451 pub text_variant: Color,
452 pub container: SurfaceContainer,
453}
454
455#[derive(Debug, Clone, Copy, PartialEq)]
456#[cfg_attr(feature = "animate", derive(iced_anim::Animate))]
457#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
458pub struct SurfaceContainer {
459 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
460 pub lowest: Color,
461 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
462 pub low: Color,
463 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
464 pub base: Color,
465 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
466 pub high: Color,
467 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
468 pub highest: Color,
469}
470
471#[derive(Debug, Clone, Copy, PartialEq)]
472#[cfg_attr(feature = "animate", derive(iced_anim::Animate))]
473#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
474pub struct Inverse {
475 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
476 pub inverse_surface: Color,
477 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
478 pub inverse_surface_text: Color,
479 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
480 pub inverse_primary: Color,
481}
482
483#[derive(Debug, Clone, Copy, PartialEq)]
484#[cfg_attr(feature = "animate", derive(iced_anim::Animate))]
485#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
486pub struct Outline {
487 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
488 pub color: Color,
489 #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
490 pub variant: Color,
491}
492
493#[cfg(feature = "serde")]
494mod color_serde {
495 use iced_widget::core::Color;
496 use serde::{Deserialize, Deserializer, Serialize, Serializer};
497
498 use crate::utils::{color_to_argb, parse_argb};
499
500 pub fn deserialize<'de, D>(deserializer: D) -> Result<Color, D::Error>
501 where
502 D: Deserializer<'de>,
503 {
504 Ok(String::deserialize(deserializer)
505 .map(|hex| parse_argb(&hex))?
506 .unwrap_or(Color::TRANSPARENT))
507 }
508
509 pub fn serialize<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
510 where
511 S: Serializer,
512 {
513 color_to_argb(*color).serialize(serializer)
514 }
515}