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 injected class tracking for CSS deduplication.
7impl InjectedClassesCell {
8    /// Returns a shared reference to the injected classes set.
9    ///
10    /// # Returns
11    ///
12    /// - `&'static HashSet<String>` - A shared reference to the global set of injected class names.
13    #[allow(static_mut_refs)]
14    pub(crate) fn get_injected_classes() -> &'static HashSet<String> {
15        unsafe { &*INJECTED_CLASSES.deref().get_0().get() }
16    }
17
18    /// Returns a mutable reference to the injected classes set.
19    ///
20    /// # Returns
21    ///
22    /// - `&'static mut HashSet<String>` - A mutable reference to the global set of injected class names.
23    #[allow(static_mut_refs)]
24    pub(crate) fn get_mut_injected_classes() -> &'static mut HashSet<String> {
25        unsafe { &mut *INJECTED_CLASSES.deref().get_0().get() }
26    }
27
28    /// Returns `true` if the given class name has already been injected into the DOM.
29    ///
30    /// Encapsulates `static mut` access so callers do not need `unsafe` blocks.
31    ///
32    /// # Arguments
33    ///
34    /// - `&str` - The CSS class name to check.
35    ///
36    /// # Returns
37    ///
38    /// - `bool` - Whether the class name has been injected.
39    pub(crate) fn is_injected(class_name: &str) -> bool {
40        Self::get_injected_classes().contains(class_name)
41    }
42
43    /// Marks a class name as injected so future calls to `is_injected` return `true`.
44    ///
45    /// Encapsulates `static mut` access so callers do not need `unsafe` blocks.
46    ///
47    /// # Arguments
48    ///
49    /// - `&str` - The CSS class name to mark as injected.
50    pub(crate) fn mark_injected(class_name: &str) {
51        Self::get_mut_injected_classes().insert(class_name.to_string());
52    }
53}
54
55/// Implementation of attribute value factory methods for reactive and merged values.
56impl AttributeValue {
57    /// Creates a reactive attribute `Self` for conditional attribute values.
58    ///
59    /// This function replaces the inline `Signal::create(...)` + `subscribe_attr(...)`
60    /// boilerplate that was previously generated by the `html!` macro for every
61    /// attribute value containing an `if` condition.
62    ///
63    /// # Arguments
64    ///
65    /// - `F: Fn() -> String + 'static` - A closure that computes the current attribute value.
66    ///   Called on initial render and whenever any signal changes.
67    ///
68    /// # Returns
69    ///
70    /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
71    ///   that reactively re-evaluates the attribute value on signal updates.
72    pub fn reactive<F>(compute: F) -> Self
73    where
74        F: Fn() -> String + 'static,
75    {
76        let attr_signal: Signal<String> = Signal::create(compute());
77        Self::subscribe_attr(attr_signal, compute);
78        Self::Signal(attr_signal)
79    }
80
81    /// Merges multiple class attribute values into a single `Self`.
82    ///
83    /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
84    /// `Css` values are injected into the DOM and their names are collected.
85    /// All non-empty class names are joined with spaces into a final `Text` attribute.
86    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute
87    /// that re-evaluates when any constituent signal changes.
88    ///
89    /// # Arguments
90    ///
91    /// - `&[Self]` - The class attribute values to merge.
92    ///
93    /// # Returns
94    ///
95    /// - `Self` - A merged attribute value containing space-separated class names.
96    pub fn merge_class(values: &[Self]) -> Self {
97        let has_signal: bool = values
98            .iter()
99            .any(|value: &Self| matches!(value, Self::Signal(_)));
100        if has_signal {
101            let owned_values: Vec<Self> = values.to_vec();
102            let compute: Box<dyn Fn() -> String> = Box::new(move || {
103                owned_values
104                    .iter()
105                    .filter_map(|value: &Self| match value {
106                        Self::Css(css) => {
107                            css.inject_style();
108                            Some(css.get_name().to_string())
109                        }
110                        Self::Text(text_value) => Some(text_value.clone()),
111                        Self::Signal(signal) => Some(signal.get()),
112                        _ => None,
113                    })
114                    .filter(|segment: &String| !segment.is_empty())
115                    .collect::<Vec<String>>()
116                    .join(&CHAR_SPACE.to_string())
117            });
118            let attr_signal: Signal<String> = Signal::create(compute());
119            Self::subscribe_attr(attr_signal, compute);
120            return Self::Signal(attr_signal);
121        }
122        let result: String = values
123            .iter()
124            .filter_map(|value: &Self| match value {
125                Self::Css(css) => {
126                    css.inject_style();
127                    Some(css.get_name().to_string())
128                }
129                Self::Text(text_value) => Some(text_value.clone()),
130                _ => None,
131            })
132            .filter(|segment: &String| !segment.is_empty())
133            .collect::<Vec<String>>()
134            .join(&CHAR_SPACE.to_string());
135        Self::Text(result)
136    }
137
138    /// Merges multiple style attribute values into a single `Self`.
139    ///
140    /// Each input value is expected to be a style string (`Text`) or a reactive
141    /// `Signal<String>` producing a style string. All non-empty style strings are
142    /// joined with spaces into a final combined style attribute.
143    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute.
144    ///
145    /// # Arguments
146    ///
147    /// - `&[Self]` - The style attribute values to merge.
148    ///
149    /// # Returns
150    ///
151    /// - `Self` - A merged attribute value containing the combined CSS style string.
152    pub fn merge_style(values: &[Self]) -> Self {
153        let has_signal: bool = values
154            .iter()
155            .any(|value: &Self| matches!(value, Self::Signal(_)));
156        if has_signal {
157            let owned_values: Vec<Self> = values.to_vec();
158            let compute: Box<dyn Fn() -> String> = Box::new(move || {
159                owned_values
160                    .iter()
161                    .filter_map(|value: &Self| match value {
162                        Self::Text(text_value) => Some(text_value.clone()),
163                        Self::Signal(signal) => Some(signal.get()),
164                        _ => None,
165                    })
166                    .filter(|segment: &String| !segment.is_empty())
167                    .collect::<Vec<String>>()
168                    .join(&CHAR_SPACE.to_string())
169            });
170            let attr_signal: Signal<String> = Signal::create(compute());
171            Self::subscribe_attr(attr_signal, compute);
172            return Self::Signal(attr_signal);
173        }
174        let result: String = values
175            .iter()
176            .filter_map(|value: &Self| match value {
177                Self::Text(text_value) => Some(text_value.clone()),
178                _ => None,
179            })
180            .filter(|segment: &String| !segment.is_empty())
181            .collect::<Vec<String>>()
182            .join(&CHAR_SPACE.to_string());
183        Self::Text(result)
184    }
185
186    /// Subscribes an attribute signal to the global signal update dispatch cycle.
187    ///
188    /// Creates a callback that re-computes the attribute value and sets
189    /// it on the signal whenever a signal update cycle runs. The callback
190    /// is registered in the signal update registry using the signal's
191    /// inner address as the key.
192    ///
193    /// # Arguments
194    ///
195    /// - `Signal<String>` - The attribute signal to subscribe.
196    /// - `F: Fn() -> String + 'static` - A closure that computes the current attribute value string.
197    fn subscribe_attr<F>(attr_signal: Signal<String>, compute: F)
198    where
199        F: Fn() -> String + 'static,
200    {
201        Registry::register_attr_listener(
202            attr_signal.get_inner(),
203            Box::new(move || {
204                attr_signal.set(compute());
205            }),
206        );
207    }
208
209    /// Converts a bool signal into a reactive `Signal<String>` attribute value.
210    ///
211    /// Creates a `Signal<String>` initialized with the bool's string
212    /// representation, then subscribes to the source signal so that
213    /// whenever the bool changes, the string signal is updated accordingly.
214    ///
215    /// # Arguments
216    ///
217    /// - `Signal<bool>` - The source boolean signal.
218    ///
219    /// # Returns
220    ///
221    /// - `AttributeValue` - An `AttributeValue::Signal` wrapping the derived string signal.
222    pub(crate) fn bool_to_attr(source: Signal<bool>) -> AttributeValue {
223        let string_signal: Signal<String> = Signal::create(source.get().to_string());
224        let string_signal_clone: Signal<String> = string_signal;
225        let source_for_sub: Signal<bool> = source;
226        source_for_sub.subscribe(move || {
227            string_signal_clone.set(source_for_sub.get().to_string());
228        });
229        AttributeValue::Signal(string_signal)
230    }
231}
232
233/// Visual equality comparison for attribute values.
234///
235/// Compares values by their visual output rather than identity. `Signal`
236/// values are compared by their current resolved string; when both signals
237/// share the same inner pointer, they are always considered **unequal**
238/// because the signal may have mutated between VDOM snapshots and `.get()`
239/// would return the same current value for both, masking the change.
240/// `Event` values are always considered equal (re-binding is handled by the
241/// handler registry), and `Css` values are compared by class name.
242impl PartialEq for AttributeValue {
243    /// Compares two attribute values for visual equality.
244    ///
245    /// # Arguments
246    ///
247    /// - `&Self` - The first attribute value.
248    /// - `&Self` - The second attribute value.
249    ///
250    /// # Returns
251    ///
252    /// - `bool` - `true` if the values are visually equal.
253    fn eq(&self, other: &Self) -> bool {
254        match (self, other) {
255            (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
256            (Self::Signal(old_signal), Self::Signal(new_signal)) => {
257                if old_signal.get_inner() == new_signal.get_inner() {
258                    return false;
259                }
260                old_signal.get() == new_signal.get()
261            }
262            (Self::Signal(old_signal), Self::Text(new_value)) => old_signal.get() == *new_value,
263            (Self::Text(old_value), Self::Signal(new_signal)) => *old_value == new_signal.get(),
264            (Self::Event(_), Self::Event(_)) => true,
265            (Self::Css(old_class), Self::Css(new_class)) => {
266                old_class.get_name() == new_class.get_name()
267            }
268            (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
269            _ => false,
270        }
271    }
272}
273
274/// Visual equality comparison for attribute entries.
275///
276/// Two attribute entries are equal when their names match and their values
277/// are visually equal as defined by `AttributeValue::eq`.
278impl PartialEq for AttributeEntry {
279    /// Compares two attribute entries for visual equality.
280    ///
281    /// # Arguments
282    ///
283    /// - `&Self` - The first attribute entry.
284    /// - `&Self` - The second attribute entry.
285    ///
286    /// # Returns
287    ///
288    /// - `bool` - `true` if both names and values match.
289    fn eq(&self, other: &Self) -> bool {
290        self.get_name() == other.get_name() && self.get_value() == other.get_value()
291    }
292}
293
294/// Visual equality comparison for CSS classes.
295///
296/// Two CSS classes are considered equal when their class names match,
297/// since the name uniquely identifies the visual style rule.
298impl PartialEq for Css {
299    /// Compares two CSS classes by name.
300    ///
301    /// # Arguments
302    ///
303    /// - `&Self` - The first CSS class.
304    /// - `&Self` - The second CSS class.
305    ///
306    /// # Returns
307    ///
308    /// - `bool` - `true` if the class names match.
309    fn eq(&self, other: &Self) -> bool {
310        self.get_name() == other.get_name()
311    }
312}
313
314/// Implementation of Css construction and style injection.
315impl Css {
316    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
317    ///
318    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
319    /// This is used by the `class!` macro for fully static class definitions
320    /// where pseudo rules can be computed at compile time.
321    ///
322    /// # Arguments
323    ///
324    /// - `I: AsRef<str>` - The serialized pseudo rules string.
325    ///
326    /// # Returns
327    ///
328    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
329    pub fn parse_pseudo_rules<I>(input: I) -> Vec<PseudoRule>
330    where
331        I: AsRef<str>,
332    {
333        let mut remaining: &str = input.as_ref();
334        let mut rules: Vec<PseudoRule> = Vec::new();
335        while !remaining.is_empty() {
336            let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
337            let Some(selector_end_index) = selector_end else {
338                break;
339            };
340            let selector: &str = &remaining[..selector_end_index];
341            let after_selector: &str = remaining[selector_end_index..]
342                .strip_prefix(CSS_RULE_OPEN)
343                .unwrap_or_default();
344            let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
345            let Some(style_end_index) = style_end else {
346                break;
347            };
348            let style: &str = &after_selector[..style_end_index];
349            if !selector.is_empty() && !style.is_empty() {
350                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
351            }
352            remaining = after_selector[style_end_index..]
353                .strip_prefix(CHAR_CSS_RULE_CLOSE)
354                .unwrap_or_default();
355        }
356        rules
357    }
358
359    /// Parses media query rules from a compact serialization string.
360    ///
361    /// The serialization format is:
362    /// `@media query { key: value; ::selector { key: value; } }@media query2 { ... }`
363    /// This is used by the `class!` macro for fully static class definitions
364    /// where media rules can be computed at compile time.
365    /// Supports nested pseudo-element blocks inside media query blocks.
366    ///
367    /// # Arguments
368    ///
369    /// - `S: AsRef<str>` - The serialized media rules string.
370    ///
371    /// # Returns
372    ///
373    /// - `Vec<MediaRule>` - The parsed media rules.
374    pub fn parse_media_rules<S>(input: S) -> Vec<MediaRule>
375    where
376        S: AsRef<str>,
377    {
378        let input: &str = input.as_ref();
379        let mut rules: Vec<MediaRule> = Vec::new();
380        let mut remaining: &str = input;
381        while !remaining.is_empty() {
382            if !remaining.starts_with(CSS_MEDIA_PREFIX) {
383                break;
384            }
385            let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
386            let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
387            let Some(query_end_index) = query_end else {
388                break;
389            };
390            let query: &str = &after_prefix[..query_end_index];
391            let after_query: &str = after_prefix[query_end_index..]
392                .strip_prefix(CSS_RULE_OPEN)
393                .unwrap_or_default();
394            let mut depth: usize = 1;
395            let mut close_pos: usize = 0;
396            for (index, char_value) in after_query.char_indices() {
397                if char_value == '{' {
398                    depth += 1;
399                } else if char_value == '}' {
400                    depth -= 1;
401                    if depth == 0 {
402                        close_pos = index;
403                        break;
404                    }
405                }
406            }
407            if close_pos == 0 {
408                break;
409            }
410            let body: &str = &after_query[..close_pos];
411            let (style, pseudo_rules): (String, Vec<PseudoRule>) = Self::parse_media_body(body);
412            if !query.is_empty() && (!style.is_empty() || !pseudo_rules.is_empty()) {
413                rules.push(MediaRule::new(query.to_string(), style, pseudo_rules));
414            }
415            remaining = after_query[close_pos..]
416                .strip_prefix(CHAR_CSS_RULE_CLOSE)
417                .unwrap_or_default();
418        }
419        rules
420    }
421
422    /// Parses the body of a media rule, separating top-level style declarations
423    /// from nested pseudo-element blocks.
424    ///
425    /// # Arguments
426    ///
427    /// - `&str` - The media rule body content (between the outer braces).
428    ///
429    /// # Returns
430    ///
431    /// - `(String, Vec<PseudoRule>)` - A tuple of the style string and pseudo rules.
432    fn parse_media_body(body: &str) -> (String, Vec<PseudoRule>) {
433        let mut style_parts: String = String::new();
434        let mut pseudo_rules: Vec<PseudoRule> = Vec::new();
435        let mut remaining: &str = body;
436        while !remaining.is_empty() {
437            let brace_pos: Option<usize> = remaining.find('{');
438            match brace_pos {
439                Some(pos) => {
440                    let before_brace: &str = remaining[..pos].trim();
441                    if before_brace.starts_with("::") || before_brace.starts_with(':') {
442                        let selector: &str = before_brace;
443                        let after_brace: &str = &remaining[pos + 1..];
444                        let mut depth: usize = 1;
445                        let mut close_pos: usize = 0;
446                        for (index, char_value) in after_brace.char_indices() {
447                            if char_value == '{' {
448                                depth += 1;
449                            } else if char_value == '}' {
450                                depth -= 1;
451                                if depth == 0 {
452                                    close_pos = index;
453                                    break;
454                                }
455                            }
456                        }
457                        if close_pos > 0 {
458                            let inner_style: &str = after_brace[..close_pos].trim();
459                            if !selector.is_empty() && !inner_style.is_empty() {
460                                pseudo_rules.push(PseudoRule::new(
461                                    selector.to_string(),
462                                    inner_style.to_string(),
463                                ));
464                            }
465                            remaining = after_brace[close_pos + 1..].trim_start();
466                            continue;
467                        }
468                        break;
469                    } else {
470                        style_parts.push_str(before_brace);
471                        style_parts.push(' ');
472                        let after_brace: &str = &remaining[pos + 1..];
473                        let mut depth: usize = 1;
474                        let mut close_pos: usize = 0;
475                        for (index, char_value) in after_brace.char_indices() {
476                            if char_value == '{' {
477                                depth += 1;
478                            } else if char_value == '}' {
479                                depth -= 1;
480                                if depth == 0 {
481                                    close_pos = index;
482                                    break;
483                                }
484                            }
485                        }
486                        if close_pos > 0 {
487                            style_parts.push_str(after_brace[..close_pos].trim());
488                            style_parts.push(' ');
489                            remaining = after_brace[close_pos + 1..].trim_start();
490                            continue;
491                        }
492                        break;
493                    }
494                }
495                None => {
496                    style_parts.push_str(remaining.trim());
497                    break;
498                }
499            }
500        }
501        (style_parts.trim().to_string(), pseudo_rules)
502    }
503
504    /// Injects this class's styles into the DOM if not already present.
505    ///
506    /// Uses a global `HashSet` to track injected class names, avoiding the
507    /// expensive `existing_css.contains(css)` full-text search on every call.
508    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
509    /// then appends them directly to the `<style>` element via
510    /// `append_child` with a new text node — no read-modify-write of the
511    /// entire stylesheet content.
512    ///
513    /// # Panics
514    ///
515    /// Panics if `window()` or `document()` is unavailable on the current platform.
516    pub fn inject_style(&self) {
517        let class_name: &String = self.get_name();
518        if InjectedClassesCell::is_injected(class_name) {
519            return;
520        }
521        InjectedClassesCell::mark_injected(class_name);
522        let raw_name: String = self.get_name().clone();
523        let mut escaped_name: String = String::with_capacity(raw_name.len() * 2);
524        for ch in raw_name.chars() {
525            if ch.is_ascii_alphanumeric() || ch == CHAR_HYPHEN || ch == CHAR_UNDERSCORE {
526                escaped_name.push(ch);
527            } else {
528                escaped_name.push(CHAR_CSS_ESCAPE);
529                escaped_name.push(ch);
530            }
531        }
532        let mut css_text: String = format!(
533            "{CHAR_CSS_CLASS_PREFIX}{escaped_name}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
534            self.get_style()
535        );
536        for pseudo_rule in self.get_pseudo_rules() {
537            if !pseudo_rule.get_style().is_empty() {
538                css_text = format!(
539                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
540                    escaped_name,
541                    pseudo_rule.get_selector(),
542                    pseudo_rule.get_style()
543                );
544            }
545        }
546        for media_rule in self.get_media_rules() {
547            if !media_rule.get_query().is_empty() {
548                let mut media_body: String = format!(
549                    "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
550                    escaped_name,
551                    media_rule.get_style()
552                );
553                for pseudo_rule in media_rule.get_pseudo_rules() {
554                    if !pseudo_rule.get_style().is_empty() {
555                        media_body = format!(
556                            "{media_body} {CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
557                            escaped_name,
558                            pseudo_rule.get_selector(),
559                            pseudo_rule.get_style()
560                        );
561                    }
562                }
563                css_text = format!(
564                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CSS_MEDIA_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
565                    media_rule.get_query(),
566                    media_body
567                );
568            }
569        }
570        Self::append_css(&css_text);
571    }
572
573    /// Appends CSS text directly to the shared `<style>` element.
574    ///
575    /// Creates a new text node and appends it as a child of the `<style>`
576    /// element, avoiding the read-modify-write pattern of reading the entire
577    /// `innerText`, concatenating, and setting it back.
578    ///
579    /// # Arguments
580    ///
581    /// - `&str` - The CSS text to append.
582    ///
583    fn append_css(css_text: &str) {
584        let style_id: &str = EUV_CSS_INJECTED_ID;
585        let window_value: Window = match window() {
586            Some(window_instance) => window_instance,
587            None => return,
588        };
589        let document: Document = match window_value.document() {
590            Some(document_instance) => document_instance,
591            None => return,
592        };
593        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
594            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
595                Ok(element) => element,
596                Err(_err) => return,
597            },
598            None => {
599                let created: Element = match document.create_element(STYLE_TAG) {
600                    Ok(element) => element,
601                    Err(_err) => return,
602                };
603                let style_element_from_id: HtmlStyleElement =
604                    match created.dyn_into::<HtmlStyleElement>() {
605                        Ok(element) => element,
606                        Err(_err) => return,
607                    };
608                style_element_from_id.set_id(style_id);
609                if let Some(head) = document.head() {
610                    let _ = head.append_child(&style_element_from_id);
611                }
612                style_element_from_id
613            }
614        };
615        if !css_text.is_empty() {
616            let text_node: Text = document.create_text_node(css_text);
617            let _ = style_element.append_child(&text_node);
618        }
619    }
620
621    /// Builds a CSS style string from an array of key-value pairs.
622    ///
623    /// This function is used by the `html!` macro to convert static `style:`
624    /// attributes into a CSS string without allocating intermediate objects.
625    ///
626    /// # Arguments
627    ///
628    /// - `S: AsRef<str>` - An array of CSS property name-value pairs.
629    ///
630    /// # Returns
631    ///
632    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
633    pub fn style_string<K, V>(props: &[(K, V)]) -> String
634    where
635        K: AsRef<str>,
636        V: AsRef<str>,
637    {
638        props
639            .iter()
640            .map(|(key, value): &(K, V)| {
641                format!(
642                    "{}{CSS_PROP_SEPARATOR}{}{CHAR_CSS_DECL_TERMINATOR}",
643                    key.as_ref(),
644                    value.as_ref()
645                )
646            })
647            .collect::<Vec<String>>()
648            .join(&CHAR_SPACE.to_string())
649    }
650
651    /// Builds a CSS style string from owned key-value pairs.
652    ///
653    /// Used by the `html!` macro for reactive style attributes (with `if`
654    /// conditions) where values are computed at runtime.
655    ///
656    /// # Arguments
657    ///
658    /// - `&[(String, String)]` - An array of owned CSS property name-value pairs.
659    ///
660    /// # Returns
661    ///
662    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
663    pub fn style_string_owned(props: &[(String, String)]) -> String {
664        props
665            .iter()
666            .map(|(key, value): &(String, String)| {
667                format!("{key}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}")
668            })
669            .collect::<Vec<String>>()
670            .join(&CHAR_SPACE.to_string())
671    }
672
673    /// Injects CSS text into the shared `<style>` element in the DOM.
674    ///
675    /// Delegates to [`Css::append_css`] for the actual DOM append.
676    /// Unlike the previous implementation, this does not read the existing
677    /// stylesheet content or perform a full-text `contains` search.
678    ///
679    /// # Arguments
680    ///
681    /// - `S: AsRef<str>` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
682    ///
683    /// # Panics
684    ///
685    /// Panics if `window()` or `document()` is unavailable on the current platform.
686    pub fn inject_css<S>(css_text: S)
687    where
688        S: AsRef<str>,
689    {
690        let css_text: &str = css_text.as_ref();
691        Self::append_css(css_text);
692    }
693}
694
695/// Displays the CSS class name.
696///
697/// This enables `format!("{}", css)` to produce the class name string,
698/// which is required for reactive `if` conditions in `class:` attributes.
699impl Display for Css {
700    /// Formats the CSS class as its name string.
701    ///
702    /// # Arguments
703    ///
704    /// - `&mut Formatter` - The formatter.
705    ///
706    /// # Returns
707    ///
708    /// - `fmt::Result` - The formatting result.
709    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
710        write!(formatter, "{}", self.get_name())
711    }
712}