Skip to main content

euv_core/vdom/
impl.rs

1use crate::*;
2
3/// Visual equality comparison for attribute values.
4///
5/// Compares values by their visual output rather than identity. `Signal`
6/// values are compared by their current resolved string, `Event` values
7/// are always considered equal (re-binding is handled by the handler
8/// registry), and `CssClass` values are compared by class name.
9impl PartialEq for AttributeValue {
10    fn eq(&self, other: &Self) -> bool {
11        match (self, other) {
12            (AttributeValue::Text(a_val), AttributeValue::Text(b_val)) => a_val == b_val,
13            (AttributeValue::Signal(a_sig), AttributeValue::Signal(b_sig)) => {
14                a_sig.get() == b_sig.get()
15            }
16            (AttributeValue::Signal(a_sig), AttributeValue::Text(b_val)) => a_sig.get() == *b_val,
17            (AttributeValue::Text(a_val), AttributeValue::Signal(b_sig)) => *a_val == b_sig.get(),
18            (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
19            (AttributeValue::Css(a_css), AttributeValue::Css(b_css)) => {
20                a_css.get_name() == b_css.get_name()
21            }
22            (AttributeValue::Dynamic(a_dyn), AttributeValue::Dynamic(b_dyn)) => a_dyn == b_dyn,
23            _ => false,
24        }
25    }
26}
27
28/// Visual equality comparison for attribute entries.
29///
30/// Two attribute entries are equal when their names match and their values
31/// are visually equal as defined by `AttributeValue::eq`.
32impl PartialEq for AttributeEntry {
33    fn eq(&self, other: &Self) -> bool {
34        self.get_name() == other.get_name() && self.get_value() == other.get_value()
35    }
36}
37
38/// Visual equality comparison for text nodes.
39///
40/// Only compares the text content; the backing signal is not considered
41/// because it does not affect visual output.
42impl PartialEq for TextNode {
43    fn eq(&self, other: &Self) -> bool {
44        self.get_content() == other.get_content()
45    }
46}
47
48/// Visual equality comparison for CSS classes.
49///
50/// Two CSS classes are considered equal when their class names match,
51/// since the name uniquely identifies the visual style rule.
52impl PartialEq for CssClass {
53    fn eq(&self, other: &Self) -> bool {
54        self.get_name() == other.get_name()
55    }
56}
57
58/// Visual equality comparison for virtual DOM nodes.
59///
60/// Used by DynamicNode re-rendering to skip unnecessary DOM patches when
61/// the rendered output has not changed. Event attributes are always
62/// considered equal because re-binding event listeners is handled
63/// separately by the handler registry and does not affect visual output.
64/// Dynamic nodes manage their own subtree re-rendering, so two Dynamic
65/// variants are always considered equal — the inner renderer handles
66/// patching when the dynamic content actually changes.
67impl PartialEq for VirtualNode {
68    fn eq(&self, other: &Self) -> bool {
69        match (self, other) {
70            (VirtualNode::Text(a_text), VirtualNode::Text(b_text)) => a_text == b_text,
71            (
72                VirtualNode::Element {
73                    tag: a_tag,
74                    attributes: a_attrs,
75                    children: a_children,
76                    ..
77                },
78                VirtualNode::Element {
79                    tag: b_tag,
80                    attributes: b_attrs,
81                    children: b_children,
82                    ..
83                },
84            ) => {
85                a_tag == b_tag
86                    && a_attrs.len() == b_attrs.len()
87                    && a_attrs.iter().zip(b_attrs.iter()).all(|(a, b)| a == b)
88                    && a_children.len() == b_children.len()
89                    && a_children
90                        .iter()
91                        .zip(b_children.iter())
92                        .all(|(a, b)| a == b)
93            }
94            (VirtualNode::Fragment(a_children), VirtualNode::Fragment(b_children)) => {
95                a_children.len() == b_children.len()
96                    && a_children
97                        .iter()
98                        .zip(b_children.iter())
99                        .all(|(a, b)| a == b)
100            }
101            (VirtualNode::Dynamic(_), VirtualNode::Dynamic(_)) => true,
102            (VirtualNode::Empty, VirtualNode::Empty) => true,
103            _ => false,
104        }
105    }
106}
107
108/// Maps each `Attribute` variant to its corresponding DOM attribute string.
109impl Attribute {
110    /// Returns the string representation of this attribute name for DOM binding.
111    pub fn as_str(&self) -> String {
112        match self {
113            Attribute::AccessKey => "accesskey".to_string(),
114            Attribute::Action => "action".to_string(),
115            Attribute::Alt => "alt".to_string(),
116            Attribute::AriaLabel => "aria-label".to_string(),
117            Attribute::AutoComplete => "autocomplete".to_string(),
118            Attribute::AutoFocus => "autofocus".to_string(),
119            Attribute::Checked => "checked".to_string(),
120            Attribute::Class => "class".to_string(),
121            Attribute::Cols => "cols".to_string(),
122            Attribute::ContentEditable => "contenteditable".to_string(),
123            Attribute::Data(name) => format!("data-{}", name),
124            Attribute::Dir => "dir".to_string(),
125            Attribute::Disabled => "disabled".to_string(),
126            Attribute::Draggable => "draggable".to_string(),
127            Attribute::EncType => "enctype".to_string(),
128            Attribute::For => "for".to_string(),
129            Attribute::Form => "form".to_string(),
130            Attribute::Height => "height".to_string(),
131            Attribute::Hidden => "hidden".to_string(),
132            Attribute::Href => "href".to_string(),
133            Attribute::Id => "id".to_string(),
134            Attribute::Lang => "lang".to_string(),
135            Attribute::Max => "max".to_string(),
136            Attribute::MaxLength => "maxlength".to_string(),
137            Attribute::Method => "method".to_string(),
138            Attribute::Min => "min".to_string(),
139            Attribute::MinLength => "minlength".to_string(),
140            Attribute::Multiple => "multiple".to_string(),
141            Attribute::Name => "name".to_string(),
142            Attribute::Pattern => "pattern".to_string(),
143            Attribute::Placeholder => "placeholder".to_string(),
144            Attribute::ReadOnly => "readonly".to_string(),
145            Attribute::Required => "required".to_string(),
146            Attribute::Rows => "rows".to_string(),
147            Attribute::Selected => "selected".to_string(),
148            Attribute::Size => "size".to_string(),
149            Attribute::SpellCheck => "spellcheck".to_string(),
150            Attribute::Src => "src".to_string(),
151            Attribute::Step => "step".to_string(),
152            Attribute::Style => "style".to_string(),
153            Attribute::TabIndex => "tabindex".to_string(),
154            Attribute::Target => "target".to_string(),
155            Attribute::Title => "title".to_string(),
156            Attribute::Type => "type".to_string(),
157            Attribute::Value => "value".to_string(),
158            Attribute::Width => "width".to_string(),
159            Attribute::Other(name) => name.clone(),
160        }
161    }
162}
163
164/// Provides a default empty dynamic node with a no-op render function.
165impl Default for DynamicNode {
166    fn default() -> Self {
167        let node: DynamicNode = DynamicNode {
168            render_fn: Rc::new(RefCell::new(|| VirtualNode::Empty)),
169            hook_context: HookContext::default(),
170            id: 0_u64,
171        };
172        node
173    }
174}
175
176/// Clones a `DynamicNode` by cloning its `HookContext` (Copy) and `render_fn` (Rc).
177impl Clone for DynamicNode {
178    fn clone(&self) -> Self {
179        DynamicNode {
180            render_fn: Rc::clone(self.get_render_fn()),
181            hook_context: self.hook_context,
182            id: self.id,
183        }
184    }
185}
186
187/// Converts a `VirtualNode` reference into an owned node.
188impl AsNode for VirtualNode {
189    fn as_node(&self) -> Option<VirtualNode> {
190        Some(self.clone())
191    }
192}
193
194/// Converts a `VirtualNode` reference into an owned node.
195impl AsNode for &VirtualNode {
196    fn as_node(&self) -> Option<VirtualNode> {
197        Some((*self).clone())
198    }
199}
200
201/// Converts a `String` into a text virtual node.
202impl AsNode for String {
203    fn as_node(&self) -> Option<VirtualNode> {
204        Some(VirtualNode::Text(TextNode::new(self.clone(), None)))
205    }
206}
207
208/// Converts a string slice into a text virtual node.
209impl AsNode for &str {
210    fn as_node(&self) -> Option<VirtualNode> {
211        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
212    }
213}
214
215/// Converts an `i32` into a text virtual node.
216impl AsNode for i32 {
217    fn as_node(&self) -> Option<VirtualNode> {
218        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
219    }
220}
221
222/// Converts an `i64` into a text virtual node.
223impl AsNode for i64 {
224    fn as_node(&self) -> Option<VirtualNode> {
225        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
226    }
227}
228
229/// Converts a `usize` into a text virtual node.
230impl AsNode for usize {
231    fn as_node(&self) -> Option<VirtualNode> {
232        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
233    }
234}
235
236/// Converts an `f32` into a text virtual node.
237impl AsNode for f32 {
238    fn as_node(&self) -> Option<VirtualNode> {
239        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
240    }
241}
242
243/// Converts an `f64` into a text virtual node.
244impl AsNode for f64 {
245    fn as_node(&self) -> Option<VirtualNode> {
246        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
247    }
248}
249
250/// Converts a `bool` into a text virtual node.
251impl AsNode for bool {
252    fn as_node(&self) -> Option<VirtualNode> {
253        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
254    }
255}
256
257/// Converts a signal into a reactive text virtual node.
258impl<T> AsNode for Signal<T>
259where
260    T: Clone + PartialEq + std::fmt::Display + 'static,
261{
262    fn as_node(&self) -> Option<VirtualNode> {
263        Some(self.as_reactive_text())
264    }
265}
266
267/// Converts a `VirtualNode` into itself via `IntoNode`.
268impl IntoNode for VirtualNode {
269    fn into_node(self) -> VirtualNode {
270        self
271    }
272}
273
274/// Wraps a `FnMut() -> VirtualNode` closure into a `DynamicNode` via `IntoNode`.
275///
276/// This enables writing `{move || html! { ... }}` directly in HTML markup
277/// without explicit `DynamicNode` construction.
278impl<F> IntoNode for F
279where
280    F: FnMut() -> VirtualNode + 'static,
281{
282    fn into_node(self) -> VirtualNode {
283        static NEXT_DYNAMIC_ID: std::sync::atomic::AtomicU64 =
284            std::sync::atomic::AtomicU64::new(1_u64);
285        let id: u64 = NEXT_DYNAMIC_ID.fetch_add(1_u64, std::sync::atomic::Ordering::Relaxed);
286        VirtualNode::Dynamic(DynamicNode {
287            render_fn: Rc::new(RefCell::new(self)),
288            hook_context: crate::reactive::create_hook_context(),
289            id,
290        })
291    }
292}
293
294/// Converts a `String` into a text virtual node via `IntoNode`.
295impl IntoNode for String {
296    fn into_node(self) -> VirtualNode {
297        VirtualNode::Text(TextNode::new(self, None))
298    }
299}
300
301/// Converts a `&str` into a text virtual node via `IntoNode`.
302impl IntoNode for &str {
303    fn into_node(self) -> VirtualNode {
304        VirtualNode::Text(TextNode::new(self.to_string(), None))
305    }
306}
307
308/// Converts an `i32` into a text virtual node via `IntoNode`.
309impl IntoNode for i32 {
310    fn into_node(self) -> VirtualNode {
311        VirtualNode::Text(TextNode::new(self.to_string(), None))
312    }
313}
314
315/// Converts a `usize` into a text virtual node via `IntoNode`.
316impl IntoNode for usize {
317    fn into_node(self) -> VirtualNode {
318        VirtualNode::Text(TextNode::new(self.to_string(), None))
319    }
320}
321
322/// Converts a `bool` into a text virtual node via `IntoNode`.
323impl IntoNode for bool {
324    fn into_node(self) -> VirtualNode {
325        VirtualNode::Text(TextNode::new(self.to_string(), None))
326    }
327}
328
329/// Converts a signal into a reactive text virtual node via `IntoNode`.
330impl<T> IntoNode for Signal<T>
331where
332    T: Clone + PartialEq + std::fmt::Display + 'static,
333{
334    fn into_node(self) -> VirtualNode {
335        self.as_reactive_text()
336    }
337}
338
339/// Implementation of virtual node construction and property extraction.
340impl VirtualNode {
341    /// Determines whether the DOM needs to be patched when transitioning
342    /// from `old` to `new`.
343    ///
344    /// Unlike `PartialEq`, this method treats two `Dynamic` variants as
345    /// **different** so that the renderer always re-evaluates dynamic
346    /// subtrees. This is essential for route-based `match` expressions
347    /// where different pages may occupy the same DynamicNode slot.
348    pub fn needs_patch(old: &VirtualNode, new: &VirtualNode) -> bool {
349        match (old, new) {
350            (VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
351                old_text.get_content() != new_text.get_content()
352            }
353            (
354                VirtualNode::Element {
355                    tag: old_tag,
356                    attributes: old_attrs,
357                    children: old_children,
358                    key: _old_key,
359                },
360                VirtualNode::Element {
361                    tag: new_tag,
362                    attributes: new_attrs,
363                    children: new_children,
364                    key: _new_key,
365                },
366            ) => {
367                if old_tag != new_tag {
368                    return true;
369                }
370                if old_attrs.len() != new_attrs.len() {
371                    return true;
372                }
373                for (old_attr, new_attr) in old_attrs.iter().zip(new_attrs.iter()) {
374                    if old_attr.get_name() != new_attr.get_name()
375                        || old_attr.get_value() != new_attr.get_value()
376                    {
377                        return true;
378                    }
379                }
380                if old_children.len() != new_children.len() {
381                    return true;
382                }
383                for (old_child, new_child) in old_children.iter().zip(new_children.iter()) {
384                    if Self::needs_patch(old_child, new_child) {
385                        return true;
386                    }
387                }
388                false
389            }
390            (VirtualNode::Fragment(old_children), VirtualNode::Fragment(new_children)) => {
391                if old_children.len() != new_children.len() {
392                    return true;
393                }
394                for (old_child, new_child) in old_children.iter().zip(new_children.iter()) {
395                    if Self::needs_patch(old_child, new_child) {
396                        return true;
397                    }
398                }
399                false
400            }
401            (VirtualNode::Dynamic(old_dyn), VirtualNode::Dynamic(new_dyn)) => {
402                old_dyn.get_id() != new_dyn.get_id()
403            }
404            (VirtualNode::Empty, VirtualNode::Empty) => false,
405            _ => true,
406        }
407    }
408
409    /// Creates a new element node with the given tag name.
410    pub fn get_element_node(tag_name: &str) -> Self {
411        VirtualNode::Element {
412            tag: Tag::Element(tag_name.to_string()),
413            attributes: Vec::new(),
414            children: Vec::new(),
415            key: None,
416        }
417    }
418
419    /// Creates a new text node with the given content.
420    pub fn get_text_node(content: &str) -> Self {
421        VirtualNode::Text(TextNode::new(content.to_string(), None))
422    }
423
424    /// Adds an attribute to this node if it is an element.
425    pub fn with_attribute(mut self, name: &str, value: AttributeValue) -> Self {
426        if let VirtualNode::Element {
427            ref mut attributes, ..
428        } = self
429        {
430            attributes.push(AttributeEntry::new(name.to_string(), value));
431        }
432        self
433    }
434
435    /// Adds a child node to this node if it is an element.
436    pub fn with_child(mut self, child: VirtualNode) -> Self {
437        if let VirtualNode::Element {
438            ref mut children, ..
439        } = self
440        {
441            children.push(child);
442        }
443        self
444    }
445
446    /// Returns true if this node is a component node.
447    pub fn is_component(&self) -> bool {
448        matches!(
449            self,
450            VirtualNode::Element {
451                tag: Tag::Component(_),
452                ..
453            }
454        )
455    }
456
457    /// Returns the tag name if this is an element or component node.
458    pub fn tag_name(&self) -> Option<String> {
459        match self {
460            VirtualNode::Element { tag, .. } => match tag {
461                Tag::Element(name) => Some(name.clone()),
462                Tag::Component(name) => Some(name.clone()),
463            },
464            _ => None,
465        }
466    }
467
468    /// Extracts a string property from this node if it is an element with the named attribute.
469    pub fn try_get_prop(&self, name: &Attribute) -> Option<String> {
470        let name_str: String = name.as_str();
471        if let VirtualNode::Element { attributes, .. } = self {
472            for attr in attributes {
473                if attr.get_name() == &name_str {
474                    match attr.get_value() {
475                        AttributeValue::Text(value) => return Some(value.clone()),
476                        AttributeValue::Signal(signal) => return Some(signal.get()),
477                        _ => {}
478                    }
479                }
480            }
481        }
482        None
483    }
484
485    /// Extracts a signal property from this node if it is an element with the named attribute.
486    ///
487    /// Returns the raw `Signal<String>` so components can reactively read the current value
488    /// and subscribe to future changes, rather than receiving a snapshot string.
489    pub fn try_get_signal_prop(&self, name: &Attribute) -> Option<Signal<String>> {
490        let name_str: String = name.as_str();
491        if let VirtualNode::Element { attributes, .. } = self {
492            for attr in attributes {
493                if attr.get_name() == &name_str
494                    && let AttributeValue::Signal(signal) = attr.get_value()
495                {
496                    return Some(*signal);
497                }
498            }
499        }
500        None
501    }
502
503    /// Extracts children from this node if it is an element.
504    pub fn get_children(&self) -> Vec<VirtualNode> {
505        if let VirtualNode::Element { children, .. } = self {
506            children.clone()
507        } else {
508            Vec::new()
509        }
510    }
511
512    /// Extracts text content from this node.
513    pub fn try_get_text(&self) -> Option<String> {
514        match self {
515            VirtualNode::Text(text_node) => Some(text_node.get_content().clone()),
516            VirtualNode::Element { children, .. } => {
517                children.first().and_then(VirtualNode::try_get_text)
518            }
519            _ => None,
520        }
521    }
522
523    /// Extracts an event handler from this node if it is an element with the named event attribute.
524    pub fn try_get_event(
525        &self,
526        name: &NativeEventName,
527    ) -> Option<crate::event::NativeEventHandler> {
528        let name_str: String = name.as_str();
529        if let VirtualNode::Element { attributes, .. } = self {
530            for attr in attributes {
531                if attr.get_name() == &name_str
532                    && let AttributeValue::Event(handler) = attr.get_value()
533                {
534                    return Some(handler.clone());
535                }
536            }
537        }
538        None
539    }
540
541    /// Extracts an event handler from this node by a custom attribute name.
542    pub fn try_get_callback(&self, name: &str) -> Option<crate::event::NativeEventHandler> {
543        if let VirtualNode::Element { attributes, .. } = self {
544            for attr in attributes {
545                if attr.get_name() == name
546                    && let AttributeValue::Event(handler) = attr.get_value()
547                {
548                    return Some(handler.clone());
549                }
550            }
551        }
552        None
553    }
554}
555
556/// Converts a signal into a reactive text node with listener wiring.
557impl<T> AsReactiveText for Signal<T>
558where
559    T: Clone + PartialEq + std::fmt::Display + 'static,
560{
561    fn as_reactive_text(&self) -> VirtualNode {
562        let signal: Signal<T> = *self;
563        let initial: String = signal.get().to_string();
564        let string_signal: Signal<String> = {
565            let boxed: Box<SignalInner<String>> = Box::new(SignalInner::new(initial.clone()));
566            Signal::from_inner(Box::leak(boxed) as *mut SignalInner<String>)
567        };
568        let source_signal: Signal<T> = *self;
569        let string_signal_clone: Signal<String> = string_signal;
570        source_signal.subscribe({
571            let source_signal: Signal<T> = source_signal;
572            move || {
573                let new_value: String = source_signal.get().to_string();
574                string_signal_clone.set(new_value);
575            }
576        });
577        VirtualNode::Text(TextNode::new(initial, Some(string_signal)))
578    }
579}
580
581/// Implementation of style CSS serialization.
582impl Style {
583    /// Adds a style property.
584    ///
585    /// Property names are automatically converted from snake_case to kebab-case
586    /// (e.g., `flex_direction` becomes `flex-direction`).
587    pub fn property<N, V>(mut self, name: N, value: V) -> Self
588    where
589        N: AsRef<str>,
590        V: AsRef<str>,
591    {
592        self.get_mut_properties().push(StyleProperty::new(
593            name.as_ref().replace('_', "-"),
594            value.as_ref().to_string(),
595        ));
596        self
597    }
598
599    /// Converts the style to a CSS string.
600    pub fn to_css_string(&self) -> String {
601        self.get_properties()
602            .iter()
603            .map(|p| format!("{}: {};", p.get_name(), p.get_value()))
604            .collect::<Vec<String>>()
605            .join(" ")
606    }
607}
608
609/// Provides a default empty style.
610impl Default for Style {
611    fn default() -> Self {
612        Self::new(Vec::new())
613    }
614}
615
616/// Implementation of CssClass construction and style injection.
617impl CssClass {
618    /// Creates a new CSS class with the given name and style declarations.
619    ///
620    /// Automatically injects the styles into the DOM upon creation.
621    pub fn new(name: String, style: String) -> Self {
622        let mut css_class: CssClass = CssClass::default();
623        css_class.set_name(name);
624        css_class.set_style(style);
625        css_class.inject_style();
626        css_class
627    }
628
629    /// Injects this class's styles into the DOM if not already present.
630    ///
631    /// Creates a `<style>` element with id `euv-css-injected` on first call,
632    /// then appends the class rule. Subsequent calls for the same class name
633    /// are no-ops. On first creation, also injects global CSS keyframes
634    /// required by built-in animations.
635    pub fn inject_style(&self) {
636        #[cfg(target_arch = "wasm32")]
637        {
638            let style_id: &str = "euv-css-injected";
639            let document: web_sys::Document = web_sys::window()
640                .expect("no global window exists")
641                .document()
642                .expect("no document exists");
643            let style_element: web_sys::HtmlStyleElement = match document
644                .get_element_by_id(style_id)
645            {
646                Some(el) => el.dyn_into::<web_sys::HtmlStyleElement>().unwrap(),
647                None => {
648                    let el: web_sys::HtmlStyleElement = document
649                        .create_element("style")
650                        .unwrap()
651                        .dyn_into::<web_sys::HtmlStyleElement>()
652                        .unwrap();
653                    el.set_id(style_id);
654                    let keyframes: &str = "@keyframes euv-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes euv-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes euv-scale-in { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes euv-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } } @keyframes euv-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } } @keyframes euv-slide-left { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes euv-fade-in-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }";
655                    let global: &str = "html, body, #app { height: 100%; margin: 0; padding: 0; overflow: hidden; } * { -webkit-tap-highlight-color: transparent; }";
656                    let media_queries: &str = "@media (max-width: 767px) { .c_app_nav { display: none; } .c_app_main { padding: 20px 16px; max-width: 100%; } .c_page_title { font-size: 22px; } .c_page_subtitle { font-size: 14px; } .c_card { padding: 16px; margin: 12px 0; border-radius: 10px; } .c_card_title { font-size: 16px; } .c_form_grid { grid-template-columns: 1fr; } .c_browser_api_row { grid-template-columns: 1fr; } .c_modal_content { max-width: 100%; width: calc(100% - 32px); border-radius: 16px 16px 0 0; position: fixed; bottom: 0; left: 16px; height: 80vh; animation: euv-slide-up 0.25s ease; } .c_modal_overlay { align-items: flex-end; } .c_event_stats { gap: 12px; flex-wrap: wrap; } .c_event_section_row { gap: 12px; flex-wrap: wrap; } .c_event_section_col { min-width: 100%; } .c_counter_value { font-size: 20px; } .c_timer_value { font-size: 36px; } .c_not_found_code { font-size: 56px; } .c_not_found_container { padding: 40px 20px; } .c_list_input_row { flex-direction: column; } .c_vconsole_button { bottom: 16px; right: 16px; width: 44px; height: 44px; border-radius: 12px; } .c_tab_bar { flex-wrap: wrap; } .c_primary_button { padding: 10px 18px; font-size: 14px; } .c_badge { padding: 4px 10px; font-size: 11px; } .c_badge_outline { padding: 4px 10px; font-size: 11px; } .c_browser_info_grid { grid-template-columns: 1fr; } .c_anim_spin { font-size: 36px; } .c_anim_spin_stopped { font-size: 36px; } .c_anim_pulse { font-size: 36px; } .c_anim_pulse_stopped { font-size: 36px; } }";
657                    el.set_inner_text(&format!("{} {} {}", global, keyframes, media_queries));
658                    document.head().unwrap().append_child(&el).unwrap();
659                    el
660                }
661            };
662            let existing_css: String = style_element.inner_text();
663            let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
664            if !existing_css.contains(&class_rule) {
665                let new_css: String = if existing_css.is_empty() {
666                    class_rule
667                } else {
668                    format!("{}\n{}", existing_css, class_rule)
669                };
670                style_element.set_inner_text(&new_css);
671            }
672        }
673    }
674}
675
676/// Displays the CSS class name.
677///
678/// This enables `format!("{}", css_class)` to produce the class name string,
679/// which is required for reactive `if` conditions in `class:` attributes.
680impl std::fmt::Display for CssClass {
681    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682        write!(f, "{}", self.get_name())
683    }
684}