euv_core/vdom/attribute/impl.rs
1use crate::*;
2
3/// SAFETY: `InjectedClassesCell` is only used in single-threaded WASM contexts.
4unsafe impl Sync for InjectedClassesCell {}
5
6/// Implementation of attribute value factory methods for reactive and merged values.
7impl AttributeValue {
8 /// Creates a reactive style `Self` that updates when signals change.
9 ///
10 /// This function replaces the inline `Signal::create(...)` + `subscribe_attr_signal(...)`
11 /// boilerplate that was previously generated by the `html!` macro for every
12 /// `style:` attribute containing reactive `if` conditions.
13 ///
14 /// # Arguments
15 ///
16 /// - `Fn() -> String + 'static` - A closure that computes the current CSS string.
17 /// Called on initial render and whenever any signal changes.
18 ///
19 /// # Returns
20 ///
21 /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
22 /// that reactively re-evaluates the CSS string on signal updates.
23 pub fn create_reactive_style<F>(compute: F) -> Self
24 where
25 F: Fn() -> String + 'static,
26 {
27 let attr_signal: Signal<String> = Signal::create(compute());
28 subscribe_attr_signal(attr_signal, compute);
29 Self::Signal(attr_signal)
30 }
31
32 /// Creates a reactive attribute `Self` for conditional attribute values.
33 ///
34 /// This function replaces the inline `Signal::create(...)` + `subscribe_attr_signal(...)`
35 /// boilerplate that was previously generated by the `html!` macro for every
36 /// attribute value containing an `if` condition.
37 ///
38 /// # Arguments
39 ///
40 /// - `Fn() -> String + 'static` - A closure that computes the current attribute value.
41 /// Called on initial render and whenever any signal changes.
42 ///
43 /// # Returns
44 ///
45 /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
46 /// that reactively re-evaluates the attribute value on signal updates.
47 pub fn create_reactive_signal<F>(compute: F) -> Self
48 where
49 F: Fn() -> String + 'static,
50 {
51 let attr_signal: Signal<String> =
52 Signal::create(IntoReactiveString::into_reactive_string(compute()));
53 subscribe_attr_signal(attr_signal, move || {
54 IntoReactiveString::into_reactive_string(compute())
55 });
56 Self::Signal(attr_signal)
57 }
58
59 /// Merges multiple class attribute values into a single `Self`.
60 ///
61 /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
62 /// `Css` values are injected into the DOM and their names are collected.
63 /// All non-empty class names are joined with spaces into a final `Text` attribute.
64 /// If any value is signal-backed, the result becomes a reactive `Signal` attribute
65 /// that re-evaluates when any constituent signal changes.
66 ///
67 /// # Arguments
68 ///
69 /// - `&[Self]` - The class attribute values to merge.
70 ///
71 /// # Returns
72 ///
73 /// - `Self` - A merged attribute value containing space-separated class names.
74 pub fn merge_class(values: &[Self]) -> Self {
75 let has_signal: bool = values
76 .iter()
77 .any(|value: &Self| matches!(value, Self::Signal(_)));
78 if has_signal {
79 let owned_values: Vec<Self> = values.to_vec();
80 let compute = move || {
81 let mut result: String = String::new();
82 for value in &owned_values {
83 let class_segment: String = match value {
84 Self::Css(css) => {
85 css.inject_style();
86 css.get_name().to_string()
87 }
88 Self::Text(text_value) => text_value.clone(),
89 Self::Signal(signal) => signal.get(),
90 _ => String::new(),
91 };
92 if !class_segment.is_empty() {
93 if !result.is_empty() {
94 result.push(CHAR_SPACE);
95 }
96 result.push_str(&class_segment);
97 }
98 }
99 result
100 };
101 let attr_signal: Signal<String> = Signal::create(compute());
102 subscribe_attr_signal(attr_signal, compute);
103 Self::Signal(attr_signal)
104 } else {
105 let mut result: String = String::new();
106 for value in values {
107 let class_segment: String = match value {
108 Self::Css(css) => {
109 css.inject_style();
110 css.get_name().to_string()
111 }
112 Self::Text(text_value) => text_value.clone(),
113 _ => String::new(),
114 };
115 if !class_segment.is_empty() {
116 if !result.is_empty() {
117 result.push(CHAR_SPACE);
118 }
119 result.push_str(&class_segment);
120 }
121 }
122 Self::Text(result)
123 }
124 }
125
126 /// Merges multiple style attribute values into a single `Self`.
127 ///
128 /// Each input value is expected to be a style string (`Text`) or a reactive
129 /// `Signal<String>` producing a style string. All non-empty style strings are
130 /// joined with spaces into a final combined style attribute.
131 /// If any value is signal-backed, the result becomes a reactive `Signal` attribute.
132 ///
133 /// # Arguments
134 ///
135 /// - `&[Self]` - The style attribute values to merge.
136 ///
137 /// # Returns
138 ///
139 /// - `Self` - A merged attribute value containing the combined CSS style string.
140 pub fn merge_style(values: &[Self]) -> Self {
141 let has_signal: bool = values
142 .iter()
143 .any(|value: &Self| matches!(value, Self::Signal(_)));
144 if has_signal {
145 let owned_values: Vec<Self> = values.to_vec();
146 let compute = move || {
147 let mut result: String = String::new();
148 for value in &owned_values {
149 let style_segment: String = match value {
150 Self::Text(text_value) => text_value.clone(),
151 Self::Signal(signal) => signal.get(),
152 _ => String::new(),
153 };
154 if !style_segment.is_empty() {
155 if !result.is_empty() {
156 result.push(CHAR_SPACE);
157 }
158 result.push_str(&style_segment);
159 }
160 }
161 result
162 };
163 let attr_signal: Signal<String> = Signal::create(compute());
164 subscribe_attr_signal(attr_signal, compute);
165 Self::Signal(attr_signal)
166 } else {
167 let mut result: String = String::new();
168 for value in values {
169 let style_segment: String = match value {
170 Self::Text(text_value) => text_value.clone(),
171 _ => String::new(),
172 };
173 if !style_segment.is_empty() {
174 if !result.is_empty() {
175 result.push(CHAR_SPACE);
176 }
177 result.push_str(&style_segment);
178 }
179 }
180 Self::Text(result)
181 }
182 }
183}
184
185/// Visual equality comparison for attribute values.
186///
187/// Compares values by their visual output rather than identity. `Signal`
188/// values are compared by their current resolved string; when both signals
189/// share the same inner pointer, they are always considered **unequal**
190/// because the signal may have mutated between VDOM snapshots and `.get()`
191/// would return the same current value for both, masking the change.
192/// `Event` values are always considered equal (re-binding is handled by the
193/// handler registry), and `Css` values are compared by class name.
194impl PartialEq for AttributeValue {
195 /// Compares two attribute values for visual equality.
196 ///
197 /// # Arguments
198 ///
199 /// - `&Self` - The first attribute value.
200 /// - `&Self` - The second attribute value.
201 ///
202 /// # Returns
203 ///
204 /// - `bool` - `true` if the values are visually equal.
205 fn eq(&self, other: &Self) -> bool {
206 match (self, other) {
207 (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
208 (Self::Signal(old_signal), Self::Signal(new_signal)) => {
209 if old_signal.get_inner_addr() == new_signal.get_inner_addr() {
210 return false;
211 }
212 old_signal.get() == new_signal.get()
213 }
214 (Self::Signal(old_signal), Self::Text(new_value)) => old_signal.get() == *new_value,
215 (Self::Text(old_value), Self::Signal(new_signal)) => *old_value == new_signal.get(),
216 (Self::Event(_), Self::Event(_)) => true,
217 (Self::Css(old_class), Self::Css(new_class)) => {
218 old_class.get_name() == new_class.get_name()
219 }
220 (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
221 _ => false,
222 }
223 }
224}
225
226/// Visual equality comparison for attribute entries.
227///
228/// Two attribute entries are equal when their names match and their values
229/// are visually equal as defined by `AttributeValue::eq`.
230impl PartialEq for AttributeEntry {
231 /// Compares two attribute entries for visual equality.
232 ///
233 /// # Arguments
234 ///
235 /// - `&Self` - The first attribute entry.
236 /// - `&Self` - The second attribute entry.
237 ///
238 /// # Returns
239 ///
240 /// - `bool` - `true` if both names and values match.
241 fn eq(&self, other: &Self) -> bool {
242 self.get_name() == other.get_name() && self.get_value() == other.get_value()
243 }
244}
245
246/// Visual equality comparison for CSS classes.
247///
248/// Two CSS classes are considered equal when their class names match,
249/// since the name uniquely identifies the visual style rule.
250impl PartialEq for Css {
251 /// Compares two CSS classes by name.
252 ///
253 /// # Arguments
254 ///
255 /// - `&Self` - The first CSS class.
256 /// - `&Self` - The second CSS class.
257 ///
258 /// # Returns
259 ///
260 /// - `bool` - `true` if the class names match.
261 fn eq(&self, other: &Self) -> bool {
262 self.get_name() == other.get_name()
263 }
264}
265
266/// Implementation of style CSS serialization.
267impl Style {
268 /// Adds a style property.
269 ///
270 /// Property names are automatically converted from snake_case to kebab-case
271 /// (e.g., `flex_direction` becomes `flex-direction`).
272 ///
273 /// # Arguments
274 ///
275 /// - `N` - The property name (snake_case will be converted to kebab-case).
276 /// - `V` - The property value.
277 ///
278 /// # Returns
279 ///
280 /// - `Self` - This style with the property added.
281 pub fn property<N, V>(mut self, name: N, value: V) -> Self
282 where
283 N: AsRef<str>,
284 V: AsRef<str>,
285 {
286 self.get_mut_properties().push(StyleProperty::new(
287 name.as_ref().replace(CHAR_UNDERSCORE, STR_HYPHEN),
288 value.as_ref().to_string(),
289 ));
290 self
291 }
292
293 /// Converts the style to a CSS string.
294 ///
295 /// # Returns
296 ///
297 /// - `String` - The CSS string representation.
298 pub fn to_css_string(&self) -> String {
299 self.get_properties()
300 .iter()
301 .map(|style: &StyleProperty| {
302 format!(
303 "{name}{CSS_PROP_SEPARATOR}{value}",
304 name = style.get_name(),
305 value = style.get_value()
306 )
307 })
308 .collect::<Vec<String>>()
309 .join(" ")
310 }
311
312 /// Builds a CSS style string from an array of key-value pairs.
313 ///
314 /// This function is used by the `html!` macro to convert static `style:`
315 /// attributes into a CSS string without allocating intermediate `Style`
316 /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
317 /// to kebab-case automatically.
318 ///
319 /// # Arguments
320 ///
321 /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
322 ///
323 /// # Returns
324 ///
325 /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
326 pub fn create_style_string(props: &[(&str, &str)]) -> String {
327 let mut result: String = String::new();
328 for (key, value) in props {
329 if !result.is_empty() {
330 result.push(CHAR_SPACE);
331 }
332 result.push_str(&key.replace(CHAR_UNDERSCORE, STR_HYPHEN));
333 result.push_str(CSS_PROP_SEPARATOR);
334 result.push_str(value);
335 result.push(CHAR_CSS_DECL_TERMINATOR);
336 }
337 result
338 }
339}
340
341/// Provides a default empty style.
342impl Default for Style {
343 /// Returns a default `Self` with no properties.
344 ///
345 /// # Returns
346 ///
347 /// - `Self` - An empty style.
348 fn default() -> Self {
349 Self::new(Vec::new())
350 }
351}
352
353/// Implementation of Css construction and style injection.
354impl Css {
355 /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
356 ///
357 /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
358 /// This is used by the `class!` macro for fully static class definitions
359 /// where pseudo rules can be computed at compile time.
360 ///
361 /// # Arguments
362 ///
363 /// - `&str` - The serialized pseudo rules string.
364 ///
365 /// # Returns
366 ///
367 /// - `Vec<PseudoRule>` - The parsed pseudo rules.
368 pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
369 let mut rules: Vec<PseudoRule> = Vec::new();
370 let mut remaining: &str = input;
371 while !remaining.is_empty() {
372 let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
373 let Some(selector_end_index) = selector_end else {
374 break;
375 };
376 let selector: &str = &remaining[..selector_end_index];
377 let after_selector: &str = remaining[selector_end_index..]
378 .strip_prefix(CSS_RULE_OPEN)
379 .unwrap_or_default();
380 let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
381 let Some(style_end_index) = style_end else {
382 break;
383 };
384 let style: &str = &after_selector[..style_end_index];
385 if !selector.is_empty() && !style.is_empty() {
386 rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
387 }
388 remaining = after_selector[style_end_index..]
389 .strip_prefix(CHAR_CSS_RULE_CLOSE)
390 .unwrap_or_default();
391 }
392 rules
393 }
394
395 /// Parses media query rules from a compact serialization string.
396 ///
397 /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
398 /// This is used by the `class!` macro for fully static class definitions
399 /// where media rules can be computed at compile time.
400 ///
401 /// # Arguments
402 ///
403 /// - `&str` - The serialized media rules string.
404 ///
405 /// # Returns
406 ///
407 /// - `Vec<MediaRule>` - The parsed media rules.
408 pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
409 let mut rules: Vec<MediaRule> = Vec::new();
410 let mut remaining: &str = input;
411 while !remaining.is_empty() {
412 if !remaining.starts_with(CSS_MEDIA_PREFIX) {
413 break;
414 }
415 let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
416 let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
417 let Some(query_end_index) = query_end else {
418 break;
419 };
420 let query: &str = &after_prefix[..query_end_index];
421 let after_query: &str = after_prefix[query_end_index..]
422 .strip_prefix(CSS_RULE_OPEN)
423 .unwrap_or_default();
424 let style_end: Option<usize> = after_query.find(CHAR_CSS_RULE_CLOSE);
425 let Some(style_end_index) = style_end else {
426 break;
427 };
428 let style: &str = &after_query[..style_end_index];
429 if !query.is_empty() && !style.is_empty() {
430 rules.push(MediaRule::new(query.to_string(), style.to_string()));
431 }
432 remaining = after_query[style_end_index..]
433 .strip_prefix(CHAR_CSS_RULE_CLOSE)
434 .unwrap_or_default();
435 }
436 rules
437 }
438
439 /// Injects this class's styles into the DOM if not already present.
440 ///
441 /// Uses a global `HashSet` to track injected class names, avoiding the
442 /// expensive `existing_css.contains(css)` full-text search on every call.
443 /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
444 /// then appends them directly to the `<style>` element via
445 /// `append_child` with a new text node — no read-modify-write of the
446 /// entire stylesheet content.
447 ///
448 /// # Panics
449 ///
450 /// Panics if `window()` or `document()` is unavailable on the current platform.
451 pub fn inject_style(&self) {
452 if !Self::mark_injected(self.get_name().clone()) {
453 return;
454 }
455 let class_rule: String = format!(
456 "{CHAR_CSS_CLASS_PREFIX}{} {{ {} }}",
457 self.get_name(),
458 self.get_style()
459 );
460 let mut css_text: String = class_rule;
461 for pseudo_rule in self.get_pseudo_rules() {
462 if !pseudo_rule.get_style().is_empty() {
463 let pseudo_rule_str: String = format!(
464 "{CHAR_CSS_CLASS_PREFIX}{}{} {{ {} }}",
465 self.get_name(),
466 pseudo_rule.get_selector(),
467 pseudo_rule.get_style()
468 );
469 css_text = format!("{css_text}\n{pseudo_rule_str}");
470 }
471 }
472 for media_rule in self.get_media_rules() {
473 if !media_rule.get_query().is_empty() {
474 let media_rule_str: String = format!(
475 "@media {} {{ {CHAR_CSS_CLASS_PREFIX}{} {{ {} }} }}",
476 media_rule.get_query(),
477 self.get_name(),
478 media_rule.get_style()
479 );
480 css_text = format!("{css_text}\n{media_rule_str}");
481 }
482 }
483 Self::append_css(&css_text);
484 }
485
486 /// Marks a class name as injected in the global `HashSet`.
487 ///
488 /// Returns `false` if the class was already injected (no-op), `true`
489 /// if this is the first injection.
490 ///
491 /// # Arguments
492 ///
493 /// - `String` - The class name to mark as injected.
494 ///
495 /// # Returns
496 ///
497 /// - `bool` - `true` if newly injected, `false` if already present.
498 fn mark_injected(class_name: String) -> bool {
499 get_injected_classes_mut().insert(class_name)
500 }
501
502 /// Appends CSS text directly to the shared `<style>` element.
503 ///
504 /// Creates a new text node and appends it as a child of the `<style>`
505 /// element, avoiding the read-modify-write pattern of reading the entire
506 /// `innerText`, concatenating, and setting it back.
507 ///
508 /// # Arguments
509 ///
510 /// - `&str` - The CSS text to append.
511 ///
512 /// # Panics
513 ///
514 /// Panics if `window()` or `document()` is unavailable on the current platform.
515 fn append_css(css_text: &str) {
516 let style_id: &str = EUV_CSS_INJECTED_ID;
517 let document: Document = window()
518 .expect("no global window exists")
519 .document()
520 .expect("no document exists");
521 let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
522 Some(existing_element) => existing_element.dyn_into::<HtmlStyleElement>().unwrap(),
523 None => {
524 let style_element_from_id: HtmlStyleElement = document
525 .create_element(STYLE_TAG)
526 .unwrap()
527 .dyn_into::<HtmlStyleElement>()
528 .unwrap();
529 style_element_from_id.set_id(style_id);
530 document
531 .head()
532 .unwrap()
533 .append_child(&style_element_from_id)
534 .unwrap();
535 style_element_from_id
536 }
537 };
538 if !css_text.is_empty() {
539 let text_node: Text = document.create_text_node(css_text);
540 style_element.append_child(&text_node).unwrap();
541 }
542 }
543
544 /// Injects CSS text into the shared `<style>` element in the DOM.
545 ///
546 /// Delegates to [`Css::append_css`] for the actual DOM append.
547 /// Unlike the previous implementation, this does not read the existing
548 /// stylesheet content or perform a full-text `contains` search.
549 ///
550 /// # Arguments
551 ///
552 /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
553 ///
554 /// # Panics
555 ///
556 /// Panics if `window()` or `document()` is unavailable on the current platform.
557 pub fn inject_css(css_text: &str) {
558 Self::append_css(css_text);
559 }
560}
561
562/// Displays the CSS class name.
563///
564/// This enables `format!("{}", css)` to produce the class name string,
565/// which is required for reactive `if` conditions in `class:` attributes.
566impl Display for Css {
567 /// Formats the CSS class as its name string.
568 ///
569 /// # Arguments
570 ///
571 /// - `&mut Formatter` - The formatter.
572 ///
573 /// # Returns
574 ///
575 /// - `fmt::Result` - The formatting result.
576 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
577 write!(formatter, "{class_name}", class_name = self.get_name())
578 }
579}