Skip to main content

euv_core/vdom/attribute/
impl.rs

1use super::*;
2
3/// Implementation of the public read side of an attribute entry.
4impl AttributeEntry {
5    /// Returns the CSS class names this attribute carries.
6    ///
7    /// The `class` attribute is the one place where a component's
8    /// styling decisions become observable, so it needs a public read
9    /// side: a component library's tests have to be able to assert
10    /// that a state actually landed on the right class, and an external
11    /// crate cannot read the raw attribute value.
12    ///
13    /// Every shape the attribute can take is reported:
14    /// `Css` and `CssRef` contribute the class name they carry, `Text`
15    /// and `StaticText` contribute their string split on whitespace,
16    /// a `Signal` contributes the class list it currently holds — that
17    /// is the shape a reactive `class:` attribute takes — and the
18    /// remaining shapes, an event handler, contribute nothing, because
19    /// neither is a class list.
20    ///
21    /// # Returns
22    ///
23    /// - `Vec<String>` - The class names, in the order they appear.
24    pub fn class_names(&self) -> Vec<String> {
25        match self.get_value() {
26            AttributeValue::Css(css) => vec![css.get_name().clone()],
27            AttributeValue::CssRef(css) => vec![css.get_name().clone()],
28            AttributeValue::Text(text) => split_class_names(text),
29            AttributeValue::StaticText(text) => split_class_names(text),
30            AttributeValue::Signal(signal) => signal.with(|text: &String| split_class_names(text)),
31            _ => Vec::new(),
32        }
33    }
34
35    /// Returns the attribute's value as text, when it carries one.
36    ///
37    /// A `Text` or `StaticText` attribute is its own value; a `Css` or
38    /// `CssRef` attribute contributes its class name, because that is
39    /// the text the browser ends up showing for it. The remaining
40    /// shapes — a reactive signal, an event handler — carry no text.
41    ///
42    /// # Returns
43    ///
44    /// - `Option<String>` - The value, when the attribute has one.
45    pub fn text_value(&self) -> Option<String> {
46        match self.get_value() {
47            AttributeValue::Text(text) => Some(text.clone()),
48            AttributeValue::StaticText(text) => Some((*text).to_string()),
49            AttributeValue::Css(css) => Some(css.get_name().clone()),
50            AttributeValue::CssRef(css) => Some(css.get_name().clone()),
51            _ => None,
52        }
53    }
54}
55
56/// Implementation of injected class tracking for CSS deduplication.
57impl InjectedClassesCell {
58    /// Returns `true` if the given class name has already been injected into
59    /// the DOM.
60    ///
61    /// # Arguments
62    ///
63    /// - `&str` - The CSS class name to check.
64    ///
65    /// # Returns
66    ///
67    /// - `bool` - Whether the class name has been injected.
68    pub(crate) fn is_injected(class_name: &str) -> bool {
69        INJECTED_CLASSES
70            .read()
71            .map(|classes: RwLockReadGuard<'_, HashSet<String>>| classes.contains(class_name))
72            .unwrap_or(false)
73    }
74
75    /// Marks a class name as injected so future calls to `is_injected`
76    /// return `true`.
77    ///
78    /// A poisoned lock (which only happens if another thread panicked while
79    /// holding the write guard) is recovered from rather than propagated:
80    /// the set is deduplication bookkeeping, so losing a lock must never
81    /// take down the render path.
82    ///
83    /// # Arguments
84    ///
85    /// - `&str` - The CSS class name to mark as injected.
86    pub(crate) fn mark_injected(class_name: &str) {
87        let write_result: Result<
88            InjectedClassesWriteGuard<'_>,
89            PoisonError<InjectedClassesWriteGuard<'_>>,
90        > = INJECTED_CLASSES.write();
91        let Ok(mut classes) = write_result else {
92            return;
93        };
94        classes.insert(class_name.to_string());
95    }
96}
97
98/// Implementation of attribute value factory methods for reactive and merged values.
99impl AttributeValue {
100    /// Creates a reactive attribute `Self` for conditional attribute values.
101    ///
102    /// This function replaces the inline `Signal::create(...)` boilerplate
103    /// that was previously generated by the `html!` macro for every
104    /// attribute value containing an `if` condition.
105    ///
106    /// The returned `Self::Text` is a snapshot computed on each render of
107    /// the enclosing dynamic node: the signals read inside
108    /// `compute` during that render register the dynamic node as their
109    /// dependent, so any source change re-renders the node and rebuilds
110    /// this attribute with the new value. No signal slot or registry
111    /// listener is allocated for the snapshot. Direct `Self::Signal`
112    /// bindings retain their independent DOM subscription behavior.
113    ///
114    /// # Arguments
115    ///
116    /// - `F` - A closure that computes the current attribute value.
117    ///   Called once per render of the enclosing dynamic node.
118    ///
119    /// # Returns
120    ///
121    /// - `Self` - A `Self::Text` carrying the freshly computed value.
122    pub fn reactive<F>(compute: F) -> Self
123    where
124        F: Fn() -> String + 'static,
125    {
126        Self::Text(compute())
127    }
128
129    /// Merges multiple class attribute values into a single `Self`.
130    ///
131    /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
132    /// `Css` values are injected into the DOM and their names are collected.
133    /// All non-empty class names are joined with spaces into a final `Text` attribute.
134    /// Signal inputs are read without allocating a new signal for the
135    /// merged snapshot. The enclosing dynamic node re-renders on any
136    /// constituent signal change (see
137    /// [`AttributeValue::reactive`]).
138    ///
139    /// # Arguments
140    ///
141    /// - `&[Self]` - The class attribute values to merge.
142    ///
143    /// # Returns
144    ///
145    /// - `Self` - A merged attribute value containing space-separated class names.
146    pub fn merge_class(values: &[Self]) -> Self {
147        Self::Text(Self::join_class_segments(values))
148    }
149
150    /// Joins class attribute values into a single space-separated string.
151    ///
152    /// Appends borrowed segments directly to the result. Signal snapshots
153    /// stay borrowed through `with`, avoiding an owned string clone for
154    /// every reactive segment on every render.
155    ///
156    /// OPT-11: both `Css` and `CssRef` arms inject the style on first
157    /// reference. Without the `CssRef` arm the ref would fall through
158    /// to `_ => None` and silently drop out of the merged class list —
159    /// the regression that wiped every multi-class CssRef entry (e.g.
160    /// the `c_binding_slider` class next to a parameterized
161    /// `c_slider_value("30%")` on the same `<input>`).
162    ///
163    /// # Arguments
164    ///
165    /// - `&[Self]` - The class attribute values to merge, in order.
166    ///
167    /// # Returns
168    ///
169    /// - `String` - The merged class list, segments separated by a
170    ///   single space; empty when no segment survived filtering.
171    fn join_class_segments(values: &[Self]) -> String {
172        let mut joined: String = String::with_capacity(Self::segments_capacity(values, true));
173        for value in values {
174            match value {
175                Self::Css(css) => {
176                    css.inject_style();
177                    Self::append_segment(&mut joined, css.get_name());
178                }
179                Self::CssRef(css) => {
180                    css.inject_style();
181                    Self::append_segment(&mut joined, css.get_name());
182                }
183                Self::Text(text_value) => {
184                    Self::append_segment(&mut joined, text_value);
185                }
186                Self::Signal(signal) => {
187                    signal.with(|text: &String| Self::append_segment(&mut joined, text));
188                }
189                _ => {}
190            }
191        }
192        joined
193    }
194
195    /// Computes the output byte capacity without copying signal payloads.
196    ///
197    /// # Arguments
198    ///
199    /// - `&[Self]` - Input segments whose supported nonempty values are counted.
200    /// - `bool` - Whether CSS class references are accepted by the merge.
201    ///
202    /// # Returns
203    ///
204    /// - `usize` - Required bytes including separators between nonempty segments.
205    fn segments_capacity(values: &[Self], classes: bool) -> usize {
206        let mut capacity: usize = 0;
207        for value in values {
208            let size: usize = match value {
209                Self::Css(css) if classes => css.get_name().len(),
210                Self::CssRef(css) if classes => css.get_name().len(),
211                Self::Text(text) => text.len(),
212                Self::Signal(signal) => signal.with(String::len),
213                _ => 0,
214            };
215            if size > 0 {
216                if capacity > 0 {
217                    capacity += CHAR_SPACE.len();
218                }
219                capacity += size;
220            }
221        }
222        capacity
223    }
224
225    /// Appends a nonempty segment, separating it from preceding segments.
226    ///
227    /// # Arguments
228    ///
229    /// - `&mut String` - The destination string.
230    /// - `&str` - The borrowed segment to append.
231    fn append_segment(joined: &mut String, segment: &str) {
232        if segment.is_empty() {
233            return;
234        }
235        if !joined.is_empty() {
236            joined.push_str(CHAR_SPACE);
237        }
238        joined.push_str(segment);
239    }
240
241    /// Merges multiple style attribute values into a single `Self`.
242    ///
243    /// Each input value is expected to be a style string (`Text`) or a reactive
244    /// `Signal<String>` producing a style string. All non-empty style strings are
245    /// joined with spaces into a final combined style attribute.
246    /// Signal inputs are read without allocating a new signal for the
247    /// merged snapshot. The enclosing dynamic node re-renders on any
248    /// constituent signal change (see
249    /// [`AttributeValue::reactive`]).
250    ///
251    /// # Arguments
252    ///
253    /// - `&[Self]` - The style attribute values to merge.
254    ///
255    /// # Returns
256    ///
257    /// - `Self` - A merged attribute value containing the combined CSS style string.
258    pub fn merge_style(values: &[Self]) -> Self {
259        Self::Text(Self::join_style_segments(values))
260    }
261
262    /// Joins style attribute values into a single space-separated string.
263    ///
264    /// Borrows signal values without allocating intermediate snapshots,
265    /// using the same separator and empty-segment rules as class merging.
266    ///
267    /// # Arguments
268    ///
269    /// - `&[Self]` - The style attribute values to merge, in order.
270    ///
271    /// # Returns
272    ///
273    /// - `String` - The merged CSS declarations, segments separated by a
274    ///   single space; empty when no segment carried a declaration.
275    fn join_style_segments(values: &[Self]) -> String {
276        let mut joined: String = String::with_capacity(Self::segments_capacity(values, false));
277        for value in values {
278            match value {
279                Self::Text(text_value) => {
280                    Self::append_segment(&mut joined, text_value);
281                }
282                Self::Signal(signal) => {
283                    signal.with(|text: &String| Self::append_segment(&mut joined, text));
284                }
285                _ => {}
286            }
287        }
288        joined
289    }
290
291    /// Converts a bool signal into a reactive boolean attribute value.
292    ///
293    /// Produces `AttributeValue::BoolSignal` directly: the renderer writes
294    /// `"true"` / `"false"` and subscribes the source signal to the element
295    /// with no intermediate mapping signal (the previous `Signal<String>`
296    /// bridge) and no per-render subscription.
297    ///
298    /// # Arguments
299    ///
300    /// - `Signal<bool>` - The source boolean signal.
301    ///
302    /// # Returns
303    ///
304    /// - `AttributeValue` - A `BoolSignal` wrapping the source signal.
305    pub(crate) fn bool_to_attr(source: Signal<bool>) -> AttributeValue {
306        AttributeValue::BoolSignal(source)
307    }
308}
309
310/// Visual equality comparison for attribute values.
311///
312/// Compares values by their visual output rather than identity. `Signal`
313/// values are compared by their current resolved string (borrow-compared
314/// via `Signal::with`, no `String` clones); when both signals share the
315/// same slab slot, they are always considered **unequal** — the patch walk
316/// recognises that case separately and skips the DOM write because the
317/// mount-time signal binding already owns the attribute's DOM value (see
318/// `patch_attributes`), while the unequal verdict keeps `visual_eq` from
319/// masking a value change between VDOM snapshots.
320/// `Event` values are always considered equal (re-binding is handled by the
321/// handler registry), and `Css` values are compared by class name.
322impl PartialEq for AttributeValue {
323    /// Compares two attribute values for visual equality.
324    ///
325    /// # Arguments
326    ///
327    /// - `&Self` - The first attribute value.
328    /// - `&Self` - The second attribute value.
329    ///
330    /// # Returns
331    ///
332    /// - `bool` - `true` if the values are visually equal.
333    fn eq(&self, other: &Self) -> bool {
334        match (self, other) {
335            (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
336            (Self::StaticText(old_value), Self::StaticText(new_value)) => old_value == new_value,
337            (Self::Text(old_value), Self::StaticText(new_value)) => old_value == new_value,
338            (Self::StaticText(old_value), Self::Text(new_value)) => old_value == new_value,
339            (Self::Signal(old_signal), Self::Signal(new_signal)) => {
340                if old_signal.get_inner() == new_signal.get_inner() {
341                    return false;
342                }
343                old_signal.with(|old_value: &String| {
344                    new_signal.with(|new_value: &String| old_value == new_value)
345                })
346            }
347            (Self::Signal(old_signal), Self::Text(new_value)) => {
348                old_signal.with(|old_value: &String| old_value == new_value)
349            }
350            (Self::Text(old_value), Self::Signal(new_signal)) => {
351                new_signal.with(|new_value: &String| old_value == new_value)
352            }
353            (Self::BoolSignal(old_signal), Self::BoolSignal(new_signal)) => {
354                if old_signal.get_inner() == new_signal.get_inner() {
355                    return false;
356                }
357                old_signal.get() == new_signal.get()
358            }
359            (Self::Event(_), Self::Event(_)) => true,
360            (Self::Css(old_class), Self::Css(new_class)) => {
361                old_class.get_name() == new_class.get_name()
362            }
363            (Self::CssRef(old_class), Self::CssRef(new_class)) => {
364                old_class.get_name() == new_class.get_name()
365            }
366            (Self::CssRef(old_class), Self::Css(new_class)) => {
367                old_class.get_name() == new_class.get_name()
368            }
369            (Self::Css(old_class), Self::CssRef(new_class)) => {
370                old_class.get_name() == new_class.get_name()
371            }
372            (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
373            _ => false,
374        }
375    }
376}
377
378/// Visual equality comparison for attribute entries.
379///
380/// Two attribute entries are equal when their names match and their values
381/// are visually equal as defined by `AttributeValue::eq`.
382impl PartialEq for AttributeEntry {
383    /// Compares two attribute entries for visual equality.
384    ///
385    /// # Arguments
386    ///
387    /// - `&Self` - The first attribute entry.
388    /// - `&Self` - The second attribute entry.
389    ///
390    /// # Returns
391    ///
392    /// - `bool` - `true` if both names and values match.
393    fn eq(&self, other: &Self) -> bool {
394        self.get_name() == other.get_name() && self.get_value() == other.get_value()
395    }
396}
397
398/// Visual equality comparison for CSS classes.
399///
400/// Two CSS classes are considered equal when their class names match,
401/// since the name uniquely identifies the visual style rule.
402impl PartialEq for Css {
403    /// Compares two CSS classes by name.
404    ///
405    /// # Arguments
406    ///
407    /// - `&Self` - The first CSS class.
408    /// - `&Self` - The second CSS class.
409    ///
410    /// # Returns
411    ///
412    /// - `bool` - `true` if the class names match.
413    fn eq(&self, other: &Self) -> bool {
414        self.get_name() == other.get_name()
415    }
416}
417
418/// Implementation of Css construction and style injection.
419impl Css {
420    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
421    ///
422    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
423    /// This is used by the `class!` macro for fully static class definitions
424    /// where pseudo rules can be computed at compile time.
425    ///
426    /// # Arguments
427    ///
428    /// - `I` - The serialized pseudo rules string.
429    ///
430    /// # Returns
431    ///
432    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
433    pub fn parse_pseudo_rules<I>(input: I) -> Vec<PseudoRule>
434    where
435        I: AsRef<str>,
436    {
437        let mut remaining: &str = input.as_ref();
438        let mut rules: Vec<PseudoRule> = Vec::new();
439        while !remaining.is_empty() {
440            let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
441            let Some(selector_end_index) = selector_end else {
442                break;
443            };
444            let selector: &str = &remaining[..selector_end_index];
445            let after_selector: &str = remaining[selector_end_index..]
446                .strip_prefix(CSS_RULE_OPEN)
447                .unwrap_or_default();
448            let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
449            let Some(style_end_index) = style_end else {
450                break;
451            };
452            let style: &str = &after_selector[..style_end_index];
453            if !selector.is_empty() && !style.is_empty() {
454                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
455            }
456            remaining = after_selector[style_end_index..]
457                .strip_prefix(CHAR_CSS_RULE_CLOSE)
458                .unwrap_or_default();
459        }
460        rules
461    }
462
463    /// Parses media query rules from a compact serialization string.
464    ///
465    /// The serialization format is:
466    /// `@media query { key: value; ::selector { key: value; } }@media query2 { ... }`
467    /// This is used by the `class!` macro for fully static class definitions
468    /// where media rules can be computed at compile time.
469    /// Supports nested pseudo-element blocks inside media query blocks.
470    ///
471    /// # Arguments
472    ///
473    /// - `S` - The serialized media rules string.
474    ///
475    /// # Returns
476    ///
477    /// - `Vec<MediaRule>` - The parsed media rules.
478    pub fn parse_media_rules<S>(input: S) -> Vec<MediaRule>
479    where
480        S: AsRef<str>,
481    {
482        let input: &str = input.as_ref();
483        let mut rules: Vec<MediaRule> = Vec::new();
484        let mut remaining: &str = input;
485        while !remaining.is_empty() {
486            if !remaining.starts_with(CSS_MEDIA_PREFIX) {
487                break;
488            }
489            let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
490            let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
491            let Some(query_end_index) = query_end else {
492                break;
493            };
494            let query: &str = &after_prefix[..query_end_index];
495            let after_query: &str = after_prefix[query_end_index..]
496                .strip_prefix(CSS_RULE_OPEN)
497                .unwrap_or_default();
498            let mut depth: usize = 1;
499            let mut close_pos: usize = 0;
500            for (index, char_value) in after_query.char_indices() {
501                if char_value == '{' {
502                    depth += 1;
503                } else if char_value == '}' {
504                    depth -= 1;
505                    if depth == 0 {
506                        close_pos = index;
507                        break;
508                    }
509                }
510            }
511            if close_pos == 0 {
512                break;
513            }
514            let body: &str = &after_query[..close_pos];
515            let (style, pseudo_rules): (String, Vec<PseudoRule>) = Self::parse_media_body(body);
516            if !query.is_empty() && (!style.is_empty() || !pseudo_rules.is_empty()) {
517                rules.push(MediaRule::new(query.to_string(), style, pseudo_rules));
518            }
519            remaining = after_query[close_pos..]
520                .strip_prefix(CHAR_CSS_RULE_CLOSE)
521                .unwrap_or_default();
522        }
523        rules
524    }
525
526    /// Parses the body of a media rule, separating top-level style declarations
527    /// from nested pseudo-element blocks.
528    ///
529    /// # Arguments
530    ///
531    /// - `&str` - The media rule body content (between the outer braces).
532    ///
533    /// # Returns
534    ///
535    /// - `(String, Vec<PseudoRule>)` - A tuple of the style string and pseudo rules.
536    fn parse_media_body(body: &str) -> (String, Vec<PseudoRule>) {
537        let mut style_parts: String = String::new();
538        let mut pseudo_rules: Vec<PseudoRule> = Vec::new();
539        let mut remaining: &str = body;
540        while !remaining.is_empty() {
541            let brace_pos: Option<usize> = remaining.find('{');
542            match brace_pos {
543                Some(pos) => {
544                    let before_brace: &str = remaining[..pos].trim();
545                    if before_brace.starts_with("::") || before_brace.starts_with(':') {
546                        let selector: &str = before_brace;
547                        let after_brace: &str = &remaining[pos + 1..];
548                        let mut depth: usize = 1;
549                        let mut close_pos: usize = 0;
550                        for (index, char_value) in after_brace.char_indices() {
551                            if char_value == '{' {
552                                depth += 1;
553                            } else if char_value == '}' {
554                                depth -= 1;
555                                if depth == 0 {
556                                    close_pos = index;
557                                    break;
558                                }
559                            }
560                        }
561                        if close_pos > 0 {
562                            let inner_style: &str = after_brace[..close_pos].trim();
563                            if !selector.is_empty() && !inner_style.is_empty() {
564                                pseudo_rules.push(PseudoRule::new(
565                                    selector.to_string(),
566                                    inner_style.to_string(),
567                                ));
568                            }
569                            remaining = after_brace[close_pos + 1..].trim_start();
570                            continue;
571                        }
572                        break;
573                    } else {
574                        style_parts.push_str(before_brace);
575                        style_parts.push(' ');
576                        let after_brace: &str = &remaining[pos + 1..];
577                        let mut depth: usize = 1;
578                        let mut close_pos: usize = 0;
579                        for (index, char_value) in after_brace.char_indices() {
580                            if char_value == '{' {
581                                depth += 1;
582                            } else if char_value == '}' {
583                                depth -= 1;
584                                if depth == 0 {
585                                    close_pos = index;
586                                    break;
587                                }
588                            }
589                        }
590                        if close_pos > 0 {
591                            style_parts.push_str(after_brace[..close_pos].trim());
592                            style_parts.push(' ');
593                            remaining = after_brace[close_pos + 1..].trim_start();
594                            continue;
595                        }
596                        break;
597                    }
598                }
599                None => {
600                    style_parts.push_str(remaining.trim());
601                    break;
602                }
603            }
604        }
605        (style_parts.trim().to_string(), pseudo_rules)
606    }
607
608    /// Injects this class's styles into the DOM if not already present.
609    ///
610    /// Uses a global `HashSet` to track injected class names, avoiding the
611    /// expensive `existing_css.contains(css)` full-text search on every call.
612    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
613    /// then appends them directly to the `<style>` element via
614    /// `append_child` with a new text node — no read-modify-write of the
615    /// entire stylesheet content.
616    ///
617    /// # Panics
618    ///
619    /// Panics if `window()` or `document()` is unavailable on the current platform.
620    pub fn inject_style(&self) {
621        let class_name: &String = self.get_name();
622        if InjectedClassesCell::is_injected(class_name) {
623            return;
624        }
625        InjectedClassesCell::mark_injected(class_name);
626        let raw_name: String = self.get_name().clone();
627        let mut escaped_name: String = String::with_capacity(raw_name.len() * 2);
628        for ch in raw_name.chars() {
629            if ch.is_ascii_alphanumeric() || ch == CHAR_HYPHEN || ch == CHAR_UNDERSCORE {
630                escaped_name.push(ch);
631            } else {
632                escaped_name.push(CHAR_CSS_ESCAPE);
633                escaped_name.push(ch);
634            }
635        }
636        let mut css_text: String = format!(
637            "{CHAR_CSS_CLASS_PREFIX}{escaped_name}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
638            self.get_style()
639        );
640        for pseudo_rule in self.get_pseudo_rules() {
641            if !pseudo_rule.get_style().is_empty() {
642                css_text = format!(
643                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
644                    escaped_name,
645                    pseudo_rule.get_selector(),
646                    pseudo_rule.get_style()
647                );
648            }
649        }
650        for media_rule in self.get_media_rules() {
651            if !media_rule.get_query().is_empty() {
652                let mut media_body: String = format!(
653                    "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
654                    escaped_name,
655                    media_rule.get_style()
656                );
657                for pseudo_rule in media_rule.get_pseudo_rules() {
658                    if !pseudo_rule.get_style().is_empty() {
659                        media_body = format!(
660                            "{media_body} {CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
661                            escaped_name,
662                            pseudo_rule.get_selector(),
663                            pseudo_rule.get_style()
664                        );
665                    }
666                }
667                css_text = format!(
668                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CSS_MEDIA_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
669                    media_rule.get_query(),
670                    media_body
671                );
672            }
673        }
674        Self::append_css(&css_text);
675    }
676
677    /// Appends CSS text directly to the shared `<style>` element.
678    ///
679    /// Creates a new text node and appends it as a child of the `<style>`
680    /// element, avoiding the read-modify-write pattern of reading the entire
681    /// `innerText`, concatenating, and setting it back.
682    ///
683    /// # Arguments
684    ///
685    /// - `&str` - The CSS text to append.
686    ///
687    /// The `cfg!` guard matches the one `Scheduler::current_time` already
688    /// uses: `window()` reads a wasm-only imported static, so on a non-wasm
689    /// target it panics with "cannot access imported statics on non-wasm
690    /// targets" rather than returning `None`. That panic short-circuits the
691    /// graceful `match window() { None => return }` below, and it fires from
692    /// `OnceLock<Css>::get_or_init` — the first time any `c_*()` class
693    /// function is called. Every styled view component calls one, so
694    /// without this guard none of them can be rendered in a native
695    /// `cargo test` process. On wasm the guard is a constant `true` and
696    /// this function behaves exactly as before.
697    fn append_css(css_text: &str) {
698        if !cfg!(target_arch = "wasm32") {
699            return;
700        }
701        let style_id: &str = EUV_CSS_INJECTED_ID;
702        let window_value: Window = match window() {
703            Some(window_instance) => window_instance,
704            None => return,
705        };
706        let document: Document = match window_value.document() {
707            Some(document_instance) => document_instance,
708            None => return,
709        };
710        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
711            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
712                Ok(element) => element,
713                Err(_err) => return,
714            },
715            None => {
716                let created: Element = match document.create_element(STYLE_TAG) {
717                    Ok(element) => element,
718                    Err(_err) => return,
719                };
720                let style_element_from_id: HtmlStyleElement =
721                    match created.dyn_into::<HtmlStyleElement>() {
722                        Ok(element) => element,
723                        Err(_err) => return,
724                    };
725                style_element_from_id.set_id(style_id);
726                if let Some(head) = document.head() {
727                    let _: Result<Node, JsValue> = head.append_child(&style_element_from_id);
728                }
729                style_element_from_id
730            }
731        };
732        if !css_text.is_empty() {
733            let text_node: Text = document.create_text_node(css_text);
734            let _: Result<Node, JsValue> = style_element.append_child(&text_node);
735        }
736    }
737
738    /// Builds a CSS style string from an array of key-value pairs.
739    ///
740    /// This function is used by the `html!` macro to convert static `style:`
741    /// attributes into a CSS string without allocating intermediate objects.
742    ///
743    /// # Arguments
744    ///
745    /// - `&[(K, V)]` - An array of CSS property name-value pairs.
746    ///
747    /// # Returns
748    ///
749    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
750    pub fn style_string<K, V>(props: &[(K, V)]) -> String
751    where
752        K: AsRef<str>,
753        V: AsRef<str>,
754    {
755        // Single pre-sized `String` instead of per-prop `format!` +
756        // intermediate `Vec<String>` + `join` (was N + 2 allocations).
757        let total_len: usize = props
758            .iter()
759            .map(|(key, value): &(K, V)| {
760                key.as_ref().len() + CSS_PROP_SEPARATOR.len() + value.as_ref().len() + 2
761            })
762            .sum();
763        let mut out: String = String::with_capacity(total_len);
764        for (index, (key, value)) in props.iter().enumerate() {
765            if index > 0 {
766                out.push_str(CHAR_SPACE);
767            }
768            out.push_str(key.as_ref());
769            out.push_str(CSS_PROP_SEPARATOR);
770            out.push_str(value.as_ref());
771            out.push(CHAR_CSS_DECL_TERMINATOR);
772        }
773        out
774    }
775
776    /// Builds a stable suffix for a class name from a dynamic parameter value.
777    ///
778    /// Used by the `class!` macro when a parameter is wrapped in `{}` in the
779    /// class body. Wrapping a parameter opts it into value-dependent class
780    /// names, so each distinct value can inject its own CSS rule.
781    ///
782    /// # Arguments
783    ///
784    /// - `&str` - The dynamic parameter value.
785    ///
786    /// # Returns
787    ///
788    /// - `String` - A stable hexadecimal suffix for the class name.
789    pub fn param_class_name(value: &str) -> String {
790        let mut hash: u64 = CLASS_PARAM_HASH_FNV_OFFSET;
791        for byte in value.as_bytes() {
792            hash ^= u64::from(*byte);
793            hash = hash.wrapping_mul(CLASS_PARAM_HASH_FNV_PRIME);
794        }
795        format!("{hash:x}")
796    }
797
798    /// Builds a CSS style string from owned key-value pairs.
799    ///
800    /// Used by the `html!` macro for reactive style attributes (with `if`
801    /// conditions) where values are computed at runtime.
802    ///
803    /// # Arguments
804    ///
805    /// - `&[(String, String)]` - An array of owned CSS property name-value pairs.
806    ///
807    /// # Returns
808    ///
809    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
810    pub fn style_string_owned(props: &[(String, String)]) -> String {
811        // Single pre-sized `String` (same shape as `style_string`).
812        let total_len: usize = props
813            .iter()
814            .map(|(key, value): &(String, String)| {
815                key.len() + CSS_PROP_SEPARATOR.len() + value.len() + 2
816            })
817            .sum();
818        let mut out: String = String::with_capacity(total_len);
819        for (index, (key, value)) in props.iter().enumerate() {
820            if index > 0 {
821                out.push_str(CHAR_SPACE);
822            }
823            out.push_str(key);
824            out.push_str(CSS_PROP_SEPARATOR);
825            out.push_str(value);
826            out.push(CHAR_CSS_DECL_TERMINATOR);
827        }
828        out
829    }
830
831    /// Injects CSS text into the shared `<style>` element in the DOM.
832    ///
833    /// Delegates to [`Css::append_css`] for the actual DOM append.
834    /// Unlike the previous implementation, this does not read the existing
835    /// stylesheet content or perform a full-text `contains` search.
836    ///
837    /// # Arguments
838    ///
839    /// - `S` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
840    ///
841    /// # Panics
842    ///
843    /// Panics if `window()` or `document()` is unavailable on the current platform.
844    pub fn inject_css<S>(css_text: S)
845    where
846        S: AsRef<str>,
847    {
848        let css_text: &str = css_text.as_ref();
849        Self::append_css(css_text);
850    }
851}
852
853/// Displays the CSS class name.
854///
855/// This enables `format!("{css}")` to produce the class name string,
856/// which is required for reactive `if` conditions in `class:` attributes.
857impl Display for Css {
858    /// Formats the CSS class as its name string.
859    ///
860    /// # Arguments
861    ///
862    /// - `&mut Formatter<'_>` - The formatter.
863    ///
864    /// # Returns
865    ///
866    /// - `fmt::Result` - The formatting result.
867    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
868        write!(formatter, "{}", self.get_name())
869    }
870}