Skip to main content

euv_core/vdom/attribute/
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    /// Compares two attribute values for visual equality.
11    ///
12    /// # Arguments
13    ///
14    /// - `&Self` - The first attribute value.
15    /// - `&Self` - The second attribute value.
16    ///
17    /// # Returns
18    ///
19    /// - `bool` - `true` if the values are visually equal.
20    fn eq(&self, other: &Self) -> bool {
21        match (self, other) {
22            (AttributeValue::Text(a_val), AttributeValue::Text(b_val)) => a_val == b_val,
23            (AttributeValue::Signal(a_sig), AttributeValue::Signal(b_sig)) => {
24                a_sig.get() == b_sig.get()
25            }
26            (AttributeValue::Signal(a_sig), AttributeValue::Text(b_val)) => a_sig.get() == *b_val,
27            (AttributeValue::Text(a_val), AttributeValue::Signal(b_sig)) => *a_val == b_sig.get(),
28            (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
29            (AttributeValue::Css(a_css), AttributeValue::Css(b_css)) => {
30                a_css.get_name() == b_css.get_name()
31            }
32            (AttributeValue::Dynamic(a_dyn), AttributeValue::Dynamic(b_dyn)) => a_dyn == b_dyn,
33            _ => false,
34        }
35    }
36}
37
38/// Visual equality comparison for attribute entries.
39///
40/// Two attribute entries are equal when their names match and their values
41/// are visually equal as defined by `AttributeValue::eq`.
42impl PartialEq for AttributeEntry {
43    /// Compares two attribute entries for visual equality.
44    ///
45    /// # Arguments
46    ///
47    /// - `&Self` - The first attribute entry.
48    /// - `&Self` - The second attribute entry.
49    ///
50    /// # Returns
51    ///
52    /// - `bool` - `true` if both names and values match.
53    fn eq(&self, other: &Self) -> bool {
54        self.get_name() == other.get_name() && self.get_value() == other.get_value()
55    }
56}
57
58/// Visual equality comparison for CSS classes.
59///
60/// Two CSS classes are considered equal when their class names match,
61/// since the name uniquely identifies the visual style rule.
62impl PartialEq for CssClass {
63    /// Compares two CSS classes by name.
64    ///
65    /// # Arguments
66    ///
67    /// - `&Self` - The first CSS class.
68    /// - `&Self` - The second CSS class.
69    ///
70    /// # Returns
71    ///
72    /// - `bool` - `true` if the class names match.
73    fn eq(&self, other: &Self) -> bool {
74        self.get_name() == other.get_name()
75    }
76}
77
78/// Implementation of style CSS serialization.
79impl Style {
80    /// Adds a style property.
81    ///
82    /// Property names are automatically converted from snake_case to kebab-case
83    /// (e.g., `flex_direction` becomes `flex-direction`).
84    ///
85    /// # Arguments
86    ///
87    /// - `N` - The property name (snake_case will be converted to kebab-case).
88    /// - `V` - The property value.
89    ///
90    /// # Returns
91    ///
92    /// - `Self` - This style with the property added.
93    pub fn property<N, V>(mut self, name: N, value: V) -> Self
94    where
95        N: AsRef<str>,
96        V: AsRef<str>,
97    {
98        self.get_mut_properties().push(StyleProperty::new(
99            name.as_ref().replace('_', "-"),
100            value.as_ref().to_string(),
101        ));
102        self
103    }
104
105    /// Converts the style to a CSS string.
106    ///
107    /// # Returns
108    ///
109    /// - `String` - The CSS string representation.
110    pub fn to_css_string(&self) -> String {
111        self.get_properties()
112            .iter()
113            .map(|style: &StyleProperty| format!("{}: {};", style.get_name(), style.get_value()))
114            .collect::<Vec<String>>()
115            .join(" ")
116    }
117
118    /// Builds a CSS style string from an array of key-value pairs.
119    ///
120    /// This function is used by the `html!` macro to convert static `style:`
121    /// attributes into a CSS string without allocating intermediate `Style`
122    /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
123    /// to kebab-case automatically.
124    ///
125    /// # Arguments
126    ///
127    /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
128    ///
129    /// # Returns
130    ///
131    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
132    pub fn create_style_string(props: &[(&str, &str)]) -> String {
133        let mut result: String = String::new();
134        for (key, value) in props {
135            if !result.is_empty() {
136                result.push(' ');
137            }
138            result.push_str(&key.replace('_', "-"));
139            result.push_str(": ");
140            result.push_str(value);
141            result.push(';');
142        }
143        result
144    }
145}
146
147/// Provides a default empty style.
148impl Default for Style {
149    /// Returns a default `Style` with no properties.
150    ///
151    /// # Returns
152    ///
153    /// - `Self` - An empty style.
154    fn default() -> Self {
155        Self::new(Vec::new())
156    }
157}
158
159/// Implementation of CssClass construction and style injection.
160impl CssClass {
161    /// Creates a new CSS class with the given name and style declarations.
162    ///
163    /// Automatically injects the styles into the DOM upon creation.
164    ///
165    /// # Arguments
166    ///
167    /// - `String` - The class name.
168    /// - `String` - The CSS style declarations.
169    ///
170    /// # Returns
171    ///
172    /// - `Self` - A new CSS class with injected styles.
173    pub fn new(name: String, style: String) -> Self {
174        let mut css_class: CssClass = CssClass::default();
175        css_class.set_name(name);
176        css_class.set_style(style);
177        css_class.inject_style();
178        css_class
179    }
180
181    /// Injects this class's styles into the DOM if not already present.
182    ///
183    /// Creates a `<style>` element with id `euv-css-injected` on first call,
184    /// then appends the class rule. Subsequent calls for the same class name
185    /// are no-ops. On first creation, also injects global CSS keyframes
186    /// required by built-in animations.
187    ///
188    /// # Panics
189    ///
190    /// Panics if `window()` or `document()` is unavailable on the current platform.
191    pub fn inject_style(&self) {
192        #[cfg(target_arch = "wasm32")]
193        {
194            let style_id: &str = "euv-css-injected";
195            let document: Document = window()
196                .expect("no global window exists")
197                .document()
198                .expect("no document exists");
199            let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
200                Some(el) => el.dyn_into::<HtmlStyleElement>().unwrap(),
201                None => {
202                    let el: HtmlStyleElement = document
203                        .create_element("style")
204                        .unwrap()
205                        .dyn_into::<HtmlStyleElement>()
206                        .unwrap();
207                    el.set_id(style_id);
208                    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); } }";
209                    let global: &str = "html, body, #app { height: 100%; margin: 0; padding: 0; overflow: hidden; } * { -webkit-tap-highlight-color: transparent; }";
210                    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; } }";
211                    el.set_inner_text(&format!("{} {} {}", global, keyframes, media_queries));
212                    document.head().unwrap().append_child(&el).unwrap();
213                    el
214                }
215            };
216            let existing_css: String = style_element.inner_text();
217            let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
218            if !existing_css.contains(&class_rule) {
219                let new_css: String = if existing_css.is_empty() {
220                    class_rule
221                } else {
222                    format!("{}\n{}", existing_css, class_rule)
223                };
224                style_element.set_inner_text(&new_css);
225            }
226        }
227    }
228}
229
230/// Displays the CSS class name.
231///
232/// This enables `format!("{}", css_class)` to produce the class name string,
233/// which is required for reactive `if` conditions in `class:` attributes.
234impl std::fmt::Display for CssClass {
235    /// Formats the CSS class as its name string.
236    ///
237    /// # Arguments
238    ///
239    /// - `&mut Formatter` - The formatter.
240    ///
241    /// # Returns
242    ///
243    /// - `std::fmt::Result` - The formatting result.
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        write!(f, "{}", self.get_name())
246    }
247}