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