Skip to main content

material_ui_rs/design/
theme.rs

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/// A custom [`Theme`].
156#[derive(Debug, PartialEq)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
158pub struct Custom {
159    /// The [`Theme`]'s name.
160    pub name: Cow<'static, str>,
161    /// Whether the [`Theme`] is dark.
162    pub is_dark: bool,
163    /// The [`Theme`]'s [`ColorScheme`].
164    #[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/// A [`Theme`]'s color scheme.
204///
205/// These color roles are based on Material Design 3. For more information about them, visit the
206/// official [M3 documentation](https://m3.material.io/styles/color/roles).
207#[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    /// The primary colors.
212    pub primary: ColorQuartet,
213    /// The secondary colors.
214    pub secondary: ColorQuartet,
215    /// The tertiary colors.
216    pub tertiary: ColorQuartet,
217    /// The error colors.
218    pub error: ColorQuartet,
219    /// The surface colors.
220    pub surface: Surface,
221    /// The inverse colors.
222    pub inverse: Inverse,
223    /// The outline colors.
224    pub outline: Outline,
225    /// The shadow color.
226    #[cfg_attr(feature = "serde", serde(with = "color_serde"))]
227    pub shadow: Color,
228    /// The scrim color.
229    #[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}