euv-core 0.5.11

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
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 || {
                owned_values
                    .iter()
                    .filter_map(|value: &Self| match value {
                        Self::Css(css) => {
                            css.inject_style();
                            Some(css.get_name().to_string())
                        }
                        Self::Text(text_value) => Some(text_value.clone()),
                        Self::Signal(signal) => Some(signal.get()),
                        _ => None,
                    })
                    .filter(|segment: &String| !segment.is_empty())
                    .collect::<Vec<String>>()
                    .join(&CHAR_SPACE.to_string())
            };
            let attr_signal: Signal<String> = Signal::create(compute());
            subscribe_attr_signal(attr_signal, compute);
            Self::Signal(attr_signal)
        } else {
            let result: String = values
                .iter()
                .filter_map(|value: &Self| match value {
                    Self::Css(css) => {
                        css.inject_style();
                        Some(css.get_name().to_string())
                    }
                    Self::Text(text_value) => Some(text_value.clone()),
                    _ => None,
                })
                .filter(|segment: &String| !segment.is_empty())
                .collect::<Vec<String>>()
                .join(&CHAR_SPACE.to_string());
            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 || {
                owned_values
                    .iter()
                    .filter_map(|value: &Self| match value {
                        Self::Text(text_value) => Some(text_value.clone()),
                        Self::Signal(signal) => Some(signal.get()),
                        _ => None,
                    })
                    .filter(|segment: &String| !segment.is_empty())
                    .collect::<Vec<String>>()
                    .join(&CHAR_SPACE.to_string())
            };
            let attr_signal: Signal<String> = Signal::create(compute());
            subscribe_attr_signal(attr_signal, compute);
            Self::Signal(attr_signal)
        } else {
            let result: String = values
                .iter()
                .filter_map(|value: &Self| match value {
                    Self::Text(text_value) => Some(text_value.clone()),
                    _ => None,
                })
                .filter(|segment: &String| !segment.is_empty())
                .collect::<Vec<String>>()
                .join(&CHAR_SPACE.to_string());
            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() == new_signal.get_inner() {
                    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 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 mut css_text: String = format!(
            "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
            self.get_name(),
            self.get_style()
        );
        for pseudo_rule in self.get_pseudo_rules() {
            if !pseudo_rule.get_style().is_empty() {
                css_text = format!(
                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
                    self.get_name(),
                    pseudo_rule.get_selector(),
                    pseudo_rule.get_style()
                );
            }
        }
        for media_rule in self.get_media_rules() {
            if !media_rule.get_query().is_empty() {
                css_text = format!(
                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CSS_MEDIA_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}{CSS_RULE_CLOSE_FORMAT}",
                    media_rule.get_query(),
                    self.get_name(),
                    media_rule.get_style()
                );
            }
        }
        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(window_instance) => window_instance,
            None => return,
        };
        let document: Document = match window_value.document() {
            Some(document_instance) => document_instance,
            None => return,
        };
        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
                Ok(element) => element,
                Err(_err) => return,
            },
            None => {
                let created: Element = match document.create_element(STYLE_TAG) {
                    Ok(element) => element,
                    Err(_err) => return,
                };
                let style_element_from_id: HtmlStyleElement =
                    match created.dyn_into::<HtmlStyleElement>() {
                        Ok(element) => element,
                        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);
        }
    }

    /// 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 objects.
    ///
    /// # 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 {
        props
            .iter()
            .map(|(key, value): &(&str, &str)| {
                format!("{key}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}")
            })
            .collect::<Vec<String>>()
            .join(&CHAR_SPACE.to_string())
    }

    /// Builds a CSS style string from owned key-value pairs.
    ///
    /// Used by the `html!` macro for reactive style attributes (with `if`
    /// conditions) where values are computed at runtime.
    ///
    /// # Arguments
    ///
    /// - `&[(String, String)]` - An array of owned CSS property name-value pairs.
    ///
    /// # Returns
    ///
    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
    pub fn create_style_string_owned(props: &[(String, String)]) -> String {
        props
            .iter()
            .map(|(key, value): &(String, String)| {
                format!("{key}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}")
            })
            .collect::<Vec<String>>()
            .join(&CHAR_SPACE.to_string())
    }

    /// 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, "{}", self.get_name())
    }
}