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