euv_core/vdom/attribute/
impl.rs1use crate::*;
2
3impl PartialEq for AttributeValue {
10 fn eq(&self, other: &Self) -> bool {
21 match (self, other) {
22 (AttributeValue::Text(old_val), AttributeValue::Text(new_val)) => old_val == new_val,
23 (AttributeValue::Signal(old_sig), AttributeValue::Signal(new_sig)) => {
24 old_sig.get() == new_sig.get()
25 }
26 (AttributeValue::Signal(old_sig), AttributeValue::Text(new_val)) => {
27 old_sig.get() == *new_val
28 }
29 (AttributeValue::Text(old_val), AttributeValue::Signal(new_sig)) => {
30 *old_val == new_sig.get()
31 }
32 (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
33 (AttributeValue::Css(old_css), AttributeValue::Css(new_css)) => {
34 old_css.get_name() == new_css.get_name()
35 }
36 (AttributeValue::Dynamic(old_dyn), AttributeValue::Dynamic(new_dyn)) => {
37 old_dyn == new_dyn
38 }
39 _ => false,
40 }
41 }
42}
43
44impl PartialEq for AttributeEntry {
49 fn eq(&self, other: &Self) -> bool {
60 self.get_name() == other.get_name() && self.get_value() == other.get_value()
61 }
62}
63
64impl PartialEq for CssClass {
69 fn eq(&self, other: &Self) -> bool {
80 self.get_name() == other.get_name()
81 }
82}
83
84impl Style {
86 pub fn property<N, V>(mut self, name: N, value: V) -> Self
100 where
101 N: AsRef<str>,
102 V: AsRef<str>,
103 {
104 self.get_mut_properties().push(StyleProperty::new(
105 name.as_ref().replace('_', "-"),
106 value.as_ref().to_string(),
107 ));
108 self
109 }
110
111 pub fn to_css_string(&self) -> String {
117 self.get_properties()
118 .iter()
119 .map(|style: &StyleProperty| format!("{}: {};", style.get_name(), style.get_value()))
120 .collect::<Vec<String>>()
121 .join(" ")
122 }
123
124 pub fn create_style_string(props: &[(&str, &str)]) -> String {
139 let mut result: String = String::new();
140 for (key, value) in props {
141 if !result.is_empty() {
142 result.push(' ');
143 }
144 result.push_str(&key.replace('_', "-"));
145 result.push_str(": ");
146 result.push_str(value);
147 result.push(';');
148 }
149 result
150 }
151}
152
153impl Default for Style {
155 fn default() -> Self {
161 Self::new(Vec::new())
162 }
163}
164
165impl CssClass {
167 pub fn new(name: String, style: String) -> Self {
180 let mut css_class: CssClass = CssClass::default();
181 css_class.set_name(name);
182 css_class.set_style(style);
183 css_class.inject_style();
184 css_class
185 }
186
187 pub fn new_with_rules(
203 name: String,
204 style: String,
205 pseudo_rules: Vec<PseudoRule>,
206 media_rules: Vec<MediaRule>,
207 ) -> Self {
208 let mut css_class: CssClass = CssClass::default();
209 css_class.set_name(name);
210 css_class.set_style(style);
211 css_class.set_pseudo_rules(pseudo_rules);
212 css_class.set_media_rules(media_rules);
213 css_class.inject_style();
214 css_class
215 }
216
217 pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
231 let mut rules: Vec<PseudoRule> = Vec::new();
232 let mut remaining: &str = input;
233 while !remaining.is_empty() {
234 let selector_end: Option<usize> = remaining.find(" { ");
235 let Some(sel_end) = selector_end else {
236 break;
237 };
238 let selector: &str = &remaining[..sel_end];
239 let after_selector: &str = remaining[sel_end..].strip_prefix(" { ").unwrap_or("");
240 let style_end: Option<usize> = after_selector.find('}');
241 let Some(st_end) = style_end else {
242 break;
243 };
244 let style: &str = &after_selector[..st_end];
245 if !selector.is_empty() && !style.is_empty() {
246 rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
247 }
248 remaining = after_selector[st_end..].strip_prefix('}').unwrap_or("");
249 }
250 rules
251 }
252
253 pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
267 let mut rules: Vec<MediaRule> = Vec::new();
268 let mut remaining: &str = input;
269 while !remaining.is_empty() {
270 if !remaining.starts_with("@media ") {
271 break;
272 }
273 let after_prefix: &str = remaining.strip_prefix("@media ").unwrap_or("");
274 let query_end: Option<usize> = after_prefix.find(" { ");
275 let Some(q_end) = query_end else {
276 break;
277 };
278 let query: &str = &after_prefix[..q_end];
279 let after_query: &str = after_prefix[q_end..].strip_prefix(" { ").unwrap_or("");
280 let style_end: Option<usize> = after_query.find('}');
281 let Some(st_end) = style_end else {
282 break;
283 };
284 let style: &str = &after_query[..st_end];
285 if !query.is_empty() && !style.is_empty() {
286 rules.push(MediaRule::new(query.to_string(), style.to_string()));
287 }
288 remaining = after_query[st_end..].strip_prefix('}').unwrap_or("");
289 }
290 rules
291 }
292
293 pub fn inject_style(&self) {
304 #[cfg(target_arch = "wasm32")]
305 {
306 let style_id: &str = "euv-css-injected";
307 let document: Document = window()
308 .expect("no global window exists")
309 .document()
310 .expect("no document exists");
311 let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
312 Some(el) => el.dyn_into::<HtmlStyleElement>().unwrap(),
313 None => {
314 let el: HtmlStyleElement = document
315 .create_element("style")
316 .unwrap()
317 .dyn_into::<HtmlStyleElement>()
318 .unwrap();
319 el.set_id(style_id);
320 let keyframes: &str = "@keyframes euv-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes euv-fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes euv-scale-in { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes euv-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } } @keyframes euv-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } } @keyframes euv-slide-left { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes euv-fade-in-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }";
321 let global: &str = "html, body, #app { height: 100%; margin: 0; padding: 0; overflow: hidden; } * { -webkit-tap-highlight-color: transparent; }";
322 let media_queries: &str = "@media (max-width: 767px) { .c_app_nav { display: none; } .c_app_main { padding: 20px 16px; max-width: 100%; } .c_page_title { font-size: 22px; } .c_page_subtitle { font-size: 14px; } .c_card { padding: 16px; margin: 12px 0; border-radius: 10px; } .c_card_title { font-size: 16px; } .c_form_grid { grid-template-columns: 1fr; } .c_browser_api_row { grid-template-columns: 1fr; } .c_modal_content { max-width: 100%; width: calc(100% - 32px); border-radius: 16px; max-height: 85vh; overflow-y: auto; } .c_modal_overlay { align-items: center; justify-content: center; } .c_event_stats { gap: 12px; flex-wrap: wrap; } .c_event_section_row { gap: 12px; flex-wrap: wrap; } .c_event_section_col { min-width: 100%; } .c_counter_value { font-size: 20px; } .c_timer_value { font-size: 36px; } .c_not_found_code { font-size: 56px; } .c_not_found_container { padding: 40px 20px; } .c_list_input_row { flex-direction: column; } .c_vconsole_button { bottom: 16px; right: 16px; width: 44px; height: 44px; border-radius: 12px; } .c_tab_bar { flex-wrap: wrap; } .c_primary_button { padding: 10px 18px; font-size: 14px; } .c_badge { padding: 4px 10px; font-size: 11px; } .c_badge_outline { padding: 4px 10px; font-size: 11px; } .c_browser_info_grid { grid-template-columns: 1fr; } .c_anim_spin { font-size: 36px; } .c_anim_spin_stopped { font-size: 36px; } .c_anim_pulse { font-size: 36px; } .c_anim_pulse_stopped { font-size: 36px; } }";
323 el.set_inner_text(&format!("{} {} {}", global, keyframes, media_queries));
324 document.head().unwrap().append_child(&el).unwrap();
325 el
326 }
327 };
328 let existing_css: String = style_element.inner_text();
329 let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
330 let mut new_css: String = existing_css.clone();
331 if !existing_css.contains(&class_rule) {
332 new_css = if new_css.is_empty() {
333 class_rule
334 } else {
335 format!("{}\n{}", new_css, class_rule)
336 };
337 }
338 for pseudo_rule in self.get_pseudo_rules() {
339 let pseudo_rule_str: String = format!(
340 ".{}{} {{ {} }}",
341 self.get_name(),
342 pseudo_rule.get_selector(),
343 pseudo_rule.get_style()
344 );
345 if !pseudo_rule.get_style().is_empty() && !existing_css.contains(&pseudo_rule_str) {
346 new_css = if new_css.is_empty() {
347 pseudo_rule_str
348 } else {
349 format!("{}\n{}", new_css, pseudo_rule_str)
350 };
351 }
352 }
353 for media_rule in self.get_media_rules() {
354 let media_rule_str: String = format!(
355 "@media {} {{ .{} {{ {} }} }}",
356 media_rule.get_query(),
357 self.get_name(),
358 media_rule.get_style()
359 );
360 if !media_rule.get_query().is_empty() && !existing_css.contains(&media_rule_str) {
361 new_css = if new_css.is_empty() {
362 media_rule_str
363 } else {
364 format!("{}\n{}", new_css, media_rule_str)
365 };
366 }
367 }
368 if new_css != existing_css {
369 style_element.set_inner_text(&new_css);
370 }
371 }
372 }
373}
374
375impl std::fmt::Display for CssClass {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390 write!(f, "{}", self.get_name())
391 }
392}