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