euv_core/vdom/attribute/impl.rs
1use crate::*;
2
3/// Visual equality comparison for attribute values.
4///
5/// Compares values by their visual output rather than identity. `Signal`
6/// values are compared by their current resolved string, `Event` values
7/// are always considered equal (re-binding is handled by the handler
8/// registry), and `CssClass` values are compared by class name.
9impl PartialEq for AttributeValue {
10 /// Compares two attribute values for visual equality.
11 ///
12 /// # Arguments
13 ///
14 /// - `&Self` - The first attribute value.
15 /// - `&Self` - The second attribute value.
16 ///
17 /// # Returns
18 ///
19 /// - `bool` - `true` if the values are visually equal.
20 fn eq(&self, other: &Self) -> bool {
21 match (self, other) {
22 (AttributeValue::Text(old_val), AttributeValue::Text(new_val)) => old_val == new_val,
23 (AttributeValue::Signal(old_sig), AttributeValue::Signal(new_sig)) => {
24 old_sig.get() == new_sig.get()
25 }
26 (AttributeValue::Signal(old_sig), AttributeValue::Text(new_val)) => {
27 old_sig.get() == *new_val
28 }
29 (AttributeValue::Text(old_val), AttributeValue::Signal(new_sig)) => {
30 *old_val == new_sig.get()
31 }
32 (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
33 (AttributeValue::Css(old_css), AttributeValue::Css(new_css)) => {
34 old_css.get_name() == new_css.get_name()
35 }
36 (AttributeValue::Dynamic(old_dyn), AttributeValue::Dynamic(new_dyn)) => {
37 old_dyn == new_dyn
38 }
39 _ => false,
40 }
41 }
42}
43
44/// Visual equality comparison for attribute entries.
45///
46/// Two attribute entries are equal when their names match and their values
47/// are visually equal as defined by `AttributeValue::eq`.
48impl PartialEq for AttributeEntry {
49 /// Compares two attribute entries for visual equality.
50 ///
51 /// # Arguments
52 ///
53 /// - `&Self` - The first attribute entry.
54 /// - `&Self` - The second attribute entry.
55 ///
56 /// # Returns
57 ///
58 /// - `bool` - `true` if both names and values match.
59 fn eq(&self, other: &Self) -> bool {
60 self.get_name() == other.get_name() && self.get_value() == other.get_value()
61 }
62}
63
64/// Visual equality comparison for CSS classes.
65///
66/// Two CSS classes are considered equal when their class names match,
67/// since the name uniquely identifies the visual style rule.
68impl PartialEq for CssClass {
69 /// Compares two CSS classes by name.
70 ///
71 /// # Arguments
72 ///
73 /// - `&Self` - The first CSS class.
74 /// - `&Self` - The second CSS class.
75 ///
76 /// # Returns
77 ///
78 /// - `bool` - `true` if the class names match.
79 fn eq(&self, other: &Self) -> bool {
80 self.get_name() == other.get_name()
81 }
82}
83
84/// Implementation of style CSS serialization.
85impl Style {
86 /// Adds a style property.
87 ///
88 /// Property names are automatically converted from snake_case to kebab-case
89 /// (e.g., `flex_direction` becomes `flex-direction`).
90 ///
91 /// # Arguments
92 ///
93 /// - `N` - The property name (snake_case will be converted to kebab-case).
94 /// - `V` - The property value.
95 ///
96 /// # Returns
97 ///
98 /// - `Self` - This style with the property added.
99 pub fn property<N, V>(mut self, name: N, value: V) -> Self
100 where
101 N: AsRef<str>,
102 V: AsRef<str>,
103 {
104 self.get_mut_properties().push(StyleProperty::new(
105 name.as_ref().replace('_', "-"),
106 value.as_ref().to_string(),
107 ));
108 self
109 }
110
111 /// Converts the style to a CSS string.
112 ///
113 /// # Returns
114 ///
115 /// - `String` - The CSS string representation.
116 pub fn to_css_string(&self) -> String {
117 self.get_properties()
118 .iter()
119 .map(|style: &StyleProperty| format!("{}: {};", style.get_name(), style.get_value()))
120 .collect::<Vec<String>>()
121 .join(" ")
122 }
123
124 /// Builds a CSS style string from an array of key-value pairs.
125 ///
126 /// This function is used by the `html!` macro to convert static `style:`
127 /// attributes into a CSS string without allocating intermediate `Style`
128 /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
129 /// to kebab-case automatically.
130 ///
131 /// # Arguments
132 ///
133 /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
134 ///
135 /// # Returns
136 ///
137 /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
138 pub fn create_style_string(props: &[(&str, &str)]) -> String {
139 let mut result: String = String::new();
140 for (key, value) in props {
141 if !result.is_empty() {
142 result.push(' ');
143 }
144 result.push_str(&key.replace('_', "-"));
145 result.push_str(": ");
146 result.push_str(value);
147 result.push(';');
148 }
149 result
150 }
151}
152
153/// Provides a default empty style.
154impl Default for Style {
155 /// Returns a default `Style` with no properties.
156 ///
157 /// # Returns
158 ///
159 /// - `Self` - An empty style.
160 fn default() -> Self {
161 Self::new(Vec::new())
162 }
163}
164
165/// Implementation of CssClass construction and style injection.
166impl CssClass {
167 /// Creates a new CSS class with the given name and style declarations.
168 ///
169 /// Automatically injects the styles into the DOM upon creation.
170 ///
171 /// # Arguments
172 ///
173 /// - `String` - The class name.
174 /// - `String` - The CSS style declarations.
175 ///
176 /// # Returns
177 ///
178 /// - `Self` - A new CSS class with injected styles.
179 pub fn new(name: String, style: String) -> Self {
180 let mut css_class: CssClass = CssClass::default();
181 css_class.set_name(name);
182 css_class.set_style(style);
183 css_class.inject_style();
184 css_class
185 }
186
187 /// Creates a new CSS class with the given name, style declarations, and pseudo rules.
188 ///
189 /// Automatically injects the base styles, pseudo-class/pseudo-element rules,
190 /// and media query rules into the DOM upon creation.
191 ///
192 /// # Arguments
193 ///
194 /// - `String` - The class name.
195 /// - `String` - The CSS style declarations.
196 /// - `Vec<PseudoRule>` - The pseudo-class and pseudo-element rules.
197 /// - `Vec<MediaRule>` - The media query rules.
198 ///
199 /// # Returns
200 ///
201 /// - `Self` - A new CSS class with injected styles and pseudo rules.
202 pub fn new_with_rules(
203 name: String,
204 style: String,
205 pseudo_rules: Vec<PseudoRule>,
206 media_rules: Vec<MediaRule>,
207 ) -> Self {
208 let mut css_class: CssClass = CssClass::default();
209 css_class.set_name(name);
210 css_class.set_style(style);
211 css_class.set_pseudo_rules(pseudo_rules);
212 css_class.set_media_rules(media_rules);
213 css_class.inject_style();
214 css_class
215 }
216
217 /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
218 ///
219 /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
220 /// This is used by the `class!` macro for fully static class definitions
221 /// where pseudo rules can be computed at compile time.
222 ///
223 /// # Arguments
224 ///
225 /// - `&str` - The serialized pseudo rules string.
226 ///
227 /// # Returns
228 ///
229 /// - `Vec<PseudoRule>` - The parsed pseudo rules.
230 pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
231 let mut rules: Vec<PseudoRule> = Vec::new();
232 let mut remaining: &str = input;
233 while !remaining.is_empty() {
234 let selector_end: Option<usize> = remaining.find(" { ");
235 let Some(sel_end) = selector_end else {
236 break;
237 };
238 let selector: &str = &remaining[..sel_end];
239 let after_selector: &str = remaining[sel_end..].strip_prefix(" { ").unwrap_or("");
240 let style_end: Option<usize> = after_selector.find('}');
241 let Some(st_end) = style_end else {
242 break;
243 };
244 let style: &str = &after_selector[..st_end];
245 if !selector.is_empty() && !style.is_empty() {
246 rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
247 }
248 remaining = after_selector[st_end..].strip_prefix('}').unwrap_or("");
249 }
250 rules
251 }
252
253 /// Parses media query rules from a compact serialization string.
254 ///
255 /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
256 /// This is used by the `class!` macro for fully static class definitions
257 /// where media rules can be computed at compile time.
258 ///
259 /// # Arguments
260 ///
261 /// - `&str` - The serialized media rules string.
262 ///
263 /// # Returns
264 ///
265 /// - `Vec<MediaRule>` - The parsed media rules.
266 pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
267 let mut rules: Vec<MediaRule> = Vec::new();
268 let mut remaining: &str = input;
269 while !remaining.is_empty() {
270 if !remaining.starts_with("@media ") {
271 break;
272 }
273 let after_prefix: &str = remaining.strip_prefix("@media ").unwrap_or("");
274 let query_end: Option<usize> = after_prefix.find(" { ");
275 let Some(q_end) = query_end else {
276 break;
277 };
278 let query: &str = &after_prefix[..q_end];
279 let after_query: &str = after_prefix[q_end..].strip_prefix(" { ").unwrap_or("");
280 let style_end: Option<usize> = after_query.find('}');
281 let Some(st_end) = style_end else {
282 break;
283 };
284 let style: &str = &after_query[..st_end];
285 if !query.is_empty() && !style.is_empty() {
286 rules.push(MediaRule::new(query.to_string(), style.to_string()));
287 }
288 remaining = after_query[st_end..].strip_prefix('}').unwrap_or("");
289 }
290 rules
291 }
292
293 /// Injects this class's styles into the DOM if not already present.
294 ///
295 /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
296 /// then delegates to [`CssClass::inject_css`] for DOM injection. Subsequent
297 /// calls for the same class name are no-ops.
298 ///
299 /// # Panics
300 ///
301 /// Panics if `window()` or `document()` is unavailable on the current platform.
302 pub fn inject_style(&self) {
303 let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
304 let mut css: String = class_rule;
305 for pseudo_rule in self.get_pseudo_rules() {
306 if !pseudo_rule.get_style().is_empty() {
307 let pseudo_rule_str: String = format!(
308 ".{}{} {{ {} }}",
309 self.get_name(),
310 pseudo_rule.get_selector(),
311 pseudo_rule.get_style()
312 );
313 css = format!("{}\n{}", css, pseudo_rule_str);
314 }
315 }
316 for media_rule in self.get_media_rules() {
317 if !media_rule.get_query().is_empty() {
318 let media_rule_str: String = format!(
319 "@media {} {{ .{} {{ {} }} }}",
320 media_rule.get_query(),
321 self.get_name(),
322 media_rule.get_style()
323 );
324 css = format!("{}\n{}", css, media_rule_str);
325 }
326 }
327 Self::inject_css(&css);
328 }
329
330 /// Injects CSS text into the shared `<style>` element in the DOM.
331 ///
332 /// Creates a `<style>` element with id `euv-css-injected` on first call,
333 /// then appends the provided `css` string if it is not already present.
334 /// Subsequent calls with identical CSS text are no-ops. Call this during
335 /// application initialisation to register global reset styles, keyframes,
336 /// media queries, or any other CSS rules.
337 ///
338 /// # Arguments
339 ///
340 /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
341 ///
342 /// # Panics
343 ///
344 /// Panics if `window()` or `document()` is unavailable on the current platform.
345 pub fn inject_css(css: &str) {
346 let _ = css;
347 #[cfg(target_arch = "wasm32")]
348 {
349 let style_id: &str = "euv-css-injected";
350 let document: Document = window()
351 .expect("no global window exists")
352 .document()
353 .expect("no document exists");
354 let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
355 Some(el) => el.dyn_into::<HtmlStyleElement>().unwrap(),
356 None => {
357 let el: HtmlStyleElement = document
358 .create_element("style")
359 .unwrap()
360 .dyn_into::<HtmlStyleElement>()
361 .unwrap();
362 el.set_id(style_id);
363 document.head().unwrap().append_child(&el).unwrap();
364 el
365 }
366 };
367 let existing_css: String = style_element.inner_text();
368 if !css.is_empty() && !existing_css.contains(css) {
369 let new_css: String = if existing_css.is_empty() {
370 css.to_string()
371 } else {
372 format!("{}\n{}", existing_css, css)
373 };
374 style_element.set_inner_text(&new_css);
375 }
376 }
377 }
378}
379
380/// Displays the CSS class name.
381///
382/// This enables `format!("{}", css_class)` to produce the class name string,
383/// which is required for reactive `if` conditions in `class:` attributes.
384impl std::fmt::Display for CssClass {
385 /// Formats the CSS class as its name string.
386 ///
387 /// # Arguments
388 ///
389 /// - `&mut Formatter` - The formatter.
390 ///
391 /// # Returns
392 ///
393 /// - `std::fmt::Result` - The formatting result.
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 write!(f, "{}", self.get_name())
396 }
397}