euv_core/vdom/attribute/impl.rs
1use super::*;
2
3/// Implementation of the public read side of an attribute entry.
4impl AttributeEntry {
5 /// Returns the CSS class names this attribute carries.
6 ///
7 /// The `class` attribute is the one place where a component's
8 /// styling decisions become observable, so it needs a public read
9 /// side: a component library's tests have to be able to assert
10 /// that a state actually landed on the right class, and an external
11 /// crate cannot read the raw attribute value.
12 ///
13 /// Every shape the attribute can take is reported:
14 /// `Css` and `CssRef` contribute the class name they carry, `Text`
15 /// and `StaticText` contribute their string split on whitespace,
16 /// a `Signal` contributes the class list it currently holds — that
17 /// is the shape a reactive `class:` attribute takes — and the
18 /// remaining shapes, an event handler, contribute nothing, because
19 /// neither is a class list.
20 ///
21 /// # Returns
22 ///
23 /// - `Vec<String>` - The class names, in the order they appear.
24 pub fn class_names(&self) -> Vec<String> {
25 match self.get_value() {
26 AttributeValue::Css(css) => vec![css.get_name().clone()],
27 AttributeValue::CssRef(css) => vec![css.get_name().clone()],
28 AttributeValue::Text(text) => split_class_names(text),
29 AttributeValue::StaticText(text) => split_class_names(text),
30 AttributeValue::Signal(signal) => split_class_names(&signal.get()),
31 _ => Vec::new(),
32 }
33 }
34
35 /// Returns the attribute's value as text, when it carries one.
36 ///
37 /// A `Text` or `StaticText` attribute is its own value; a `Css` or
38 /// `CssRef` attribute contributes its class name, because that is
39 /// the text the browser ends up showing for it. The remaining
40 /// shapes — a reactive signal, an event handler — carry no text.
41 ///
42 /// # Returns
43 ///
44 /// - `Option<String>` - The value, when the attribute has one.
45 pub fn text_value(&self) -> Option<String> {
46 match self.get_value() {
47 AttributeValue::Text(text) => Some(text.clone()),
48 AttributeValue::StaticText(text) => Some((*text).to_string()),
49 AttributeValue::Css(css) => Some(css.get_name().clone()),
50 AttributeValue::CssRef(css) => Some(css.get_name().clone()),
51 _ => None,
52 }
53 }
54}
55
56/// Implementation of injected class tracking for CSS deduplication.
57impl InjectedClassesCell {
58 /// Returns `true` if the given class name has already been injected into
59 /// the DOM.
60 ///
61 /// # Arguments
62 ///
63 /// - `&str` - The CSS class name to check.
64 ///
65 /// # Returns
66 ///
67 /// - `bool` - Whether the class name has been injected.
68 pub(crate) fn is_injected(class_name: &str) -> bool {
69 INJECTED_CLASSES
70 .read()
71 .map(|classes: RwLockReadGuard<'_, HashSet<String>>| classes.contains(class_name))
72 .unwrap_or(false)
73 }
74
75 /// Marks a class name as injected so future calls to `is_injected`
76 /// return `true`.
77 ///
78 /// A poisoned lock (which only happens if another thread panicked while
79 /// holding the write guard) is recovered from rather than propagated:
80 /// the set is deduplication bookkeeping, so losing a lock must never
81 /// take down the render path.
82 ///
83 /// # Arguments
84 ///
85 /// - `&str` - The CSS class name to mark as injected.
86 pub(crate) fn mark_injected(class_name: &str) {
87 let write_result: Result<
88 InjectedClassesWriteGuard<'_>,
89 PoisonError<InjectedClassesWriteGuard<'_>>,
90 > = INJECTED_CLASSES.write();
91 let Ok(mut classes) = write_result else {
92 return;
93 };
94 classes.insert(class_name.to_string());
95 }
96}
97
98/// Implementation of attribute value factory methods for reactive and merged values.
99impl AttributeValue {
100 /// Creates a reactive attribute `Self` for conditional attribute values.
101 ///
102 /// This function replaces the inline `Signal::create(...)` boilerplate
103 /// that was previously generated by the `html!` macro for every
104 /// attribute value containing an `if` condition.
105 ///
106 /// The returned `Self::Signal` is a value carrier created fresh on each
107 /// render of the enclosing dynamic node: the signals read inside
108 /// `compute` during that render register the dynamic node as their
109 /// dependent, so any source change re-renders the node and rebuilds
110 /// this attribute with the new value. No registry listener is kept —
111 /// the former `subscribe_attr` registration was unreachable dead weight
112 /// (its key could never be marked dirty) and aliased the dynamic-node
113 /// key space of the update registry.
114 ///
115 /// # Arguments
116 ///
117 /// - `F` - A closure that computes the current attribute value.
118 /// Called once per render of the enclosing dynamic node.
119 ///
120 /// # Returns
121 ///
122 /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
123 /// carrying the freshly computed attribute value.
124 pub fn reactive<F>(compute: F) -> Self
125 where
126 F: Fn() -> String + 'static,
127 {
128 let attr_signal: Signal<String> = Signal::create(compute());
129 Self::Signal(attr_signal)
130 }
131
132 /// Merges multiple class attribute values into a single `Self`.
133 ///
134 /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
135 /// `Css` values are injected into the DOM and their names are collected.
136 /// All non-empty class names are joined with spaces into a final `Text` attribute.
137 /// If any value is signal-backed, the result becomes a `Signal` attribute
138 /// carrying the freshly merged value for this render; the enclosing
139 /// dynamic node re-renders on any constituent signal change (see
140 /// [`AttributeValue::reactive`]).
141 ///
142 /// # Arguments
143 ///
144 /// - `&[Self]` - The class attribute values to merge.
145 ///
146 /// # Returns
147 ///
148 /// - `Self` - A merged attribute value containing space-separated class names.
149 pub fn merge_class(values: &[Self]) -> Self {
150 let has_signal: bool = values
151 .iter()
152 .any(|value: &Self| matches!(value, Self::Signal(_)));
153 if has_signal {
154 let owned_values: Vec<Self> = values.to_vec();
155 let compute: Box<dyn Fn() -> String> =
156 Box::new(move || Self::join_class_segments(&owned_values));
157 let attr_signal: Signal<String> = Signal::create(compute());
158 return Self::Signal(attr_signal);
159 }
160 Self::Text(Self::join_class_segments(values))
161 }
162
163 /// Joins class attribute values into a single space-separated string.
164 ///
165 /// OPT-20: builds the result in a single `String` (amortized growth,
166 /// no intermediate `Vec<String>` + `join(" ")` round-trip), iterating
167 /// the input values by reference and cloning only the segments that
168 /// survive the filter (skipping `_ => None` arms and empty `Text`
169 /// strings), so the per-class render allocation drops from `N + 2`
170 /// to `1`.
171 ///
172 /// OPT-11: both `Css` and `CssRef` arms inject the style on first
173 /// reference. Without the `CssRef` arm the ref would fall through
174 /// to `_ => None` and silently drop out of the merged class list —
175 /// the regression that wiped every multi-class CssRef entry (e.g.
176 /// the `c_binding_slider` class next to a parameterized
177 /// `c_slider_value("30%")` on the same `<input>`).
178 ///
179 /// # Arguments
180 ///
181 /// - `&[Self]` - The class attribute values to merge, in order.
182 ///
183 /// # Returns
184 ///
185 /// - `String` - The merged class list, segments separated by a
186 /// single space; empty when no segment survived filtering.
187 fn join_class_segments(values: &[Self]) -> String {
188 let mut joined: String = String::new();
189 for value in values.iter() {
190 let segment: Cow<'_, str> = match value {
191 Self::Css(css) => {
192 css.inject_style();
193 let name: &str = css.get_name();
194 if name.is_empty() {
195 continue;
196 }
197 Cow::Borrowed(name)
198 }
199 Self::CssRef(css) => {
200 css.inject_style();
201 let name: &str = css.get_name();
202 if name.is_empty() {
203 continue;
204 }
205 Cow::Borrowed(name)
206 }
207 Self::Text(text_value) => {
208 if text_value.is_empty() {
209 continue;
210 }
211 Cow::Borrowed(text_value.as_str())
212 }
213 Self::Signal(signal) => {
214 let current: String = signal.get();
215 if current.is_empty() {
216 continue;
217 }
218 Cow::Owned(current)
219 }
220 _ => continue,
221 };
222 if !joined.is_empty() {
223 joined.push(' ');
224 }
225 joined.push_str(segment.as_ref());
226 }
227 joined
228 }
229
230 /// Merges multiple style attribute values into a single `Self`.
231 ///
232 /// Each input value is expected to be a style string (`Text`) or a reactive
233 /// `Signal<String>` producing a style string. All non-empty style strings are
234 /// joined with spaces into a final combined style attribute.
235 /// If any value is signal-backed, the result becomes a `Signal` attribute
236 /// carrying the freshly merged value for this render; the enclosing
237 /// dynamic node re-renders on any constituent signal change (see
238 /// [`AttributeValue::reactive`]).
239 ///
240 /// # Arguments
241 ///
242 /// - `&[Self]` - The style attribute values to merge.
243 ///
244 /// # Returns
245 ///
246 /// - `Self` - A merged attribute value containing the combined CSS style string.
247 pub fn merge_style(values: &[Self]) -> Self {
248 let has_signal: bool = values
249 .iter()
250 .any(|value: &Self| matches!(value, Self::Signal(_)));
251 if has_signal {
252 let owned_values: Vec<Self> = values.to_vec();
253 let compute: Box<dyn Fn() -> String> =
254 Box::new(move || Self::join_style_segments(&owned_values));
255 let attr_signal: Signal<String> = Signal::create(compute());
256 return Self::Signal(attr_signal);
257 }
258 Self::Text(Self::join_style_segments(values))
259 }
260
261 /// Joins style attribute values into a single space-separated string.
262 ///
263 /// OPT-20: same single-`String` approach as `join_class_segments`
264 /// (amortized growth), specialised to the `Text` + `Signal` cases that
265 /// style merging uses. Avoids the intermediate `Vec<String>` plus
266 /// `join(" ")` round-trip of the previous implementation, dropping
267 /// per-render allocation count from `N + 2` to `1`.
268 ///
269 /// # Arguments
270 ///
271 /// - `&[Self]` - The style attribute values to merge, in order.
272 ///
273 /// # Returns
274 ///
275 /// - `String` - The merged CSS declarations, segments separated by a
276 /// single space; empty when no segment carried a declaration.
277 fn join_style_segments(values: &[Self]) -> String {
278 let mut joined: String = String::new();
279 for value in values.iter() {
280 let segment: Cow<'_, str> = match value {
281 Self::Text(text_value) => {
282 if text_value.is_empty() {
283 continue;
284 }
285 Cow::Borrowed(text_value.as_str())
286 }
287 Self::Signal(signal) => {
288 let current: String = signal.get();
289 if current.is_empty() {
290 continue;
291 }
292 Cow::Owned(current)
293 }
294 _ => continue,
295 };
296 if !joined.is_empty() {
297 joined.push(' ');
298 }
299 joined.push_str(segment.as_ref());
300 }
301 joined
302 }
303
304 /// Converts a bool signal into a reactive boolean attribute value.
305 ///
306 /// Produces `AttributeValue::BoolSignal` directly: the renderer writes
307 /// `"true"` / `"false"` and subscribes the source signal to the element
308 /// with no intermediate mapping signal (the previous `Signal<String>`
309 /// bridge) and no per-render subscription.
310 ///
311 /// # Arguments
312 ///
313 /// - `Signal<bool>` - The source boolean signal.
314 ///
315 /// # Returns
316 ///
317 /// - `AttributeValue` - A `BoolSignal` wrapping the source signal.
318 pub(crate) fn bool_to_attr(source: Signal<bool>) -> AttributeValue {
319 AttributeValue::BoolSignal(source)
320 }
321}
322
323/// Visual equality comparison for attribute values.
324///
325/// Compares values by their visual output rather than identity. `Signal`
326/// values are compared by their current resolved string (borrow-compared
327/// via `Signal::with`, no `String` clones); when both signals share the
328/// same slab slot, they are always considered **unequal** — the patch walk
329/// recognises that case separately and skips the DOM write because the
330/// mount-time signal binding already owns the attribute's DOM value (see
331/// `patch_attributes`), while the unequal verdict keeps `visual_eq` from
332/// masking a value change between VDOM snapshots.
333/// `Event` values are always considered equal (re-binding is handled by the
334/// handler registry), and `Css` values are compared by class name.
335impl PartialEq for AttributeValue {
336 /// Compares two attribute values for visual equality.
337 ///
338 /// # Arguments
339 ///
340 /// - `&Self` - The first attribute value.
341 /// - `&Self` - The second attribute value.
342 ///
343 /// # Returns
344 ///
345 /// - `bool` - `true` if the values are visually equal.
346 fn eq(&self, other: &Self) -> bool {
347 match (self, other) {
348 (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
349 (Self::StaticText(old_value), Self::StaticText(new_value)) => old_value == new_value,
350 (Self::Text(old_value), Self::StaticText(new_value)) => old_value == new_value,
351 (Self::StaticText(old_value), Self::Text(new_value)) => old_value == new_value,
352 (Self::Signal(old_signal), Self::Signal(new_signal)) => {
353 if old_signal.get_inner() == new_signal.get_inner() {
354 return false;
355 }
356 old_signal.with(|old_value: &String| {
357 new_signal.with(|new_value: &String| old_value == new_value)
358 })
359 }
360 (Self::Signal(old_signal), Self::Text(new_value)) => {
361 old_signal.with(|old_value: &String| old_value == new_value)
362 }
363 (Self::Text(old_value), Self::Signal(new_signal)) => {
364 new_signal.with(|new_value: &String| old_value == new_value)
365 }
366 (Self::BoolSignal(old_signal), Self::BoolSignal(new_signal)) => {
367 if old_signal.get_inner() == new_signal.get_inner() {
368 return false;
369 }
370 old_signal.get() == new_signal.get()
371 }
372 (Self::Event(_), Self::Event(_)) => true,
373 (Self::Css(old_class), Self::Css(new_class)) => {
374 old_class.get_name() == new_class.get_name()
375 }
376 (Self::CssRef(old_class), Self::CssRef(new_class)) => {
377 old_class.get_name() == new_class.get_name()
378 }
379 (Self::CssRef(old_class), Self::Css(new_class)) => {
380 old_class.get_name() == new_class.get_name()
381 }
382 (Self::Css(old_class), Self::CssRef(new_class)) => {
383 old_class.get_name() == new_class.get_name()
384 }
385 (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
386 _ => false,
387 }
388 }
389}
390
391/// Visual equality comparison for attribute entries.
392///
393/// Two attribute entries are equal when their names match and their values
394/// are visually equal as defined by `AttributeValue::eq`.
395impl PartialEq for AttributeEntry {
396 /// Compares two attribute entries for visual equality.
397 ///
398 /// # Arguments
399 ///
400 /// - `&Self` - The first attribute entry.
401 /// - `&Self` - The second attribute entry.
402 ///
403 /// # Returns
404 ///
405 /// - `bool` - `true` if both names and values match.
406 fn eq(&self, other: &Self) -> bool {
407 self.get_name() == other.get_name() && self.get_value() == other.get_value()
408 }
409}
410
411/// Visual equality comparison for CSS classes.
412///
413/// Two CSS classes are considered equal when their class names match,
414/// since the name uniquely identifies the visual style rule.
415impl PartialEq for Css {
416 /// Compares two CSS classes by name.
417 ///
418 /// # Arguments
419 ///
420 /// - `&Self` - The first CSS class.
421 /// - `&Self` - The second CSS class.
422 ///
423 /// # Returns
424 ///
425 /// - `bool` - `true` if the class names match.
426 fn eq(&self, other: &Self) -> bool {
427 self.get_name() == other.get_name()
428 }
429}
430
431/// Implementation of Css construction and style injection.
432impl Css {
433 /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
434 ///
435 /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
436 /// This is used by the `class!` macro for fully static class definitions
437 /// where pseudo rules can be computed at compile time.
438 ///
439 /// # Arguments
440 ///
441 /// - `I` - The serialized pseudo rules string.
442 ///
443 /// # Returns
444 ///
445 /// - `Vec<PseudoRule>` - The parsed pseudo rules.
446 pub fn parse_pseudo_rules<I>(input: I) -> Vec<PseudoRule>
447 where
448 I: AsRef<str>,
449 {
450 let mut remaining: &str = input.as_ref();
451 let mut rules: Vec<PseudoRule> = Vec::new();
452 while !remaining.is_empty() {
453 let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
454 let Some(selector_end_index) = selector_end else {
455 break;
456 };
457 let selector: &str = &remaining[..selector_end_index];
458 let after_selector: &str = remaining[selector_end_index..]
459 .strip_prefix(CSS_RULE_OPEN)
460 .unwrap_or_default();
461 let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
462 let Some(style_end_index) = style_end else {
463 break;
464 };
465 let style: &str = &after_selector[..style_end_index];
466 if !selector.is_empty() && !style.is_empty() {
467 rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
468 }
469 remaining = after_selector[style_end_index..]
470 .strip_prefix(CHAR_CSS_RULE_CLOSE)
471 .unwrap_or_default();
472 }
473 rules
474 }
475
476 /// Parses media query rules from a compact serialization string.
477 ///
478 /// The serialization format is:
479 /// `@media query { key: value; ::selector { key: value; } }@media query2 { ... }`
480 /// This is used by the `class!` macro for fully static class definitions
481 /// where media rules can be computed at compile time.
482 /// Supports nested pseudo-element blocks inside media query blocks.
483 ///
484 /// # Arguments
485 ///
486 /// - `S` - The serialized media rules string.
487 ///
488 /// # Returns
489 ///
490 /// - `Vec<MediaRule>` - The parsed media rules.
491 pub fn parse_media_rules<S>(input: S) -> Vec<MediaRule>
492 where
493 S: AsRef<str>,
494 {
495 let input: &str = input.as_ref();
496 let mut rules: Vec<MediaRule> = Vec::new();
497 let mut remaining: &str = input;
498 while !remaining.is_empty() {
499 if !remaining.starts_with(CSS_MEDIA_PREFIX) {
500 break;
501 }
502 let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
503 let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
504 let Some(query_end_index) = query_end else {
505 break;
506 };
507 let query: &str = &after_prefix[..query_end_index];
508 let after_query: &str = after_prefix[query_end_index..]
509 .strip_prefix(CSS_RULE_OPEN)
510 .unwrap_or_default();
511 let mut depth: usize = 1;
512 let mut close_pos: usize = 0;
513 for (index, char_value) in after_query.char_indices() {
514 if char_value == '{' {
515 depth += 1;
516 } else if char_value == '}' {
517 depth -= 1;
518 if depth == 0 {
519 close_pos = index;
520 break;
521 }
522 }
523 }
524 if close_pos == 0 {
525 break;
526 }
527 let body: &str = &after_query[..close_pos];
528 let (style, pseudo_rules): (String, Vec<PseudoRule>) = Self::parse_media_body(body);
529 if !query.is_empty() && (!style.is_empty() || !pseudo_rules.is_empty()) {
530 rules.push(MediaRule::new(query.to_string(), style, pseudo_rules));
531 }
532 remaining = after_query[close_pos..]
533 .strip_prefix(CHAR_CSS_RULE_CLOSE)
534 .unwrap_or_default();
535 }
536 rules
537 }
538
539 /// Parses the body of a media rule, separating top-level style declarations
540 /// from nested pseudo-element blocks.
541 ///
542 /// # Arguments
543 ///
544 /// - `&str` - The media rule body content (between the outer braces).
545 ///
546 /// # Returns
547 ///
548 /// - `(String, Vec<PseudoRule>)` - A tuple of the style string and pseudo rules.
549 fn parse_media_body(body: &str) -> (String, Vec<PseudoRule>) {
550 let mut style_parts: String = String::new();
551 let mut pseudo_rules: Vec<PseudoRule> = Vec::new();
552 let mut remaining: &str = body;
553 while !remaining.is_empty() {
554 let brace_pos: Option<usize> = remaining.find('{');
555 match brace_pos {
556 Some(pos) => {
557 let before_brace: &str = remaining[..pos].trim();
558 if before_brace.starts_with("::") || before_brace.starts_with(':') {
559 let selector: &str = before_brace;
560 let after_brace: &str = &remaining[pos + 1..];
561 let mut depth: usize = 1;
562 let mut close_pos: usize = 0;
563 for (index, char_value) in after_brace.char_indices() {
564 if char_value == '{' {
565 depth += 1;
566 } else if char_value == '}' {
567 depth -= 1;
568 if depth == 0 {
569 close_pos = index;
570 break;
571 }
572 }
573 }
574 if close_pos > 0 {
575 let inner_style: &str = after_brace[..close_pos].trim();
576 if !selector.is_empty() && !inner_style.is_empty() {
577 pseudo_rules.push(PseudoRule::new(
578 selector.to_string(),
579 inner_style.to_string(),
580 ));
581 }
582 remaining = after_brace[close_pos + 1..].trim_start();
583 continue;
584 }
585 break;
586 } else {
587 style_parts.push_str(before_brace);
588 style_parts.push(' ');
589 let after_brace: &str = &remaining[pos + 1..];
590 let mut depth: usize = 1;
591 let mut close_pos: usize = 0;
592 for (index, char_value) in after_brace.char_indices() {
593 if char_value == '{' {
594 depth += 1;
595 } else if char_value == '}' {
596 depth -= 1;
597 if depth == 0 {
598 close_pos = index;
599 break;
600 }
601 }
602 }
603 if close_pos > 0 {
604 style_parts.push_str(after_brace[..close_pos].trim());
605 style_parts.push(' ');
606 remaining = after_brace[close_pos + 1..].trim_start();
607 continue;
608 }
609 break;
610 }
611 }
612 None => {
613 style_parts.push_str(remaining.trim());
614 break;
615 }
616 }
617 }
618 (style_parts.trim().to_string(), pseudo_rules)
619 }
620
621 /// Injects this class's styles into the DOM if not already present.
622 ///
623 /// Uses a global `HashSet` to track injected class names, avoiding the
624 /// expensive `existing_css.contains(css)` full-text search on every call.
625 /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
626 /// then appends them directly to the `<style>` element via
627 /// `append_child` with a new text node — no read-modify-write of the
628 /// entire stylesheet content.
629 ///
630 /// # Panics
631 ///
632 /// Panics if `window()` or `document()` is unavailable on the current platform.
633 pub fn inject_style(&self) {
634 let class_name: &String = self.get_name();
635 if InjectedClassesCell::is_injected(class_name) {
636 return;
637 }
638 InjectedClassesCell::mark_injected(class_name);
639 let raw_name: String = self.get_name().clone();
640 let mut escaped_name: String = String::with_capacity(raw_name.len() * 2);
641 for ch in raw_name.chars() {
642 if ch.is_ascii_alphanumeric() || ch == CHAR_HYPHEN || ch == CHAR_UNDERSCORE {
643 escaped_name.push(ch);
644 } else {
645 escaped_name.push(CHAR_CSS_ESCAPE);
646 escaped_name.push(ch);
647 }
648 }
649 let mut css_text: String = format!(
650 "{CHAR_CSS_CLASS_PREFIX}{escaped_name}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
651 self.get_style()
652 );
653 for pseudo_rule in self.get_pseudo_rules() {
654 if !pseudo_rule.get_style().is_empty() {
655 css_text = format!(
656 "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
657 escaped_name,
658 pseudo_rule.get_selector(),
659 pseudo_rule.get_style()
660 );
661 }
662 }
663 for media_rule in self.get_media_rules() {
664 if !media_rule.get_query().is_empty() {
665 let mut media_body: String = format!(
666 "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
667 escaped_name,
668 media_rule.get_style()
669 );
670 for pseudo_rule in media_rule.get_pseudo_rules() {
671 if !pseudo_rule.get_style().is_empty() {
672 media_body = format!(
673 "{media_body} {CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
674 escaped_name,
675 pseudo_rule.get_selector(),
676 pseudo_rule.get_style()
677 );
678 }
679 }
680 css_text = format!(
681 "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CSS_MEDIA_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
682 media_rule.get_query(),
683 media_body
684 );
685 }
686 }
687 Self::append_css(&css_text);
688 }
689
690 /// Appends CSS text directly to the shared `<style>` element.
691 ///
692 /// Creates a new text node and appends it as a child of the `<style>`
693 /// element, avoiding the read-modify-write pattern of reading the entire
694 /// `innerText`, concatenating, and setting it back.
695 ///
696 /// # Arguments
697 ///
698 /// - `&str` - The CSS text to append.
699 ///
700 /// The `cfg!` guard matches the one `Scheduler::current_time` already
701 /// uses: `window()` reads a wasm-only imported static, so on a non-wasm
702 /// target it panics with "cannot access imported statics on non-wasm
703 /// targets" rather than returning `None`. That panic short-circuits the
704 /// graceful `match window() { None => return }` below, and it fires from
705 /// `OnceLock<Css>::get_or_init` — the first time any `c_*()` class
706 /// function is called. Every styled view component calls one, so
707 /// without this guard none of them can be rendered in a native
708 /// `cargo test` process. On wasm the guard is a constant `true` and
709 /// this function behaves exactly as before.
710 fn append_css(css_text: &str) {
711 if !cfg!(target_arch = "wasm32") {
712 return;
713 }
714 let style_id: &str = EUV_CSS_INJECTED_ID;
715 let window_value: Window = match window() {
716 Some(window_instance) => window_instance,
717 None => return,
718 };
719 let document: Document = match window_value.document() {
720 Some(document_instance) => document_instance,
721 None => return,
722 };
723 let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
724 Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
725 Ok(element) => element,
726 Err(_err) => return,
727 },
728 None => {
729 let created: Element = match document.create_element(STYLE_TAG) {
730 Ok(element) => element,
731 Err(_err) => return,
732 };
733 let style_element_from_id: HtmlStyleElement =
734 match created.dyn_into::<HtmlStyleElement>() {
735 Ok(element) => element,
736 Err(_err) => return,
737 };
738 style_element_from_id.set_id(style_id);
739 if let Some(head) = document.head() {
740 let _: Result<Node, JsValue> = head.append_child(&style_element_from_id);
741 }
742 style_element_from_id
743 }
744 };
745 if !css_text.is_empty() {
746 let text_node: Text = document.create_text_node(css_text);
747 let _: Result<Node, JsValue> = style_element.append_child(&text_node);
748 }
749 }
750
751 /// Builds a CSS style string from an array of key-value pairs.
752 ///
753 /// This function is used by the `html!` macro to convert static `style:`
754 /// attributes into a CSS string without allocating intermediate objects.
755 ///
756 /// # Arguments
757 ///
758 /// - `&[(K, V)]` - An array of CSS property name-value pairs.
759 ///
760 /// # Returns
761 ///
762 /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
763 pub fn style_string<K, V>(props: &[(K, V)]) -> String
764 where
765 K: AsRef<str>,
766 V: AsRef<str>,
767 {
768 // Single pre-sized `String` instead of per-prop `format!` +
769 // intermediate `Vec<String>` + `join` (was N + 2 allocations).
770 let total_len: usize = props
771 .iter()
772 .map(|(key, value): &(K, V)| {
773 key.as_ref().len() + CSS_PROP_SEPARATOR.len() + value.as_ref().len() + 2
774 })
775 .sum();
776 let mut out: String = String::with_capacity(total_len);
777 for (index, (key, value)) in props.iter().enumerate() {
778 if index > 0 {
779 out.push_str(CHAR_SPACE);
780 }
781 out.push_str(key.as_ref());
782 out.push_str(CSS_PROP_SEPARATOR);
783 out.push_str(value.as_ref());
784 out.push(CHAR_CSS_DECL_TERMINATOR);
785 }
786 out
787 }
788
789 /// Builds a stable suffix for a class name from a dynamic parameter value.
790 ///
791 /// Used by the `class!` macro when a parameter is wrapped in `{}` in the
792 /// class body. Wrapping a parameter opts it into value-dependent class
793 /// names, so each distinct value can inject its own CSS rule.
794 ///
795 /// # Arguments
796 ///
797 /// - `&str` - The dynamic parameter value.
798 ///
799 /// # Returns
800 ///
801 /// - `String` - A stable hexadecimal suffix for the class name.
802 pub fn param_class_name(value: &str) -> String {
803 let mut hash: u64 = CLASS_PARAM_HASH_FNV_OFFSET;
804 for byte in value.as_bytes() {
805 hash ^= u64::from(*byte);
806 hash = hash.wrapping_mul(CLASS_PARAM_HASH_FNV_PRIME);
807 }
808 format!("{hash:x}")
809 }
810
811 /// Builds a CSS style string from owned key-value pairs.
812 ///
813 /// Used by the `html!` macro for reactive style attributes (with `if`
814 /// conditions) where values are computed at runtime.
815 ///
816 /// # Arguments
817 ///
818 /// - `&[(String, String)]` - An array of owned CSS property name-value pairs.
819 ///
820 /// # Returns
821 ///
822 /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
823 pub fn style_string_owned(props: &[(String, String)]) -> String {
824 // Single pre-sized `String` (same shape as `style_string`).
825 let total_len: usize = props
826 .iter()
827 .map(|(key, value): &(String, String)| {
828 key.len() + CSS_PROP_SEPARATOR.len() + value.len() + 2
829 })
830 .sum();
831 let mut out: String = String::with_capacity(total_len);
832 for (index, (key, value)) in props.iter().enumerate() {
833 if index > 0 {
834 out.push_str(CHAR_SPACE);
835 }
836 out.push_str(key);
837 out.push_str(CSS_PROP_SEPARATOR);
838 out.push_str(value);
839 out.push(CHAR_CSS_DECL_TERMINATOR);
840 }
841 out
842 }
843
844 /// Injects CSS text into the shared `<style>` element in the DOM.
845 ///
846 /// Delegates to [`Css::append_css`] for the actual DOM append.
847 /// Unlike the previous implementation, this does not read the existing
848 /// stylesheet content or perform a full-text `contains` search.
849 ///
850 /// # Arguments
851 ///
852 /// - `S` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
853 ///
854 /// # Panics
855 ///
856 /// Panics if `window()` or `document()` is unavailable on the current platform.
857 pub fn inject_css<S>(css_text: S)
858 where
859 S: AsRef<str>,
860 {
861 let css_text: &str = css_text.as_ref();
862 Self::append_css(css_text);
863 }
864}
865
866/// Displays the CSS class name.
867///
868/// This enables `format!("{css}")` to produce the class name string,
869/// which is required for reactive `if` conditions in `class:` attributes.
870impl Display for Css {
871 /// Formats the CSS class as its name string.
872 ///
873 /// # Arguments
874 ///
875 /// - `&mut Formatter<'_>` - The formatter.
876 ///
877 /// # Returns
878 ///
879 /// - `fmt::Result` - The formatting result.
880 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
881 write!(formatter, "{}", self.get_name())
882 }
883}