textiler_core/theme/
sx.rs1use 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#[derive(Debug, Default, PartialEq, Clone)]
31pub struct Sx {
32 props: IndexMap<String, SxValue>,
33}
34
35pub type Css = Sheet;
36
37impl Sx {
38 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 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 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#[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 (@object $object:ident () () ()) => {
113 };
114
115 (@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 (@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 (@object $object:ident ($($key:tt)+) (: {$($map:tt)*} $($rest:tt)*) $copy:tt) => {
131 sx_internal!(@object $object [$($key)+] (sx_internal!({$($map)*})) $($rest)*);
132 };
133
134 (@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 (@object $object:ident ($($key:tt)+) (: |$theme:ident| $func:expr) $copy:tt) => {
141 sx_internal!(@object $object [$($key)+] (sx_internal!(|$theme| $func)));
142 };
143
144 (@object $object:ident ($($key:tt)+) (: $value:expr , $($rest:tt)*) $copy:tt) => {
146 sx_internal!(@object $object [$($key)+] (sx_internal!($value)) , $($rest)*);
147 };
148
149 (@object $object:ident ($($key:tt)+) (: $value:expr) $copy:tt) => {
151 sx_internal!(@object $object [$($key)+] ( sx_internal!($value) ) );
152 };
153
154 (@object $object:ident [$key:ident] ($value:expr)) => {
156 let _ = $object.insert((stringify!($key)).trim(), sx_internal!($value));
157 };
158
159 (@object $object:ident [$($key:tt)+] ($value:expr)) => {
161 let _ = $object.insert(($($key)+), sx_internal!($value));
162 };
163
164
165 (@object $object:ident () (($key:expr) : $($rest:tt)*) $copy:tt) => {
168 sx_internal!(@object $object ($key) (: $($rest)*) (: $($rest)*));
169 };
170
171 (@object $object:ident ($($key:tt)*) (: $($unexpected:tt)+) $copy:tt) => {
173 compile_error!("unexpected colon")
174 };
175
176 (@object $object:ident ($($key:tt)*) ($tt:tt $($rest:tt)*) $copy:tt) => {
178 sx_internal!(@object $object ($($key)* $tt) ($($rest)*) ($($rest)*));
179 };
180
181
182 ({}) => {
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#[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}