euv-core 0.3.29

A declarative, cross-platform UI framework for Rust with virtual DOM, reactive signals, and HTML macros for WebAssembly.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
use crate::*;

/// SAFETY: `InjectedClassesCell` is only used in single-threaded WASM contexts.
unsafe impl Sync for InjectedClassesCell {}

/// Visual equality comparison for attribute values.
///
/// Compares values by their visual output rather than identity. `Signal`
/// values are compared by their current resolved string; when both signals
/// share the same inner pointer, they are always considered **unequal**
/// because the signal may have mutated between VDOM snapshots and `.get()`
/// would return the same current value for both, masking the change.
/// `Event` values are always considered equal (re-binding is handled by the
/// handler registry), and `CssClass` values are compared by class name.
impl PartialEq for AttributeValue {
    /// Compares two attribute values for visual equality.
    ///
    /// # Arguments
    ///
    /// - `&Self` - The first attribute value.
    /// - `&Self` - The second attribute value.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if the values are visually equal.
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (AttributeValue::Text(old_val), AttributeValue::Text(new_val)) => old_val == new_val,
            (AttributeValue::Signal(old_sig), AttributeValue::Signal(new_sig)) => {
                if old_sig.get_inner_addr() == new_sig.get_inner_addr() {
                    return false;
                }
                old_sig.get() == new_sig.get()
            }
            (AttributeValue::Signal(old_sig), AttributeValue::Text(new_val)) => {
                old_sig.get() == *new_val
            }
            (AttributeValue::Text(old_val), AttributeValue::Signal(new_sig)) => {
                *old_val == new_sig.get()
            }
            (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
            (AttributeValue::Css(old_css), AttributeValue::Css(new_css)) => {
                old_css.get_name() == new_css.get_name()
            }
            (AttributeValue::Dynamic(old_dyn), AttributeValue::Dynamic(new_dyn)) => {
                old_dyn == new_dyn
            }
            _ => false,
        }
    }
}

/// Visual equality comparison for attribute entries.
///
/// Two attribute entries are equal when their names match and their values
/// are visually equal as defined by `AttributeValue::eq`.
impl PartialEq for AttributeEntry {
    /// Compares two attribute entries for visual equality.
    ///
    /// # Arguments
    ///
    /// - `&Self` - The first attribute entry.
    /// - `&Self` - The second attribute entry.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if both names and values match.
    fn eq(&self, other: &Self) -> bool {
        self.get_name() == other.get_name() && self.get_value() == other.get_value()
    }
}

/// Visual equality comparison for CSS classes.
///
/// Two CSS classes are considered equal when their class names match,
/// since the name uniquely identifies the visual style rule.
impl PartialEq for CssClass {
    /// Compares two CSS classes by name.
    ///
    /// # Arguments
    ///
    /// - `&Self` - The first CSS class.
    /// - `&Self` - The second CSS class.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if the class names match.
    fn eq(&self, other: &Self) -> bool {
        self.get_name() == other.get_name()
    }
}

/// Implementation of style CSS serialization.
impl Style {
    /// Adds a style property.
    ///
    /// Property names are automatically converted from snake_case to kebab-case
    /// (e.g., `flex_direction` becomes `flex-direction`).
    ///
    /// # Arguments
    ///
    /// - `N` - The property name (snake_case will be converted to kebab-case).
    /// - `V` - The property value.
    ///
    /// # Returns
    ///
    /// - `Self` - This style with the property added.
    pub fn property<N, V>(mut self, name: N, value: V) -> Self
    where
        N: AsRef<str>,
        V: AsRef<str>,
    {
        self.get_mut_properties().push(StyleProperty::new(
            name.as_ref().replace('_', "-"),
            value.as_ref().to_string(),
        ));
        self
    }

    /// Converts the style to a CSS string.
    ///
    /// # Returns
    ///
    /// - `String` - The CSS string representation.
    pub fn to_css_string(&self) -> String {
        self.get_properties()
            .iter()
            .map(|style: &StyleProperty| format!("{}: {};", style.get_name(), style.get_value()))
            .collect::<Vec<String>>()
            .join(" ")
    }

    /// Builds a CSS style string from an array of key-value pairs.
    ///
    /// This function is used by the `html!` macro to convert static `style:`
    /// attributes into a CSS string without allocating intermediate `Style`
    /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
    /// to kebab-case automatically.
    ///
    /// # Arguments
    ///
    /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
    ///
    /// # Returns
    ///
    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
    pub fn create_style_string(props: &[(&str, &str)]) -> String {
        let mut result: String = String::new();
        for (key, value) in props {
            if !result.is_empty() {
                result.push(' ');
            }
            result.push_str(&key.replace('_', "-"));
            result.push_str(": ");
            result.push_str(value);
            result.push(';');
        }
        result
    }
}

/// Provides a default empty style.
impl Default for Style {
    /// Returns a default `Style` with no properties.
    ///
    /// # Returns
    ///
    /// - `Self` - An empty style.
    fn default() -> Self {
        Self::new(Vec::new())
    }
}

/// Implementation of CssClass construction and style injection.
impl CssClass {
    /// Creates a new CSS class with the given name and style declarations.
    ///
    /// Automatically injects the styles into the DOM upon creation.
    ///
    /// # Arguments
    ///
    /// - `String` - The class name.
    /// - `String` - The CSS style declarations.
    ///
    /// # Returns
    ///
    /// - `Self` - A new CSS class with injected styles.
    pub fn new(name: String, style: String) -> Self {
        let mut css_class: CssClass = CssClass::default();
        css_class.set_name(name);
        css_class.set_style(style);
        css_class.inject_style();
        css_class
    }

    /// Creates a new CSS class with the given name, style declarations, and pseudo rules.
    ///
    /// Automatically injects the base styles, pseudo-class/pseudo-element rules,
    /// and media query rules into the DOM upon creation.
    ///
    /// # Arguments
    ///
    /// - `String` - The class name.
    /// - `String` - The CSS style declarations.
    /// - `Vec<PseudoRule>` - The pseudo-class and pseudo-element rules.
    /// - `Vec<MediaRule>` - The media query rules.
    ///
    /// # Returns
    ///
    /// - `Self` - A new CSS class with injected styles and pseudo rules.
    pub fn new_with_rules(
        name: String,
        style: String,
        pseudo_rules: Vec<PseudoRule>,
        media_rules: Vec<MediaRule>,
    ) -> Self {
        let mut css_class: CssClass = CssClass::default();
        css_class.set_name(name);
        css_class.set_style(style);
        css_class.set_pseudo_rules(pseudo_rules);
        css_class.set_media_rules(media_rules);
        css_class.inject_style();
        css_class
    }

    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
    ///
    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
    /// This is used by the `class!` macro for fully static class definitions
    /// where pseudo rules can be computed at compile time.
    ///
    /// # Arguments
    ///
    /// - `&str` - The serialized pseudo rules string.
    ///
    /// # Returns
    ///
    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
    pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
        let mut rules: Vec<PseudoRule> = Vec::new();
        let mut remaining: &str = input;
        while !remaining.is_empty() {
            let selector_end: Option<usize> = remaining.find(" { ");
            let Some(sel_end) = selector_end else {
                break;
            };
            let selector: &str = &remaining[..sel_end];
            let after_selector: &str = remaining[sel_end..].strip_prefix(" { ").unwrap_or("");
            let style_end: Option<usize> = after_selector.find('}');
            let Some(st_end) = style_end else {
                break;
            };
            let style: &str = &after_selector[..st_end];
            if !selector.is_empty() && !style.is_empty() {
                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
            }
            remaining = after_selector[st_end..].strip_prefix('}').unwrap_or("");
        }
        rules
    }

    /// Parses media query rules from a compact serialization string.
    ///
    /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
    /// This is used by the `class!` macro for fully static class definitions
    /// where media rules can be computed at compile time.
    ///
    /// # Arguments
    ///
    /// - `&str` - The serialized media rules string.
    ///
    /// # Returns
    ///
    /// - `Vec<MediaRule>` - The parsed media rules.
    pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
        let mut rules: Vec<MediaRule> = Vec::new();
        let mut remaining: &str = input;
        while !remaining.is_empty() {
            if !remaining.starts_with("@media ") {
                break;
            }
            let after_prefix: &str = remaining.strip_prefix("@media ").unwrap_or("");
            let query_end: Option<usize> = after_prefix.find(" { ");
            let Some(q_end) = query_end else {
                break;
            };
            let query: &str = &after_prefix[..q_end];
            let after_query: &str = after_prefix[q_end..].strip_prefix(" { ").unwrap_or("");
            let style_end: Option<usize> = after_query.find('}');
            let Some(st_end) = style_end else {
                break;
            };
            let style: &str = &after_query[..st_end];
            if !query.is_empty() && !style.is_empty() {
                rules.push(MediaRule::new(query.to_string(), style.to_string()));
            }
            remaining = after_query[st_end..].strip_prefix('}').unwrap_or("");
        }
        rules
    }

    /// Injects this class's styles into the DOM if not already present.
    ///
    /// Uses a global `HashSet` to track injected class names, avoiding the
    /// expensive `existing_css.contains(css)` full-text search on every call.
    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
    /// then appends them directly to the `<style>` element via
    /// `append_child` with a new text node — no read-modify-write of the
    /// entire stylesheet content.
    ///
    /// # Panics
    ///
    /// Panics if `window()` or `document()` is unavailable on the current platform.
    pub fn inject_style(&self) {
        if !Self::mark_injected(self.get_name().clone()) {
            return;
        }
        let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
        let mut css: String = class_rule;
        for pseudo_rule in self.get_pseudo_rules() {
            if !pseudo_rule.get_style().is_empty() {
                let pseudo_rule_str: String = format!(
                    ".{}{} {{ {} }}",
                    self.get_name(),
                    pseudo_rule.get_selector(),
                    pseudo_rule.get_style()
                );
                css = format!("{}\n{}", css, pseudo_rule_str);
            }
        }
        for media_rule in self.get_media_rules() {
            if !media_rule.get_query().is_empty() {
                let media_rule_str: String = format!(
                    "@media {} {{ .{} {{ {} }} }}",
                    media_rule.get_query(),
                    self.get_name(),
                    media_rule.get_style()
                );
                css = format!("{}\n{}", css, media_rule_str);
            }
        }
        Self::append_css(&css);
    }

    /// Marks a class name as injected in the global `HashSet`.
    ///
    /// Returns `false` if the class was already injected (no-op), `true`
    /// if this is the first injection.
    ///
    /// # Arguments
    ///
    /// - `String` - The class name to mark as injected.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if newly injected, `false` if already present.
    fn mark_injected(class_name: String) -> bool {
        mark_injected_class(class_name)
    }

    /// Appends CSS text directly to the shared `<style>` element.
    ///
    /// Creates a new text node and appends it as a child of the `<style>`
    /// element, avoiding the read-modify-write pattern of reading the entire
    /// `innerText`, concatenating, and setting it back.
    ///
    /// # Arguments
    ///
    /// - `&str` - The CSS text to append.
    ///
    /// # Panics
    ///
    /// Panics if `window()` or `document()` is unavailable on the current platform.
    fn append_css(css: &str) {
        let style_id: &str = "euv-css-injected";
        let document: Document = window()
            .expect("no global window exists")
            .document()
            .expect("no document exists");
        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
            Some(el) => el.dyn_into::<HtmlStyleElement>().unwrap(),
            None => {
                let el: HtmlStyleElement = document
                    .create_element("style")
                    .unwrap()
                    .dyn_into::<HtmlStyleElement>()
                    .unwrap();
                el.set_id(style_id);
                document.head().unwrap().append_child(&el).unwrap();
                el
            }
        };
        if !css.is_empty() {
            let text_node: Text = document.create_text_node(css);
            style_element.append_child(&text_node).unwrap();
        }
    }

    /// Injects CSS text into the shared `<style>` element in the DOM.
    ///
    /// Delegates to [`CssClass::append_css`] for the actual DOM append.
    /// Unlike the previous implementation, this does not read the existing
    /// stylesheet content or perform a full-text `contains` search.
    ///
    /// # Arguments
    ///
    /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
    ///
    /// # Panics
    ///
    /// Panics if `window()` or `document()` is unavailable on the current platform.
    pub fn inject_css(css: &str) {
        Self::append_css(css);
    }
}

/// Displays the CSS class name.
///
/// This enables `format!("{}", css_class)` to produce the class name string,
/// which is required for reactive `if` conditions in `class:` attributes.
impl std::fmt::Display for CssClass {
    /// Formats the CSS class as its name string.
    ///
    /// # Arguments
    ///
    /// - `&mut Formatter` - The formatter.
    ///
    /// # Returns
    ///
    /// - `std::fmt::Result` - The formatting result.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.get_name())
    }
}