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