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