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