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