Skip to main content

freya_components/theming/
macros.rs

1use std::time::Duration;
2
3#[doc(hidden)]
4pub use ::paste::paste;
5use freya_core::prelude::*;
6use torin::{
7    gaps::Gaps,
8    size::Size,
9};
10
11use crate::theming::component_themes::ColorsSheet;
12
13#[macro_export]
14macro_rules! define_theme {
15    (NOTHING=) => {};
16
17    (
18        @ext_impls
19        [ $head_ty:ident $($rest_ty:ident)* ]
20        [ $head_field:ident $($rest_field:ident)* ]
21        $name:ident ;
22        $( $(#[$field_attrs:meta])* $field_name:ident : $field_ty:ty , )*
23    ) => {
24        $crate::theming::macros::paste! {
25            impl [<$name ThemePartialExt>] for $head_ty {
26                $(
27                    $(#[$field_attrs])*
28                    fn $field_name(mut self, $field_name: impl Into<$field_ty>) -> Self {
29                        self.$head_field = Some(self.$head_field.unwrap_or_default().$field_name($field_name));
30                        self
31                    }
32                )*
33            }
34        }
35        $crate::define_theme! {
36            @ext_impls
37            [ $($rest_ty)* ]
38            [ $($rest_field)* ]
39            $name ;
40            $( $(#[$field_attrs])* $field_name : $field_ty , )*
41        }
42    };
43
44    (
45        @ext_impls
46        [] []
47        $name:ident ;
48        $( $(#[$field_attrs:meta])* $field_name:ident : $field_ty:ty , )*
49    ) => {};
50
51    // Emits the structs and their inherent impls.
52    (
53        @types
54        $(#[$attrs:meta])*
55        $(%[component$($component_attr_control:tt)?])?
56        pub $name:ident {
57            $(
58                %[fields$($cows_attr_control:tt)?]
59                $(
60                    $(#[$field_attrs:meta])*
61                    $field_name:ident: $field_ty:ty,
62                )*
63            )?
64    }) => {
65        $crate::define_theme!(NOTHING=$($($component_attr_control)?)?);
66        $crate::theming::macros::paste! {
67            #[derive(Default, Clone, Debug, PartialEq)]
68            #[doc = "You can use this to change a theme for only one component, with the `theme` property."]
69            $(#[$attrs])*
70            pub struct [<$name ThemePartial>] {
71                $($(
72                    $(#[$field_attrs])*
73                    pub $field_name: Option<$crate::theming::macros::Preference<$field_ty>>,
74                )*)?
75            }
76
77            #[derive(Clone, Debug, PartialEq)]
78            $(#[doc = "Theming properties for the `" $name "` component."] $($component_attr_control)?)?
79            $(#[$attrs])*
80            pub struct [<$name ThemePreference>] {
81                $($(
82                    $(#[$field_attrs])*
83                    pub $field_name: $crate::theming::macros::Preference<$field_ty>,
84                )*)?
85            }
86
87            #[derive(Clone, Debug, PartialEq)]
88            $(#[doc = "Theming properties for the `" $name "` component."] $($component_attr_control)?)?
89            $(#[$attrs])*
90            pub struct [<$name Theme>] {
91                $($(
92                    $(#[$field_attrs])*
93                    pub $field_name: $field_ty,
94                )*)?
95            }
96
97            impl [<$name ThemePreference>] {
98                #[doc = "Checks each field in `optional` and if it's `Some`, it overwrites the corresponding `self` field."]
99                pub fn apply_optional(&mut self, optional: &[<$name ThemePartial>]) {
100
101                    $($(
102                        if let Some($field_name) = &optional.$field_name {
103                            self.$field_name = $field_name.clone();
104                        }
105                    )*)?
106                }
107
108                #[doc = "Checks each field in `optional` and if it's `Some`, it overwrites the corresponding `self` field."]
109                pub fn resolve(&mut self, colors_sheet: &$crate::theming::component_themes::ColorsSheet) -> [<$name Theme>] {
110                    use $crate::theming::macros::ResolvablePreference;
111                    [<$name Theme>] {
112                        $(
113                            $(
114                                $field_name: self.$field_name.resolve(colors_sheet),
115                            )*
116                        )?
117                    }
118                }
119            }
120
121            impl [<$name ThemePartial>] {
122                pub fn new() -> Self {
123                    Self::default()
124                }
125
126                $($(
127                    $(#[$field_attrs])*
128                    pub fn $field_name(mut self, $field_name: impl Into<$field_ty>) -> Self {
129                        self.$field_name = Some($crate::theming::macros::Preference::Specific($field_name.into()));
130                        self
131                    }
132                )*)?
133            }
134        }
135    };
136
137    (
138        $(#[$attrs:meta])*
139        $(for = $for_ty:ident ; theme_field = $theme_field:ident ;)+
140        $(%[component$($component_attr_control:tt)?])?
141        pub $name:ident {
142            $(
143                %[fields$($cows_attr_control:tt)?]
144                $(
145                    $(#[$field_attrs:meta])*
146                    $field_name:ident: $field_ty:ty,
147                )*
148            )?
149    }) => {
150        $crate::define_theme! {
151            @types
152            $(#[$attrs])*
153            $(%[component$($component_attr_control)?])?
154            pub $name {
155                $(
156                    %[fields$($cows_attr_control)?]
157                    $(
158                        $(#[$field_attrs])*
159                        $field_name: $field_ty,
160                    )*
161                )?
162            }
163        }
164        $crate::theming::macros::paste! {
165            pub trait [<$name ThemePartialExt>] {
166                $($(
167                    $(#[$field_attrs])*
168                    fn $field_name(self, $field_name: impl Into<$field_ty>) -> Self;
169                )*)?
170            }
171        }
172        $crate::define_theme! {
173            @ext_impls
174            [ $($for_ty)+ ]
175            [ $($theme_field)+ ]
176            $name ;
177            $($( $(#[$field_attrs])* $field_name : $field_ty , )*)?
178        }
179    };
180
181    // Skips the auto-generated `ThemePartialExt` trait.
182    (
183        $(#[$attrs:meta])*
184        %[no_ext]
185        $(%[component$($component_attr_control:tt)?])?
186        pub $name:ident {
187            $(
188                %[fields$($cows_attr_control:tt)?]
189                $(
190                    $(#[$field_attrs:meta])*
191                    $field_name:ident: $field_ty:ty,
192                )*
193            )?
194    }) => {
195        $crate::define_theme! {
196            @types
197            $(#[$attrs])*
198            $(%[component$($component_attr_control)?])?
199            pub $name {
200                $(
201                    %[fields$($cows_attr_control)?]
202                    $(
203                        $(#[$field_attrs])*
204                        $field_name: $field_ty,
205                    )*
206                )?
207            }
208        }
209    };
210
211    (
212        $(#[$attrs:meta])*
213        $(%[component$($component_attr_control:tt)?])?
214        pub $name:ident {
215            $(
216                %[fields$($cows_attr_control:tt)?]
217                $(
218                    $(#[$field_attrs:meta])*
219                    $field_name:ident: $field_ty:ty,
220                )*
221            )?
222    }) => {
223        $crate::define_theme! {
224            for = $name; theme_field = theme;
225            $(%[component$($component_attr_control)?])?
226            pub $name {
227                $(
228                    %[fields$($cows_attr_control)?]
229                    $(
230                        $(#[$field_attrs])*
231                        $field_name: $field_ty,
232                    )*
233                )?
234            }
235        }
236    };
237}
238
239/// Like [`get_theme!`](crate::get_theme), but falls back to a `default` callback
240/// instead of panicking when the key is not registered.
241#[macro_export]
242macro_rules! get_theme_or_default {
243    ($theme_prop:expr, $theme_type:ty, $theme_key:expr, $default:expr) => {{
244        let theme = $crate::theming::hooks::get_theme_or_default();
245        let theme = theme.read();
246        let mut requested_theme = theme
247            .get::<$theme_type>($theme_key)
248            .cloned()
249            .unwrap_or_else($default);
250
251        if let Some(theme_override) = $theme_prop {
252            requested_theme.apply_optional(&theme_override);
253        }
254
255        requested_theme.resolve(&theme.colors)
256    }};
257}
258
259#[macro_export]
260macro_rules! get_theme {
261    ($theme_prop:expr, $theme_type:ty, $theme_key:expr) => {
262        $crate::get_theme_or_default!($theme_prop, $theme_type, $theme_key, || {
263            ::core::panic!(concat!("Theme key not found: ", $theme_key))
264        })
265    };
266}
267
268#[derive(Clone, Debug, PartialEq, Eq)]
269pub enum Preference<T> {
270    Specific(T),
271    Reference(&'static str),
272}
273
274impl<T> From<T> for Preference<T> {
275    fn from(value: T) -> Self {
276        Preference::Specific(value)
277    }
278}
279
280pub trait ResolvablePreference<T: Clone> {
281    fn resolve(&self, colors_sheet: &ColorsSheet) -> T;
282}
283
284impl ResolvablePreference<Color> for Preference<Color> {
285    fn resolve(&self, colors_sheet: &ColorsSheet) -> Color {
286        match self {
287            Self::Reference(reference) => match *reference {
288                // Brand & Accent
289                "primary" => colors_sheet.primary,
290                "secondary" => colors_sheet.secondary,
291                "tertiary" => colors_sheet.tertiary,
292
293                // Status
294                "success" => colors_sheet.success,
295                "warning" => colors_sheet.warning,
296                "error" => colors_sheet.error,
297                "info" => colors_sheet.info,
298
299                // Surfaces
300                "background" => colors_sheet.background,
301                "surface_primary" => colors_sheet.surface_primary,
302                "surface_secondary" => colors_sheet.surface_secondary,
303                "surface_tertiary" => colors_sheet.surface_tertiary,
304                "surface_inverse" => colors_sheet.surface_inverse,
305                "surface_inverse_secondary" => colors_sheet.surface_inverse_secondary,
306                "surface_inverse_tertiary" => colors_sheet.surface_inverse_tertiary,
307
308                // Borders
309                "border" => colors_sheet.border,
310                "border_focus" => colors_sheet.border_focus,
311                "border_disabled" => colors_sheet.border_disabled,
312
313                // Text
314                "text_primary" => colors_sheet.text_primary,
315                "text_secondary" => colors_sheet.text_secondary,
316                "text_placeholder" => colors_sheet.text_placeholder,
317                "text_inverse" => colors_sheet.text_inverse,
318                "text_highlight" => colors_sheet.text_highlight,
319
320                // States
321                "focus" => colors_sheet.focus,
322                "active" => colors_sheet.active,
323                "disabled" => colors_sheet.disabled,
324
325                // Utility
326                "overlay" => colors_sheet.overlay,
327                "shadow" => colors_sheet.shadow,
328
329                // Fallback
330                _ => colors_sheet.primary,
331            },
332
333            Self::Specific(value) => *value,
334        }
335    }
336}
337
338impl ResolvablePreference<Size> for Preference<Size> {
339    fn resolve(&self, _colors_sheet: &ColorsSheet) -> Size {
340        match self {
341            Self::Reference(_) => {
342                panic!("Only Colors support references.")
343            }
344            Self::Specific(value) => value.clone(),
345        }
346    }
347}
348
349impl ResolvablePreference<Gaps> for Preference<Gaps> {
350    fn resolve(&self, _colors_sheet: &ColorsSheet) -> Gaps {
351        match self {
352            Self::Reference(_) => {
353                panic!("Only Colors support references.")
354            }
355            Self::Specific(value) => *value,
356        }
357    }
358}
359
360impl ResolvablePreference<CornerRadius> for Preference<CornerRadius> {
361    fn resolve(&self, _colors_sheet: &ColorsSheet) -> CornerRadius {
362        match self {
363            Self::Reference(_) => {
364                panic!("Only Colors support references.")
365            }
366            Self::Specific(value) => *value,
367        }
368    }
369}
370
371impl ResolvablePreference<f32> for Preference<f32> {
372    fn resolve(&self, _colors_sheet: &ColorsSheet) -> f32 {
373        match self {
374            Self::Reference(_) => {
375                panic!("Only Colors support references.")
376            }
377            Self::Specific(value) => *value,
378        }
379    }
380}
381
382impl ResolvablePreference<Duration> for Preference<Duration> {
383    fn resolve(&self, _colors_sheet: &ColorsSheet) -> Duration {
384        match self {
385            Self::Reference(_) => {
386                panic!("Only Colors support references.")
387            }
388            Self::Specific(value) => *value,
389        }
390    }
391}