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/// Implementation of attribute value factory methods for reactive and merged values.
7impl AttributeValue {
8    /// Creates a reactive attribute `Self` for conditional attribute values.
9    ///
10    /// This function replaces the inline `Signal::create(...)` + `subscribe_attr_signal(...)`
11    /// boilerplate that was previously generated by the `html!` macro for every
12    /// attribute value containing an `if` condition.
13    ///
14    /// # Arguments
15    ///
16    /// - `Fn() -> String + 'static` - A closure that computes the current attribute value.
17    ///   Called on initial render and whenever any signal changes.
18    ///
19    /// # Returns
20    ///
21    /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
22    ///   that reactively re-evaluates the attribute value on signal updates.
23    pub fn create_reactive_signal<F>(compute: F) -> Self
24    where
25        F: Fn() -> String + 'static,
26    {
27        let attr_signal: Signal<String> =
28            Signal::create(IntoReactiveString::into_reactive_string(compute()));
29        subscribe_attr_signal(attr_signal, move || {
30            IntoReactiveString::into_reactive_string(compute())
31        });
32        Self::Signal(attr_signal)
33    }
34
35    /// Merges multiple class attribute values into a single `Self`.
36    ///
37    /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
38    /// `Css` values are injected into the DOM and their names are collected.
39    /// All non-empty class names are joined with spaces into a final `Text` attribute.
40    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute
41    /// that re-evaluates when any constituent signal changes.
42    ///
43    /// # Arguments
44    ///
45    /// - `&[Self]` - The class attribute values to merge.
46    ///
47    /// # Returns
48    ///
49    /// - `Self` - A merged attribute value containing space-separated class names.
50    pub fn merge_class(values: &[Self]) -> Self {
51        let has_signal: bool = values
52            .iter()
53            .any(|value: &Self| matches!(value, Self::Signal(_)));
54        if has_signal {
55            let owned_values: Vec<Self> = values.to_vec();
56            let compute = move || {
57                owned_values
58                    .iter()
59                    .filter_map(|value: &Self| match value {
60                        Self::Css(css) => {
61                            css.inject_style();
62                            Some(css.get_name().to_string())
63                        }
64                        Self::Text(text_value) => Some(text_value.clone()),
65                        Self::Signal(signal) => Some(signal.get()),
66                        _ => None,
67                    })
68                    .filter(|segment: &String| !segment.is_empty())
69                    .collect::<Vec<String>>()
70                    .join(&CHAR_SPACE.to_string())
71            };
72            let attr_signal: Signal<String> = Signal::create(compute());
73            subscribe_attr_signal(attr_signal, compute);
74            Self::Signal(attr_signal)
75        } else {
76            let result: String = values
77                .iter()
78                .filter_map(|value: &Self| match value {
79                    Self::Css(css) => {
80                        css.inject_style();
81                        Some(css.get_name().to_string())
82                    }
83                    Self::Text(text_value) => Some(text_value.clone()),
84                    _ => None,
85                })
86                .filter(|segment: &String| !segment.is_empty())
87                .collect::<Vec<String>>()
88                .join(&CHAR_SPACE.to_string());
89            Self::Text(result)
90        }
91    }
92
93    /// Merges multiple style attribute values into a single `Self`.
94    ///
95    /// Each input value is expected to be a style string (`Text`) or a reactive
96    /// `Signal<String>` producing a style string. All non-empty style strings are
97    /// joined with spaces into a final combined style attribute.
98    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute.
99    ///
100    /// # Arguments
101    ///
102    /// - `&[Self]` - The style attribute values to merge.
103    ///
104    /// # Returns
105    ///
106    /// - `Self` - A merged attribute value containing the combined CSS style string.
107    pub fn merge_style(values: &[Self]) -> Self {
108        let has_signal: bool = values
109            .iter()
110            .any(|value: &Self| matches!(value, Self::Signal(_)));
111        if has_signal {
112            let owned_values: Vec<Self> = values.to_vec();
113            let compute = move || {
114                owned_values
115                    .iter()
116                    .filter_map(|value: &Self| match value {
117                        Self::Text(text_value) => Some(text_value.clone()),
118                        Self::Signal(signal) => Some(signal.get()),
119                        _ => None,
120                    })
121                    .filter(|segment: &String| !segment.is_empty())
122                    .collect::<Vec<String>>()
123                    .join(&CHAR_SPACE.to_string())
124            };
125            let attr_signal: Signal<String> = Signal::create(compute());
126            subscribe_attr_signal(attr_signal, compute);
127            Self::Signal(attr_signal)
128        } else {
129            let result: String = values
130                .iter()
131                .filter_map(|value: &Self| match value {
132                    Self::Text(text_value) => Some(text_value.clone()),
133                    _ => None,
134                })
135                .filter(|segment: &String| !segment.is_empty())
136                .collect::<Vec<String>>()
137                .join(&CHAR_SPACE.to_string());
138            Self::Text(result)
139        }
140    }
141}
142
143/// Visual equality comparison for attribute values.
144///
145/// Compares values by their visual output rather than identity. `Signal`
146/// values are compared by their current resolved string; when both signals
147/// share the same inner pointer, they are always considered **unequal**
148/// because the signal may have mutated between VDOM snapshots and `.get()`
149/// would return the same current value for both, masking the change.
150/// `Event` values are always considered equal (re-binding is handled by the
151/// handler registry), and `Css` values are compared by class name.
152impl PartialEq for AttributeValue {
153    /// Compares two attribute values for visual equality.
154    ///
155    /// # Arguments
156    ///
157    /// - `&Self` - The first attribute value.
158    /// - `&Self` - The second attribute value.
159    ///
160    /// # Returns
161    ///
162    /// - `bool` - `true` if the values are visually equal.
163    fn eq(&self, other: &Self) -> bool {
164        match (self, other) {
165            (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
166            (Self::Signal(old_signal), Self::Signal(new_signal)) => {
167                if old_signal.get_inner() == new_signal.get_inner() {
168                    return false;
169                }
170                old_signal.get() == new_signal.get()
171            }
172            (Self::Signal(old_signal), Self::Text(new_value)) => old_signal.get() == *new_value,
173            (Self::Text(old_value), Self::Signal(new_signal)) => *old_value == new_signal.get(),
174            (Self::Event(_), Self::Event(_)) => true,
175            (Self::Css(old_class), Self::Css(new_class)) => {
176                old_class.get_name() == new_class.get_name()
177            }
178            (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
179            _ => false,
180        }
181    }
182}
183
184/// Visual equality comparison for attribute entries.
185///
186/// Two attribute entries are equal when their names match and their values
187/// are visually equal as defined by `AttributeValue::eq`.
188impl PartialEq for AttributeEntry {
189    /// Compares two attribute entries for visual equality.
190    ///
191    /// # Arguments
192    ///
193    /// - `&Self` - The first attribute entry.
194    /// - `&Self` - The second attribute entry.
195    ///
196    /// # Returns
197    ///
198    /// - `bool` - `true` if both names and values match.
199    fn eq(&self, other: &Self) -> bool {
200        self.get_name() == other.get_name() && self.get_value() == other.get_value()
201    }
202}
203
204/// Visual equality comparison for CSS classes.
205///
206/// Two CSS classes are considered equal when their class names match,
207/// since the name uniquely identifies the visual style rule.
208impl PartialEq for Css {
209    /// Compares two CSS classes by name.
210    ///
211    /// # Arguments
212    ///
213    /// - `&Self` - The first CSS class.
214    /// - `&Self` - The second CSS class.
215    ///
216    /// # Returns
217    ///
218    /// - `bool` - `true` if the class names match.
219    fn eq(&self, other: &Self) -> bool {
220        self.get_name() == other.get_name()
221    }
222}
223
224/// Implementation of Css construction and style injection.
225impl Css {
226    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
227    ///
228    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
229    /// This is used by the `class!` macro for fully static class definitions
230    /// where pseudo rules can be computed at compile time.
231    ///
232    /// # Arguments
233    ///
234    /// - `&str` - The serialized pseudo rules string.
235    ///
236    /// # Returns
237    ///
238    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
239    pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
240        let mut rules: Vec<PseudoRule> = Vec::new();
241        let mut remaining: &str = input;
242        while !remaining.is_empty() {
243            let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
244            let Some(selector_end_index) = selector_end else {
245                break;
246            };
247            let selector: &str = &remaining[..selector_end_index];
248            let after_selector: &str = remaining[selector_end_index..]
249                .strip_prefix(CSS_RULE_OPEN)
250                .unwrap_or_default();
251            let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
252            let Some(style_end_index) = style_end else {
253                break;
254            };
255            let style: &str = &after_selector[..style_end_index];
256            if !selector.is_empty() && !style.is_empty() {
257                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
258            }
259            remaining = after_selector[style_end_index..]
260                .strip_prefix(CHAR_CSS_RULE_CLOSE)
261                .unwrap_or_default();
262        }
263        rules
264    }
265
266    /// Parses media query rules from a compact serialization string.
267    ///
268    /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
269    /// This is used by the `class!` macro for fully static class definitions
270    /// where media rules can be computed at compile time.
271    ///
272    /// # Arguments
273    ///
274    /// - `&str` - The serialized media rules string.
275    ///
276    /// # Returns
277    ///
278    /// - `Vec<MediaRule>` - The parsed media rules.
279    pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
280        let mut rules: Vec<MediaRule> = Vec::new();
281        let mut remaining: &str = input;
282        while !remaining.is_empty() {
283            if !remaining.starts_with(CSS_MEDIA_PREFIX) {
284                break;
285            }
286            let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
287            let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
288            let Some(query_end_index) = query_end else {
289                break;
290            };
291            let query: &str = &after_prefix[..query_end_index];
292            let after_query: &str = after_prefix[query_end_index..]
293                .strip_prefix(CSS_RULE_OPEN)
294                .unwrap_or_default();
295            let style_end: Option<usize> = after_query.find(CHAR_CSS_RULE_CLOSE);
296            let Some(style_end_index) = style_end else {
297                break;
298            };
299            let style: &str = &after_query[..style_end_index];
300            if !query.is_empty() && !style.is_empty() {
301                rules.push(MediaRule::new(query.to_string(), style.to_string()));
302            }
303            remaining = after_query[style_end_index..]
304                .strip_prefix(CHAR_CSS_RULE_CLOSE)
305                .unwrap_or_default();
306        }
307        rules
308    }
309
310    /// Injects this class's styles into the DOM if not already present.
311    ///
312    /// Uses a global `HashSet` to track injected class names, avoiding the
313    /// expensive `existing_css.contains(css)` full-text search on every call.
314    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
315    /// then appends them directly to the `<style>` element via
316    /// `append_child` with a new text node — no read-modify-write of the
317    /// entire stylesheet content.
318    ///
319    /// # Panics
320    ///
321    /// Panics if `window()` or `document()` is unavailable on the current platform.
322    pub fn inject_style(&self) {
323        if !Self::mark_injected(self.get_name().clone()) {
324            return;
325        }
326        let mut css_text: String = format!(
327            "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
328            self.get_name(),
329            self.get_style()
330        );
331        for pseudo_rule in self.get_pseudo_rules() {
332            if !pseudo_rule.get_style().is_empty() {
333                css_text = format!(
334                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
335                    self.get_name(),
336                    pseudo_rule.get_selector(),
337                    pseudo_rule.get_style()
338                );
339            }
340        }
341        for media_rule in self.get_media_rules() {
342            if !media_rule.get_query().is_empty() {
343                css_text = format!(
344                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CSS_MEDIA_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}{CSS_RULE_CLOSE_FORMAT}",
345                    media_rule.get_query(),
346                    self.get_name(),
347                    media_rule.get_style()
348                );
349            }
350        }
351        Self::append_css(&css_text);
352    }
353
354    /// Marks a class name as injected in the global `HashSet`.
355    ///
356    /// Returns `false` if the class was already injected (no-op), `true`
357    /// if this is the first injection.
358    ///
359    /// # Arguments
360    ///
361    /// - `String` - The class name to mark as injected.
362    ///
363    /// # Returns
364    ///
365    /// - `bool` - `true` if newly injected, `false` if already present.
366    fn mark_injected(class_name: String) -> bool {
367        get_injected_classes_mut().insert(class_name)
368    }
369
370    /// Appends CSS text directly to the shared `<style>` element.
371    ///
372    /// Creates a new text node and appends it as a child of the `<style>`
373    /// element, avoiding the read-modify-write pattern of reading the entire
374    /// `innerText`, concatenating, and setting it back.
375    ///
376    /// # Arguments
377    ///
378    /// - `&str` - The CSS text to append.
379    ///
380    fn append_css(css_text: &str) {
381        let style_id: &str = EUV_CSS_INJECTED_ID;
382        let window_value: Window = match window() {
383            Some(window_instance) => window_instance,
384            None => return,
385        };
386        let document: Document = match window_value.document() {
387            Some(document_instance) => document_instance,
388            None => return,
389        };
390        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
391            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
392                Ok(element) => element,
393                Err(_err) => return,
394            },
395            None => {
396                let created: Element = match document.create_element(STYLE_TAG) {
397                    Ok(element) => element,
398                    Err(_err) => return,
399                };
400                let style_element_from_id: HtmlStyleElement =
401                    match created.dyn_into::<HtmlStyleElement>() {
402                        Ok(element) => element,
403                        Err(_err) => return,
404                    };
405                style_element_from_id.set_id(style_id);
406                if let Some(head) = document.head() {
407                    let _ = head.append_child(&style_element_from_id);
408                }
409                style_element_from_id
410            }
411        };
412        if !css_text.is_empty() {
413            let text_node: Text = document.create_text_node(css_text);
414            let _ = style_element.append_child(&text_node);
415        }
416    }
417
418    /// Builds a CSS style string from an array of key-value pairs.
419    ///
420    /// This function is used by the `html!` macro to convert static `style:`
421    /// attributes into a CSS string without allocating intermediate objects.
422    ///
423    /// # Arguments
424    ///
425    /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
426    ///
427    /// # Returns
428    ///
429    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
430    pub fn create_style_string(props: &[(&str, &str)]) -> String {
431        props
432            .iter()
433            .map(|(key, value): &(&str, &str)| {
434                format!("{key}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}")
435            })
436            .collect::<Vec<String>>()
437            .join(&CHAR_SPACE.to_string())
438    }
439
440    /// Builds a CSS style string from owned key-value pairs.
441    ///
442    /// Used by the `html!` macro for reactive style attributes (with `if`
443    /// conditions) where values are computed at runtime.
444    ///
445    /// # Arguments
446    ///
447    /// - `&[(String, String)]` - An array of owned CSS property name-value pairs.
448    ///
449    /// # Returns
450    ///
451    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
452    pub fn create_style_string_owned(props: &[(String, String)]) -> String {
453        props
454            .iter()
455            .map(|(key, value): &(String, String)| {
456                format!("{key}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}")
457            })
458            .collect::<Vec<String>>()
459            .join(&CHAR_SPACE.to_string())
460    }
461
462    /// Injects CSS text into the shared `<style>` element in the DOM.
463    ///
464    /// Delegates to [`Css::append_css`] for the actual DOM append.
465    /// Unlike the previous implementation, this does not read the existing
466    /// stylesheet content or perform a full-text `contains` search.
467    ///
468    /// # Arguments
469    ///
470    /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
471    ///
472    /// # Panics
473    ///
474    /// Panics if `window()` or `document()` is unavailable on the current platform.
475    pub fn inject_css(css_text: &str) {
476        Self::append_css(css_text);
477    }
478}
479
480/// Displays the CSS class name.
481///
482/// This enables `format!("{}", css)` to produce the class name string,
483/// which is required for reactive `if` conditions in `class:` attributes.
484impl Display for Css {
485    /// Formats the CSS class as its name string.
486    ///
487    /// # Arguments
488    ///
489    /// - `&mut Formatter` - The formatter.
490    ///
491    /// # Returns
492    ///
493    /// - `fmt::Result` - The formatting result.
494    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
495        write!(formatter, "{}", self.get_name())
496    }
497}