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(old_val), AttributeValue::Text(new_val)) => old_val == new_val,
23            (AttributeValue::Signal(old_sig), AttributeValue::Signal(new_sig)) => {
24                old_sig.get() == new_sig.get()
25            }
26            (AttributeValue::Signal(old_sig), AttributeValue::Text(new_val)) => {
27                old_sig.get() == *new_val
28            }
29            (AttributeValue::Text(old_val), AttributeValue::Signal(new_sig)) => {
30                *old_val == new_sig.get()
31            }
32            (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
33            (AttributeValue::Css(old_css), AttributeValue::Css(new_css)) => {
34                old_css.get_name() == new_css.get_name()
35            }
36            (AttributeValue::Dynamic(old_dyn), AttributeValue::Dynamic(new_dyn)) => {
37                old_dyn == new_dyn
38            }
39            _ => false,
40        }
41    }
42}
43
44/// Visual equality comparison for attribute entries.
45///
46/// Two attribute entries are equal when their names match and their values
47/// are visually equal as defined by `AttributeValue::eq`.
48impl PartialEq for AttributeEntry {
49    /// Compares two attribute entries for visual equality.
50    ///
51    /// # Arguments
52    ///
53    /// - `&Self` - The first attribute entry.
54    /// - `&Self` - The second attribute entry.
55    ///
56    /// # Returns
57    ///
58    /// - `bool` - `true` if both names and values match.
59    fn eq(&self, other: &Self) -> bool {
60        self.get_name() == other.get_name() && self.get_value() == other.get_value()
61    }
62}
63
64/// Visual equality comparison for CSS classes.
65///
66/// Two CSS classes are considered equal when their class names match,
67/// since the name uniquely identifies the visual style rule.
68impl PartialEq for CssClass {
69    /// Compares two CSS classes by name.
70    ///
71    /// # Arguments
72    ///
73    /// - `&Self` - The first CSS class.
74    /// - `&Self` - The second CSS class.
75    ///
76    /// # Returns
77    ///
78    /// - `bool` - `true` if the class names match.
79    fn eq(&self, other: &Self) -> bool {
80        self.get_name() == other.get_name()
81    }
82}
83
84/// Implementation of style CSS serialization.
85impl Style {
86    /// Adds a style property.
87    ///
88    /// Property names are automatically converted from snake_case to kebab-case
89    /// (e.g., `flex_direction` becomes `flex-direction`).
90    ///
91    /// # Arguments
92    ///
93    /// - `N` - The property name (snake_case will be converted to kebab-case).
94    /// - `V` - The property value.
95    ///
96    /// # Returns
97    ///
98    /// - `Self` - This style with the property added.
99    pub fn property<N, V>(mut self, name: N, value: V) -> Self
100    where
101        N: AsRef<str>,
102        V: AsRef<str>,
103    {
104        self.get_mut_properties().push(StyleProperty::new(
105            name.as_ref().replace('_', "-"),
106            value.as_ref().to_string(),
107        ));
108        self
109    }
110
111    /// Converts the style to a CSS string.
112    ///
113    /// # Returns
114    ///
115    /// - `String` - The CSS string representation.
116    pub fn to_css_string(&self) -> String {
117        self.get_properties()
118            .iter()
119            .map(|style: &StyleProperty| format!("{}: {};", style.get_name(), style.get_value()))
120            .collect::<Vec<String>>()
121            .join(" ")
122    }
123
124    /// Builds a CSS style string from an array of key-value pairs.
125    ///
126    /// This function is used by the `html!` macro to convert static `style:`
127    /// attributes into a CSS string without allocating intermediate `Style`
128    /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
129    /// to kebab-case automatically.
130    ///
131    /// # Arguments
132    ///
133    /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
134    ///
135    /// # Returns
136    ///
137    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
138    pub fn create_style_string(props: &[(&str, &str)]) -> String {
139        let mut result: String = String::new();
140        for (key, value) in props {
141            if !result.is_empty() {
142                result.push(' ');
143            }
144            result.push_str(&key.replace('_', "-"));
145            result.push_str(": ");
146            result.push_str(value);
147            result.push(';');
148        }
149        result
150    }
151}
152
153/// Provides a default empty style.
154impl Default for Style {
155    /// Returns a default `Style` with no properties.
156    ///
157    /// # Returns
158    ///
159    /// - `Self` - An empty style.
160    fn default() -> Self {
161        Self::new(Vec::new())
162    }
163}
164
165/// Implementation of CssClass construction and style injection.
166impl CssClass {
167    /// Creates a new CSS class with the given name and style declarations.
168    ///
169    /// Automatically injects the styles into the DOM upon creation.
170    ///
171    /// # Arguments
172    ///
173    /// - `String` - The class name.
174    /// - `String` - The CSS style declarations.
175    ///
176    /// # Returns
177    ///
178    /// - `Self` - A new CSS class with injected styles.
179    pub fn new(name: String, style: String) -> Self {
180        let mut css_class: CssClass = CssClass::default();
181        css_class.set_name(name);
182        css_class.set_style(style);
183        css_class.inject_style();
184        css_class
185    }
186
187    /// Creates a new CSS class with the given name, style declarations, and pseudo rules.
188    ///
189    /// Automatically injects the base styles, pseudo-class/pseudo-element rules,
190    /// and media query rules into the DOM upon creation.
191    ///
192    /// # Arguments
193    ///
194    /// - `String` - The class name.
195    /// - `String` - The CSS style declarations.
196    /// - `Vec<PseudoRule>` - The pseudo-class and pseudo-element rules.
197    /// - `Vec<MediaRule>` - The media query rules.
198    ///
199    /// # Returns
200    ///
201    /// - `Self` - A new CSS class with injected styles and pseudo rules.
202    pub fn new_with_rules(
203        name: String,
204        style: String,
205        pseudo_rules: Vec<PseudoRule>,
206        media_rules: Vec<MediaRule>,
207    ) -> Self {
208        let mut css_class: CssClass = CssClass::default();
209        css_class.set_name(name);
210        css_class.set_style(style);
211        css_class.set_pseudo_rules(pseudo_rules);
212        css_class.set_media_rules(media_rules);
213        css_class.inject_style();
214        css_class
215    }
216
217    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
218    ///
219    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
220    /// This is used by the `class!` macro for fully static class definitions
221    /// where pseudo rules can be computed at compile time.
222    ///
223    /// # Arguments
224    ///
225    /// - `&str` - The serialized pseudo rules string.
226    ///
227    /// # Returns
228    ///
229    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
230    pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
231        let mut rules: Vec<PseudoRule> = Vec::new();
232        let mut remaining: &str = input;
233        while !remaining.is_empty() {
234            let selector_end: Option<usize> = remaining.find(" { ");
235            let Some(sel_end) = selector_end else {
236                break;
237            };
238            let selector: &str = &remaining[..sel_end];
239            let after_selector: &str = remaining[sel_end..].strip_prefix(" { ").unwrap_or("");
240            let style_end: Option<usize> = after_selector.find('}');
241            let Some(st_end) = style_end else {
242                break;
243            };
244            let style: &str = &after_selector[..st_end];
245            if !selector.is_empty() && !style.is_empty() {
246                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
247            }
248            remaining = after_selector[st_end..].strip_prefix('}').unwrap_or("");
249        }
250        rules
251    }
252
253    /// Parses media query rules from a compact serialization string.
254    ///
255    /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
256    /// This is used by the `class!` macro for fully static class definitions
257    /// where media rules can be computed at compile time.
258    ///
259    /// # Arguments
260    ///
261    /// - `&str` - The serialized media rules string.
262    ///
263    /// # Returns
264    ///
265    /// - `Vec<MediaRule>` - The parsed media rules.
266    pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
267        let mut rules: Vec<MediaRule> = Vec::new();
268        let mut remaining: &str = input;
269        while !remaining.is_empty() {
270            if !remaining.starts_with("@media ") {
271                break;
272            }
273            let after_prefix: &str = remaining.strip_prefix("@media ").unwrap_or("");
274            let query_end: Option<usize> = after_prefix.find(" { ");
275            let Some(q_end) = query_end else {
276                break;
277            };
278            let query: &str = &after_prefix[..q_end];
279            let after_query: &str = after_prefix[q_end..].strip_prefix(" { ").unwrap_or("");
280            let style_end: Option<usize> = after_query.find('}');
281            let Some(st_end) = style_end else {
282                break;
283            };
284            let style: &str = &after_query[..st_end];
285            if !query.is_empty() && !style.is_empty() {
286                rules.push(MediaRule::new(query.to_string(), style.to_string()));
287            }
288            remaining = after_query[st_end..].strip_prefix('}').unwrap_or("");
289        }
290        rules
291    }
292
293    /// Injects this class's styles into the DOM if not already present.
294    ///
295    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
296    /// then delegates to [`CssClass::inject_css`] for DOM injection. Subsequent
297    /// calls for the same class name are no-ops.
298    ///
299    /// # Panics
300    ///
301    /// Panics if `window()` or `document()` is unavailable on the current platform.
302    pub fn inject_style(&self) {
303        let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
304        let mut css: String = class_rule;
305        for pseudo_rule in self.get_pseudo_rules() {
306            if !pseudo_rule.get_style().is_empty() {
307                let pseudo_rule_str: String = format!(
308                    ".{}{} {{ {} }}",
309                    self.get_name(),
310                    pseudo_rule.get_selector(),
311                    pseudo_rule.get_style()
312                );
313                css = format!("{}\n{}", css, pseudo_rule_str);
314            }
315        }
316        for media_rule in self.get_media_rules() {
317            if !media_rule.get_query().is_empty() {
318                let media_rule_str: String = format!(
319                    "@media {} {{ .{} {{ {} }} }}",
320                    media_rule.get_query(),
321                    self.get_name(),
322                    media_rule.get_style()
323                );
324                css = format!("{}\n{}", css, media_rule_str);
325            }
326        }
327        Self::inject_css(&css);
328    }
329
330    /// Injects CSS text into the shared `<style>` element in the DOM.
331    ///
332    /// Creates a `<style>` element with id `euv-css-injected` on first call,
333    /// then appends the provided `css` string if it is not already present.
334    /// Subsequent calls with identical CSS text are no-ops. Call this during
335    /// application initialisation to register global reset styles, keyframes,
336    /// media queries, or any other CSS rules.
337    ///
338    /// # Arguments
339    ///
340    /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
341    ///
342    /// # Panics
343    ///
344    /// Panics if `window()` or `document()` is unavailable on the current platform.
345    pub fn inject_css(css: &str) {
346        let _ = css;
347        #[cfg(target_arch = "wasm32")]
348        {
349            let style_id: &str = "euv-css-injected";
350            let document: Document = window()
351                .expect("no global window exists")
352                .document()
353                .expect("no document exists");
354            let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
355                Some(el) => el.dyn_into::<HtmlStyleElement>().unwrap(),
356                None => {
357                    let el: HtmlStyleElement = document
358                        .create_element("style")
359                        .unwrap()
360                        .dyn_into::<HtmlStyleElement>()
361                        .unwrap();
362                    el.set_id(style_id);
363                    document.head().unwrap().append_child(&el).unwrap();
364                    el
365                }
366            };
367            let existing_css: String = style_element.inner_text();
368            if !css.is_empty() && !existing_css.contains(css) {
369                let new_css: String = if existing_css.is_empty() {
370                    css.to_string()
371                } else {
372                    format!("{}\n{}", existing_css, css)
373                };
374                style_element.set_inner_text(&new_css);
375            }
376        }
377    }
378}
379
380/// Displays the CSS class name.
381///
382/// This enables `format!("{}", css_class)` to produce the class name string,
383/// which is required for reactive `if` conditions in `class:` attributes.
384impl std::fmt::Display for CssClass {
385    /// Formats the CSS class as its name string.
386    ///
387    /// # Arguments
388    ///
389    /// - `&mut Formatter` - The formatter.
390    ///
391    /// # Returns
392    ///
393    /// - `std::fmt::Result` - The formatting result.
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        write!(f, "{}", self.get_name())
396    }
397}