euv_core/vdom/attribute/impl.rs
1use crate::*;
2
3/// SAFETY: `InjectedClassesCell` is only used in single-threaded WASM contexts.
4unsafe impl Sync for InjectedClassesCell {}
5
6/// Visual equality comparison for attribute values.
7///
8/// Compares values by their visual output rather than identity. `Signal`
9/// values are compared by their current resolved string; when both signals
10/// share the same inner pointer, they are always considered **unequal**
11/// because the signal may have mutated between VDOM snapshots and `.get()`
12/// would return the same current value for both, masking the change.
13/// `Event` values are always considered equal (re-binding is handled by the
14/// handler registry), and `CssClass` values are compared by class name.
15impl PartialEq for AttributeValue {
16 /// Compares two attribute values for visual equality.
17 ///
18 /// # Arguments
19 ///
20 /// - `&Self` - The first attribute value.
21 /// - `&Self` - The second attribute value.
22 ///
23 /// # Returns
24 ///
25 /// - `bool` - `true` if the values are visually equal.
26 fn eq(&self, other: &Self) -> bool {
27 match (self, other) {
28 (AttributeValue::Text(old_value), AttributeValue::Text(new_value)) => {
29 old_value == new_value
30 }
31 (AttributeValue::Signal(old_signal), AttributeValue::Signal(new_signal)) => {
32 if old_signal.get_inner_addr() == new_signal.get_inner_addr() {
33 return false;
34 }
35 old_signal.get() == new_signal.get()
36 }
37 (AttributeValue::Signal(old_signal), AttributeValue::Text(new_value)) => {
38 old_signal.get() == *new_value
39 }
40 (AttributeValue::Text(old_value), AttributeValue::Signal(new_signal)) => {
41 *old_value == new_signal.get()
42 }
43 (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
44 (AttributeValue::Css(old_class), AttributeValue::Css(new_class)) => {
45 old_class.get_name() == new_class.get_name()
46 }
47 (AttributeValue::Dynamic(old_dynamic), AttributeValue::Dynamic(new_dynamic)) => {
48 old_dynamic == new_dynamic
49 }
50 _ => false,
51 }
52 }
53}
54
55/// Visual equality comparison for attribute entries.
56///
57/// Two attribute entries are equal when their names match and their values
58/// are visually equal as defined by `AttributeValue::eq`.
59impl PartialEq for AttributeEntry {
60 /// Compares two attribute entries for visual equality.
61 ///
62 /// # Arguments
63 ///
64 /// - `&Self` - The first attribute entry.
65 /// - `&Self` - The second attribute entry.
66 ///
67 /// # Returns
68 ///
69 /// - `bool` - `true` if both names and values match.
70 fn eq(&self, other: &Self) -> bool {
71 self.get_name() == other.get_name() && self.get_value() == other.get_value()
72 }
73}
74
75/// Visual equality comparison for CSS classes.
76///
77/// Two CSS classes are considered equal when their class names match,
78/// since the name uniquely identifies the visual style rule.
79impl PartialEq for CssClass {
80 /// Compares two CSS classes by name.
81 ///
82 /// # Arguments
83 ///
84 /// - `&Self` - The first CSS class.
85 /// - `&Self` - The second CSS class.
86 ///
87 /// # Returns
88 ///
89 /// - `bool` - `true` if the class names match.
90 fn eq(&self, other: &Self) -> bool {
91 self.get_name() == other.get_name()
92 }
93}
94
95/// Implementation of style CSS serialization.
96impl Style {
97 /// Adds a style property.
98 ///
99 /// Property names are automatically converted from snake_case to kebab-case
100 /// (e.g., `flex_direction` becomes `flex-direction`).
101 ///
102 /// # Arguments
103 ///
104 /// - `N` - The property name (snake_case will be converted to kebab-case).
105 /// - `V` - The property value.
106 ///
107 /// # Returns
108 ///
109 /// - `Self` - This style with the property added.
110 pub fn property<N, V>(mut self, name: N, value: V) -> Self
111 where
112 N: AsRef<str>,
113 V: AsRef<str>,
114 {
115 self.get_mut_properties().push(StyleProperty::new(
116 name.as_ref().replace('_', "-"),
117 value.as_ref().to_string(),
118 ));
119 self
120 }
121
122 /// Converts the style to a CSS string.
123 ///
124 /// # Returns
125 ///
126 /// - `String` - The CSS string representation.
127 pub fn to_css_string(&self) -> String {
128 self.get_properties()
129 .iter()
130 .map(|style: &StyleProperty| format!("{}: {};", style.get_name(), style.get_value()))
131 .collect::<Vec<String>>()
132 .join(" ")
133 }
134
135 /// Builds a CSS style string from an array of key-value pairs.
136 ///
137 /// This function is used by the `html!` macro to convert static `style:`
138 /// attributes into a CSS string without allocating intermediate `Style`
139 /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
140 /// to kebab-case automatically.
141 ///
142 /// # Arguments
143 ///
144 /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
145 ///
146 /// # Returns
147 ///
148 /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
149 pub fn create_style_string(props: &[(&str, &str)]) -> String {
150 let mut result: String = String::new();
151 for (key, value) in props {
152 if !result.is_empty() {
153 result.push(' ');
154 }
155 result.push_str(&key.replace('_', "-"));
156 result.push_str(": ");
157 result.push_str(value);
158 result.push(';');
159 }
160 result
161 }
162}
163
164/// Provides a default empty style.
165impl Default for Style {
166 /// Returns a default `Style` with no properties.
167 ///
168 /// # Returns
169 ///
170 /// - `Self` - An empty style.
171 fn default() -> Self {
172 Self::new(Vec::new())
173 }
174}
175
176/// Implementation of CssClass construction and style injection.
177impl CssClass {
178 /// Creates a new CSS class with the given name and style declarations.
179 ///
180 /// Automatically injects the styles into the DOM upon creation.
181 ///
182 /// # Arguments
183 ///
184 /// - `String` - The class name.
185 /// - `String` - The CSS style declarations.
186 ///
187 /// # Returns
188 ///
189 /// - `Self` - A new CSS class with injected styles.
190 pub fn new(name: String, style: String) -> Self {
191 let mut css_class: CssClass = CssClass::default();
192 css_class.set_name(name);
193 css_class.set_style(style);
194 css_class.inject_style();
195 css_class
196 }
197
198 /// Creates a new CSS class with the given name, style declarations, and pseudo rules.
199 ///
200 /// Automatically injects the base styles, pseudo-class/pseudo-element rules,
201 /// and media query rules into the DOM upon creation.
202 ///
203 /// # Arguments
204 ///
205 /// - `String` - The class name.
206 /// - `String` - The CSS style declarations.
207 /// - `Vec<PseudoRule>` - The pseudo-class and pseudo-element rules.
208 /// - `Vec<MediaRule>` - The media query rules.
209 ///
210 /// # Returns
211 ///
212 /// - `Self` - A new CSS class with injected styles and pseudo rules.
213 pub fn new_with_rules(
214 name: String,
215 style: String,
216 pseudo_rules: Vec<PseudoRule>,
217 media_rules: Vec<MediaRule>,
218 ) -> Self {
219 let mut css_class: CssClass = CssClass::default();
220 css_class.set_name(name);
221 css_class.set_style(style);
222 css_class.set_pseudo_rules(pseudo_rules);
223 css_class.set_media_rules(media_rules);
224 css_class.inject_style();
225 css_class
226 }
227
228 /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
229 ///
230 /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
231 /// This is used by the `class!` macro for fully static class definitions
232 /// where pseudo rules can be computed at compile time.
233 ///
234 /// # Arguments
235 ///
236 /// - `&str` - The serialized pseudo rules string.
237 ///
238 /// # Returns
239 ///
240 /// - `Vec<PseudoRule>` - The parsed pseudo rules.
241 pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
242 let mut rules: Vec<PseudoRule> = Vec::new();
243 let mut remaining: &str = input;
244 while !remaining.is_empty() {
245 let selector_end: Option<usize> = remaining.find(" { ");
246 let Some(selector_end_index) = selector_end else {
247 break;
248 };
249 let selector: &str = &remaining[..selector_end_index];
250 let after_selector: &str = remaining[selector_end_index..]
251 .strip_prefix(" { ")
252 .unwrap_or("");
253 let style_end: Option<usize> = after_selector.find('}');
254 let Some(style_end_index) = style_end else {
255 break;
256 };
257 let style: &str = &after_selector[..style_end_index];
258 if !selector.is_empty() && !style.is_empty() {
259 rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
260 }
261 remaining = after_selector[style_end_index..]
262 .strip_prefix('}')
263 .unwrap_or("");
264 }
265 rules
266 }
267
268 /// Parses media query rules from a compact serialization string.
269 ///
270 /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
271 /// This is used by the `class!` macro for fully static class definitions
272 /// where media rules can be computed at compile time.
273 ///
274 /// # Arguments
275 ///
276 /// - `&str` - The serialized media rules string.
277 ///
278 /// # Returns
279 ///
280 /// - `Vec<MediaRule>` - The parsed media rules.
281 pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
282 let mut rules: Vec<MediaRule> = Vec::new();
283 let mut remaining: &str = input;
284 while !remaining.is_empty() {
285 if !remaining.starts_with("@media ") {
286 break;
287 }
288 let after_prefix: &str = remaining.strip_prefix("@media ").unwrap_or("");
289 let query_end: Option<usize> = after_prefix.find(" { ");
290 let Some(query_end_index) = query_end else {
291 break;
292 };
293 let query: &str = &after_prefix[..query_end_index];
294 let after_query: &str = after_prefix[query_end_index..]
295 .strip_prefix(" { ")
296 .unwrap_or("");
297 let style_end: Option<usize> = after_query.find('}');
298 let Some(style_end_index) = style_end else {
299 break;
300 };
301 let style: &str = &after_query[..style_end_index];
302 if !query.is_empty() && !style.is_empty() {
303 rules.push(MediaRule::new(query.to_string(), style.to_string()));
304 }
305 remaining = after_query[style_end_index..]
306 .strip_prefix('}')
307 .unwrap_or("");
308 }
309 rules
310 }
311
312 /// Injects this class's styles into the DOM if not already present.
313 ///
314 /// Uses a global `HashSet` to track injected class names, avoiding the
315 /// expensive `existing_css.contains(css)` full-text search on every call.
316 /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
317 /// then appends them directly to the `<style>` element via
318 /// `append_child` with a new text node — no read-modify-write of the
319 /// entire stylesheet content.
320 ///
321 /// # Panics
322 ///
323 /// Panics if `window()` or `document()` is unavailable on the current platform.
324 pub fn inject_style(&self) {
325 if !Self::mark_injected(self.get_name().clone()) {
326 return;
327 }
328 let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
329 let mut css_text: String = class_rule;
330 for pseudo_rule in self.get_pseudo_rules() {
331 if !pseudo_rule.get_style().is_empty() {
332 let pseudo_rule_str: String = format!(
333 ".{}{} {{ {} }}",
334 self.get_name(),
335 pseudo_rule.get_selector(),
336 pseudo_rule.get_style()
337 );
338 css_text = format!("{}\n{}", css_text, pseudo_rule_str);
339 }
340 }
341 for media_rule in self.get_media_rules() {
342 if !media_rule.get_query().is_empty() {
343 let media_rule_str: String = format!(
344 "@media {} {{ .{} {{ {} }} }}",
345 media_rule.get_query(),
346 self.get_name(),
347 media_rule.get_style()
348 );
349 css_text = format!("{}\n{}", css_text, media_rule_str);
350 }
351 }
352 Self::append_css(&css_text);
353 }
354
355 /// Marks a class name as injected in the global `HashSet`.
356 ///
357 /// Returns `false` if the class was already injected (no-op), `true`
358 /// if this is the first injection.
359 ///
360 /// # Arguments
361 ///
362 /// - `String` - The class name to mark as injected.
363 ///
364 /// # Returns
365 ///
366 /// - `bool` - `true` if newly injected, `false` if already present.
367 fn mark_injected(class_name: String) -> bool {
368 mark_injected_class(class_name)
369 }
370
371 /// Appends CSS text directly to the shared `<style>` element.
372 ///
373 /// Creates a new text node and appends it as a child of the `<style>`
374 /// element, avoiding the read-modify-write pattern of reading the entire
375 /// `innerText`, concatenating, and setting it back.
376 ///
377 /// # Arguments
378 ///
379 /// - `&str` - The CSS text to append.
380 ///
381 /// # Panics
382 ///
383 /// Panics if `window()` or `document()` is unavailable on the current platform.
384 fn append_css(css_text: &str) {
385 let style_id: &str = "euv-css-injected";
386 let document: Document = window()
387 .expect("no global window exists")
388 .document()
389 .expect("no document exists");
390 let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
391 Some(existing_element) => existing_element.dyn_into::<HtmlStyleElement>().unwrap(),
392 None => {
393 let style_element_from_id: HtmlStyleElement = document
394 .create_element("style")
395 .unwrap()
396 .dyn_into::<HtmlStyleElement>()
397 .unwrap();
398 style_element_from_id.set_id(style_id);
399 document
400 .head()
401 .unwrap()
402 .append_child(&style_element_from_id)
403 .unwrap();
404 style_element_from_id
405 }
406 };
407 if !css_text.is_empty() {
408 let text_node: Text = document.create_text_node(css_text);
409 style_element.append_child(&text_node).unwrap();
410 }
411 }
412
413 /// Injects CSS text into the shared `<style>` element in the DOM.
414 ///
415 /// Delegates to [`CssClass::append_css`] for the actual DOM append.
416 /// Unlike the previous implementation, this does not read the existing
417 /// stylesheet content or perform a full-text `contains` search.
418 ///
419 /// # Arguments
420 ///
421 /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
422 ///
423 /// # Panics
424 ///
425 /// Panics if `window()` or `document()` is unavailable on the current platform.
426 pub fn inject_css(css_text: &str) {
427 Self::append_css(css_text);
428 }
429}
430
431/// Displays the CSS class name.
432///
433/// This enables `format!("{}", css_class)` to produce the class name string,
434/// which is required for reactive `if` conditions in `class:` attributes.
435impl std::fmt::Display for CssClass {
436 /// Formats the CSS class as its name string.
437 ///
438 /// # Arguments
439 ///
440 /// - `&mut Formatter` - The formatter.
441 ///
442 /// # Returns
443 ///
444 /// - `std::fmt::Result` - The formatting result.
445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446 write!(f, "{}", self.get_name())
447 }
448}