euv-core 0.5.6

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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
use crate::*;

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

/// Implementation of attribute value factory methods for reactive and merged values.
impl AttributeValue {
    /// Creates a reactive attribute `Self` for conditional attribute values.
    ///
    /// This function replaces the inline `Signal::create(...)` + `subscribe_attr_signal(...)`
    /// boilerplate that was previously generated by the `html!` macro for every
    /// attribute value containing an `if` condition.
    ///
    /// # Arguments
    ///
    /// - `Fn() -> String + 'static` - A closure that computes the current attribute value.
    ///   Called on initial render and whenever any signal changes.
    ///
    /// # Returns
    ///
    /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
    ///   that reactively re-evaluates the attribute value on signal updates.
    pub fn create_reactive_signal<F>(compute: F) -> Self
    where
        F: Fn() -> String + 'static,
    {
        let attr_signal: Signal<String> =
            Signal::create(IntoReactiveString::into_reactive_string(compute()));
        subscribe_attr_signal(attr_signal, move || {
            IntoReactiveString::into_reactive_string(compute())
        });
        Self::Signal(attr_signal)
    }

    /// Merges multiple class attribute values into a single `Self`.
    ///
    /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
    /// `Css` values are injected into the DOM and their names are collected.
    /// All non-empty class names are joined with spaces into a final `Text` attribute.
    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute
    /// that re-evaluates when any constituent signal changes.
    ///
    /// # Arguments
    ///
    /// - `&[Self]` - The class attribute values to merge.
    ///
    /// # Returns
    ///
    /// - `Self` - A merged attribute value containing space-separated class names.
    pub fn merge_class(values: &[Self]) -> Self {
        let has_signal: bool = values
            .iter()
            .any(|value: &Self| matches!(value, Self::Signal(_)));
        if has_signal {
            let owned_values: Vec<Self> = values.to_vec();
            let compute = move || {
                let mut result: String = String::new();
                for value in &owned_values {
                    let class_segment: String = match value {
                        Self::Css(css) => {
                            css.inject_style();
                            css.get_name().to_string()
                        }
                        Self::Text(text_value) => text_value.clone(),
                        Self::Signal(signal) => signal.get(),
                        _ => String::new(),
                    };
                    if !class_segment.is_empty() {
                        if !result.is_empty() {
                            result.push(CHAR_SPACE);
                        }
                        result.push_str(&class_segment);
                    }
                }
                result
            };
            let attr_signal: Signal<String> = Signal::create(compute());
            subscribe_attr_signal(attr_signal, compute);
            Self::Signal(attr_signal)
        } else {
            let mut result: String = String::new();
            for value in values {
                let class_segment: String = match value {
                    Self::Css(css) => {
                        css.inject_style();
                        css.get_name().to_string()
                    }
                    Self::Text(text_value) => text_value.clone(),
                    _ => String::new(),
                };
                if !class_segment.is_empty() {
                    if !result.is_empty() {
                        result.push(CHAR_SPACE);
                    }
                    result.push_str(&class_segment);
                }
            }
            Self::Text(result)
        }
    }

    /// Merges multiple style attribute values into a single `Self`.
    ///
    /// Each input value is expected to be a style string (`Text`) or a reactive
    /// `Signal<String>` producing a style string. All non-empty style strings are
    /// joined with spaces into a final combined style attribute.
    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute.
    ///
    /// # Arguments
    ///
    /// - `&[Self]` - The style attribute values to merge.
    ///
    /// # Returns
    ///
    /// - `Self` - A merged attribute value containing the combined CSS style string.
    pub fn merge_style(values: &[Self]) -> Self {
        let has_signal: bool = values
            .iter()
            .any(|value: &Self| matches!(value, Self::Signal(_)));
        if has_signal {
            let owned_values: Vec<Self> = values.to_vec();
            let compute = move || {
                let mut result: String = String::new();
                for value in &owned_values {
                    let style_segment: String = match value {
                        Self::Text(text_value) => text_value.clone(),
                        Self::Signal(signal) => signal.get(),
                        _ => String::new(),
                    };
                    if !style_segment.is_empty() {
                        if !result.is_empty() {
                            result.push(CHAR_SPACE);
                        }
                        result.push_str(&style_segment);
                    }
                }
                result
            };
            let attr_signal: Signal<String> = Signal::create(compute());
            subscribe_attr_signal(attr_signal, compute);
            Self::Signal(attr_signal)
        } else {
            let mut result: String = String::new();
            for value in values {
                let style_segment: String = match value {
                    Self::Text(text_value) => text_value.clone(),
                    _ => String::new(),
                };
                if !style_segment.is_empty() {
                    if !result.is_empty() {
                        result.push(CHAR_SPACE);
                    }
                    result.push_str(&style_segment);
                }
            }
            Self::Text(result)
        }
    }
}

/// 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 `Css` 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) {
            (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
            (Self::Signal(old_signal), Self::Signal(new_signal)) => {
                if old_signal.get_inner_addr() == new_signal.get_inner_addr() {
                    return false;
                }
                old_signal.get() == new_signal.get()
            }
            (Self::Signal(old_signal), Self::Text(new_value)) => old_signal.get() == *new_value,
            (Self::Text(old_value), Self::Signal(new_signal)) => *old_value == new_signal.get(),
            (Self::Event(_), Self::Event(_)) => true,
            (Self::Css(old_class), Self::Css(new_class)) => {
                old_class.get_name() == new_class.get_name()
            }
            (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
            _ => 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 Css {
    /// 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(CHAR_UNDERSCORE, STR_HYPHEN),
            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!(
                    "{name}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}",
                    name = style.get_name(),
                    value = 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(CHAR_SPACE);
            }
            result.push_str(&key.replace(CHAR_UNDERSCORE, STR_HYPHEN));
            result.push_str(CSS_PROP_SEPARATOR);
            result.push_str(value);
            result.push(CHAR_CSS_DECL_TERMINATOR);
        }
        result
    }
}

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

/// Implementation of Css construction and style injection.
impl Css {
    /// 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(CSS_RULE_OPEN);
            let Some(selector_end_index) = selector_end else {
                break;
            };
            let selector: &str = &remaining[..selector_end_index];
            let after_selector: &str = remaining[selector_end_index..]
                .strip_prefix(CSS_RULE_OPEN)
                .unwrap_or_default();
            let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
            let Some(style_end_index) = style_end else {
                break;
            };
            let style: &str = &after_selector[..style_end_index];
            if !selector.is_empty() && !style.is_empty() {
                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
            }
            remaining = after_selector[style_end_index..]
                .strip_prefix(CHAR_CSS_RULE_CLOSE)
                .unwrap_or_default();
        }
        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(CSS_MEDIA_PREFIX) {
                break;
            }
            let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
            let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
            let Some(query_end_index) = query_end else {
                break;
            };
            let query: &str = &after_prefix[..query_end_index];
            let after_query: &str = after_prefix[query_end_index..]
                .strip_prefix(CSS_RULE_OPEN)
                .unwrap_or_default();
            let style_end: Option<usize> = after_query.find(CHAR_CSS_RULE_CLOSE);
            let Some(style_end_index) = style_end else {
                break;
            };
            let style: &str = &after_query[..style_end_index];
            if !query.is_empty() && !style.is_empty() {
                rules.push(MediaRule::new(query.to_string(), style.to_string()));
            }
            remaining = after_query[style_end_index..]
                .strip_prefix(CHAR_CSS_RULE_CLOSE)
                .unwrap_or_default();
        }
        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!(
            "{CHAR_CSS_CLASS_PREFIX}{} {{ {} }}",
            self.get_name(),
            self.get_style()
        );
        let mut css_text: String = class_rule;
        for pseudo_rule in self.get_pseudo_rules() {
            if !pseudo_rule.get_style().is_empty() {
                let pseudo_rule_str: String = format!(
                    "{CHAR_CSS_CLASS_PREFIX}{}{} {{ {} }}",
                    self.get_name(),
                    pseudo_rule.get_selector(),
                    pseudo_rule.get_style()
                );
                css_text = format!("{css_text}\n{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 {} {{ {CHAR_CSS_CLASS_PREFIX}{} {{ {} }} }}",
                    media_rule.get_query(),
                    self.get_name(),
                    media_rule.get_style()
                );
                css_text = format!("{css_text}\n{media_rule_str}");
            }
        }
        Self::append_css(&css_text);
    }

    /// 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 {
        get_injected_classes_mut().insert(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.
    ///
    fn append_css(css_text: &str) {
        let style_id: &str = EUV_CSS_INJECTED_ID;
        let window_value: Window = match window() {
            Some(w) => w,
            None => return,
        };
        let document: Document = match window_value.document() {
            Some(d) => d,
            None => return,
        };
        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
                Ok(el) => el,
                Err(_err) => return,
            },
            None => {
                let created: Element = match document.create_element(STYLE_TAG) {
                    Ok(el) => el,
                    Err(_err) => return,
                };
                let style_element_from_id: HtmlStyleElement =
                    match created.dyn_into::<HtmlStyleElement>() {
                        Ok(el) => el,
                        Err(_err) => return,
                    };
                style_element_from_id.set_id(style_id);
                if let Some(head) = document.head() {
                    let _ = head.append_child(&style_element_from_id);
                }
                style_element_from_id
            }
        };
        if !css_text.is_empty() {
            let text_node: Text = document.create_text_node(css_text);
            let _ = style_element.append_child(&text_node);
        }
    }

    /// Injects CSS text into the shared `<style>` element in the DOM.
    ///
    /// Delegates to [`Css::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_text: &str) {
        Self::append_css(css_text);
    }
}

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