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