Skip to main content

euv_core/vdom/attribute/
impl.rs

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