textiler_core/theme/
sx.rs

1//! Contains the definition of the `Sx` type and the `sx!` macro
2//!
3//!
4use cssparser::ToCss;
5use gloo::history::query::FromQuery;
6use heck::{ToKebabCase, ToLowerCamelCase, ToTrainCase};
7use indexmap::map::Entry;
8use indexmap::IndexMap;
9use serde::Deserialize;
10use std::fmt::Debug;
11use std::ops::Index;
12use std::str::FromStr;
13use stylist::ast::{Sheet, ToStyleStr};
14use stylist::Style;
15use yew::Classes;
16
17pub use crate::theme::sx;
18use crate::theme::sx::sx_to_css::sx_to_css;
19use crate::theme::theme_mode::ThemeMode;
20use crate::theme::Theme;
21
22mod sx_to_css;
23mod sx_value;
24mod sx_value_parsing;
25use crate::system_props::{CssPropertyTranslator, SYSTEM_PROPERTIES};
26use crate::utils::to_property;
27pub use sx_value::*;
28
29/// Contains CSS definition with some customization
30#[derive(Debug, Default, PartialEq, Clone)]
31pub struct Sx {
32    props: IndexMap<String, SxValue>,
33}
34
35pub type Css = Sheet;
36
37impl Sx {
38    /// Sets a css property
39    pub fn insert<K: AsRef<str>, V: Into<SxValue>>(&mut self, key: K, value: V) {
40        let translated = SYSTEM_PROPERTIES.translate(key.as_ref());
41        let value = value.into();
42        for translated in translated {
43            self.props.insert(to_property(translated), value.clone());
44        }
45    }
46
47    /// Merges this Sx with another Sx. Uses the left's values for conflicting keys.
48    pub fn merge(self, other: Self) -> Self {
49        let mut sx = self;
50
51        for (prop, value) in other.props {
52            match sx.props.entry(prop) {
53                Entry::Occupied(mut occ) => match occ.get_mut() {
54                    SxValue::Nested(old_sx) => {
55                        if let SxValue::Nested(sx) = value {
56                            *old_sx = old_sx.clone().merge(sx);
57                        }
58                    }
59                    _ => {}
60                },
61                Entry::Vacant(v) => {
62                    v.insert(value);
63                }
64            }
65        }
66
67        sx
68    }
69
70    pub fn to_css(self, mode: &ThemeMode, theme: &Theme) -> Css {
71        let css = sx_to_css(self, mode, theme, None).expect("invalid sx");
72        Sheet::from_str(&css).unwrap()
73    }
74
75    /// Gets the properties set in this sx
76    pub fn properties(&self) -> impl IntoIterator<Item = &str> {
77        self.props.keys().map(|s| s.as_ref())
78    }
79}
80
81impl Index<&str> for Sx {
82    type Output = SxValue;
83
84    fn index(&self, index: &str) -> &Self::Output {
85        &self.props[index]
86    }
87}
88
89impl From<SxRef> for Classes {
90    fn from(value: SxRef) -> Self {
91        Classes::from(value.style)
92    }
93}
94
95/// Creates [`Sx`][Sx] instances
96#[macro_export]
97macro_rules! sx {
98    (
99        $($json:tt)*
100    ) => {
101        $crate::sx_internal!({ $($json)* })
102    }
103}
104
105#[macro_export]
106#[doc(hidden)]
107macro_rules! sx_internal {
108
109    // TT parser for objects
110
111    // done
112    (@object $object:ident () () ()) => {
113    };
114
115    // Insert the current entry followed by trailing comma.
116    (@object $object:ident [$key:ident] ($value:expr) , $($rest:tt)*) => {
117        let _ = $object.insert((stringify!($key)).trim(), sx_internal!($value));
118        sx_internal!(@object $object () ($($rest)*) ($($rest)*));
119    };
120
121    // Insert the current entry followed by trailing comma.
122    (@object $object:ident [$($key:tt)+] ($value:expr) , $($rest:tt)*) => {
123        let _ = $object.insert(($($key)+), $value);
124        sx_internal!(@object $object () ($($rest)*) ($($rest)*));
125    };
126
127
128
129     // Next value is a map.
130    (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
131       sx_internal!(@object $object [$($key)+] (sx_internal!({$($map)*})) $($rest)*);
132    };
133
134     // Next value is a callback
135    (@object $object:ident ($($key:tt)+) (: |$theme:ident| $func:expr , $($rest:tt)*) $copy:tt) => {
136       sx_internal!(@object $object [$($key)+] (sx_internal!(|$theme| $func)) $($rest)*);
137    };
138
139    // Next value is a callback with no rest
140    (@object $object:ident ($($key:tt)+) (: |$theme:ident| $func:expr) $copy:tt) => {
141       sx_internal!(@object $object [$($key)+] (sx_internal!(|$theme| $func)));
142    };
143
144    // Next value is an expression followed by comma.
145    (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
146        sx_internal!(@object $object [$($key)+] (sx_internal!($value)) , $($rest)*);
147    };
148
149    // Last value is an expression with no trailing comma.
150    (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
151        sx_internal!(@object $object [$($key)+] ( sx_internal!($value) ) );
152    };
153
154     // Insert the last entry without trailing comma.
155    (@object $object:ident [$key:ident] ($value:expr)) => {
156        let _ = $object.insert((stringify!($key)).trim(), sx_internal!($value));
157    };
158
159     // Insert the last entry without trailing comma.
160    (@object $object:ident [$($key:tt)+] ($value:expr)) => {
161        let _ = $object.insert(($($key)+), sx_internal!($value));
162    };
163
164
165    // Key is fully parenthesized. This avoids clippy double_parens false
166    // positives because the parenthesization may be necessary here.
167    (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
168        sx_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
169    };
170
171    // Refuse to absorb colon token into key expression.
172    (@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => {
173        compile_error!("unexpected colon")
174    };
175
176    // Munch a token into the current key.
177    (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
178        sx_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
179    };
180
181
182    // main implementation
183    ({}) => {
184        crate::theme::sx::Sx::default()
185    };
186
187    ({ $($tt:tt)+ }) => {
188        {
189            use $crate::theme::sx::*;
190            use $crate::{sx, sx_internal};
191
192            let mut sx: Sx = Sx::default();
193            sx_internal!(@object sx () ($($tt)+) ($($tt)+));
194            sx
195        }
196    };
197
198    (|$theme:ident| $expr:expr) => {
199        SxValue::Callback(FnSxValue::new(|$theme| $expr))
200    };
201
202    ($expr:expr) => {
203        SxValue::try_from($expr).expect("could not create sxvalue")
204    };
205
206
207}
208
209/// A style ref can be used as a css class
210#[derive(Debug, Clone)]
211pub struct SxRef {
212    style: Style,
213}
214
215impl SxRef {
216    pub(crate) fn new(style: Style) -> Self {
217        Self { style }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn create_sx_with_macro() {
227        let sx = sx! {
228            width: "123.5%",
229            p: "background.body",
230        };
231        assert_eq!(
232            sx["p"],
233            SxValue::ThemeToken {
234                palette: "background".to_string(),
235                selector: "body".to_string()
236            }
237        )
238    }
239
240    #[test]
241    fn merge_sx() {
242        let base = sx! {
243            "bgcolor": "background.level1",
244        };
245        let merged = base.clone().merge(sx! {
246            "bgcolor": SxValue::var("sheet", "background-color", None)
247        });
248
249        assert_eq!(
250            &base["bgcolor"],
251            &SxValue::ThemeToken {
252                palette: "background".to_string(),
253                selector: "level1".to_string(),
254            }
255        );
256    }
257
258    #[test]
259    fn to_css() {
260        let theme = Theme::default();
261
262        let sx = sx! {
263            padding: "15px",
264            color: "background.body"
265        };
266
267        let style = sx.to_css(&ThemeMode::default(), &theme);
268        println!("style: {style:#?}");
269    }
270
271    #[test]
272    fn breakpoints_create_media_queries() {
273        let theme = Theme::new();
274
275        let sx = sx! {
276            padding: "15px",
277            md: {
278                padding: "20px"
279            }
280        };
281
282        let style = sx.to_css(&ThemeMode::default(), &theme);
283        println!("style: {style:#?}");
284    }
285
286    #[test]
287    fn sub_class() {
288        let theme = Theme::new();
289
290        let sx = sx! {
291            ".box": {
292                "p": "10px"
293            }
294        };
295
296        let style = sx.to_css(&ThemeMode::default(), &theme);
297        println!("style: {style:#?}");
298    }
299}