1use crate::*;
2
3impl PartialEq for AttributeValue {
10 fn eq(&self, other: &Self) -> bool {
11 match (self, other) {
12 (AttributeValue::Text(a_val), AttributeValue::Text(b_val)) => a_val == b_val,
13 (AttributeValue::Signal(a_sig), AttributeValue::Signal(b_sig)) => {
14 a_sig.get() == b_sig.get()
15 }
16 (AttributeValue::Signal(a_sig), AttributeValue::Text(b_val)) => a_sig.get() == *b_val,
17 (AttributeValue::Text(a_val), AttributeValue::Signal(b_sig)) => *a_val == b_sig.get(),
18 (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
19 (AttributeValue::Css(a_css), AttributeValue::Css(b_css)) => {
20 a_css.get_name() == b_css.get_name()
21 }
22 (AttributeValue::Dynamic(a_dyn), AttributeValue::Dynamic(b_dyn)) => a_dyn == b_dyn,
23 _ => false,
24 }
25 }
26}
27
28impl PartialEq for AttributeEntry {
33 fn eq(&self, other: &Self) -> bool {
34 self.get_name() == other.get_name() && self.get_value() == other.get_value()
35 }
36}
37
38impl PartialEq for TextNode {
43 fn eq(&self, other: &Self) -> bool {
44 self.get_content() == other.get_content()
45 }
46}
47
48impl PartialEq for CssClass {
53 fn eq(&self, other: &Self) -> bool {
54 self.get_name() == other.get_name()
55 }
56}
57
58impl PartialEq for VirtualNode {
68 fn eq(&self, other: &Self) -> bool {
69 match (self, other) {
70 (VirtualNode::Text(a_text), VirtualNode::Text(b_text)) => a_text == b_text,
71 (
72 VirtualNode::Element {
73 tag: a_tag,
74 attributes: a_attrs,
75 children: a_children,
76 ..
77 },
78 VirtualNode::Element {
79 tag: b_tag,
80 attributes: b_attrs,
81 children: b_children,
82 ..
83 },
84 ) => {
85 a_tag == b_tag
86 && a_attrs.len() == b_attrs.len()
87 && a_attrs.iter().zip(b_attrs.iter()).all(|(a, b)| a == b)
88 && a_children.len() == b_children.len()
89 && a_children
90 .iter()
91 .zip(b_children.iter())
92 .all(|(a, b)| a == b)
93 }
94 (VirtualNode::Fragment(a_children), VirtualNode::Fragment(b_children)) => {
95 a_children.len() == b_children.len()
96 && a_children
97 .iter()
98 .zip(b_children.iter())
99 .all(|(a, b)| a == b)
100 }
101 (VirtualNode::Dynamic(_), VirtualNode::Dynamic(_)) => true,
102 (VirtualNode::Empty, VirtualNode::Empty) => true,
103 _ => false,
104 }
105 }
106}
107
108impl Attribute {
110 pub fn as_str(&self) -> String {
112 match self {
113 Attribute::AccessKey => "accesskey".to_string(),
114 Attribute::Action => "action".to_string(),
115 Attribute::Alt => "alt".to_string(),
116 Attribute::AriaLabel => "aria-label".to_string(),
117 Attribute::AutoComplete => "autocomplete".to_string(),
118 Attribute::AutoFocus => "autofocus".to_string(),
119 Attribute::Checked => "checked".to_string(),
120 Attribute::Class => "class".to_string(),
121 Attribute::Cols => "cols".to_string(),
122 Attribute::ContentEditable => "contenteditable".to_string(),
123 Attribute::Data(name) => format!("data-{}", name),
124 Attribute::Dir => "dir".to_string(),
125 Attribute::Disabled => "disabled".to_string(),
126 Attribute::Draggable => "draggable".to_string(),
127 Attribute::EncType => "enctype".to_string(),
128 Attribute::For => "for".to_string(),
129 Attribute::Form => "form".to_string(),
130 Attribute::Height => "height".to_string(),
131 Attribute::Hidden => "hidden".to_string(),
132 Attribute::Href => "href".to_string(),
133 Attribute::Id => "id".to_string(),
134 Attribute::Lang => "lang".to_string(),
135 Attribute::Max => "max".to_string(),
136 Attribute::MaxLength => "maxlength".to_string(),
137 Attribute::Method => "method".to_string(),
138 Attribute::Min => "min".to_string(),
139 Attribute::MinLength => "minlength".to_string(),
140 Attribute::Multiple => "multiple".to_string(),
141 Attribute::Name => "name".to_string(),
142 Attribute::Pattern => "pattern".to_string(),
143 Attribute::Placeholder => "placeholder".to_string(),
144 Attribute::ReadOnly => "readonly".to_string(),
145 Attribute::Required => "required".to_string(),
146 Attribute::Rows => "rows".to_string(),
147 Attribute::Selected => "selected".to_string(),
148 Attribute::Size => "size".to_string(),
149 Attribute::SpellCheck => "spellcheck".to_string(),
150 Attribute::Src => "src".to_string(),
151 Attribute::Step => "step".to_string(),
152 Attribute::Style => "style".to_string(),
153 Attribute::TabIndex => "tabindex".to_string(),
154 Attribute::Target => "target".to_string(),
155 Attribute::Title => "title".to_string(),
156 Attribute::Type => "type".to_string(),
157 Attribute::Value => "value".to_string(),
158 Attribute::Width => "width".to_string(),
159 Attribute::Other(name) => name.clone(),
160 }
161 }
162}
163
164impl Default for DynamicNode {
166 fn default() -> Self {
167 let node: DynamicNode = DynamicNode {
168 render_fn: Rc::new(RefCell::new(|| VirtualNode::Empty)),
169 hook_context: HookContext::default(),
170 id: 0_u64,
171 };
172 node
173 }
174}
175
176impl Clone for DynamicNode {
178 fn clone(&self) -> Self {
179 DynamicNode {
180 render_fn: Rc::clone(self.get_render_fn()),
181 hook_context: self.hook_context,
182 id: self.id,
183 }
184 }
185}
186
187impl AsNode for VirtualNode {
189 fn as_node(&self) -> Option<VirtualNode> {
190 Some(self.clone())
191 }
192}
193
194impl AsNode for &VirtualNode {
196 fn as_node(&self) -> Option<VirtualNode> {
197 Some((*self).clone())
198 }
199}
200
201impl AsNode for String {
203 fn as_node(&self) -> Option<VirtualNode> {
204 Some(VirtualNode::Text(TextNode::new(self.clone(), None)))
205 }
206}
207
208impl AsNode for &str {
210 fn as_node(&self) -> Option<VirtualNode> {
211 Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
212 }
213}
214
215impl AsNode for i32 {
217 fn as_node(&self) -> Option<VirtualNode> {
218 Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
219 }
220}
221
222impl AsNode for i64 {
224 fn as_node(&self) -> Option<VirtualNode> {
225 Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
226 }
227}
228
229impl AsNode for usize {
231 fn as_node(&self) -> Option<VirtualNode> {
232 Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
233 }
234}
235
236impl AsNode for f32 {
238 fn as_node(&self) -> Option<VirtualNode> {
239 Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
240 }
241}
242
243impl AsNode for f64 {
245 fn as_node(&self) -> Option<VirtualNode> {
246 Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
247 }
248}
249
250impl AsNode for bool {
252 fn as_node(&self) -> Option<VirtualNode> {
253 Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
254 }
255}
256
257impl<T> AsNode for Signal<T>
259where
260 T: Clone + PartialEq + std::fmt::Display + 'static,
261{
262 fn as_node(&self) -> Option<VirtualNode> {
263 Some(self.as_reactive_text())
264 }
265}
266
267impl IntoNode for VirtualNode {
269 fn into_node(self) -> VirtualNode {
270 self
271 }
272}
273
274impl<F> IntoNode for F
279where
280 F: FnMut() -> VirtualNode + 'static,
281{
282 fn into_node(self) -> VirtualNode {
283 static NEXT_DYNAMIC_ID: std::sync::atomic::AtomicU64 =
284 std::sync::atomic::AtomicU64::new(1_u64);
285 let id: u64 = NEXT_DYNAMIC_ID.fetch_add(1_u64, std::sync::atomic::Ordering::Relaxed);
286 VirtualNode::Dynamic(DynamicNode {
287 render_fn: Rc::new(RefCell::new(self)),
288 hook_context: crate::reactive::create_hook_context(),
289 id,
290 })
291 }
292}
293
294impl IntoNode for String {
296 fn into_node(self) -> VirtualNode {
297 VirtualNode::Text(TextNode::new(self, None))
298 }
299}
300
301impl IntoNode for &str {
303 fn into_node(self) -> VirtualNode {
304 VirtualNode::Text(TextNode::new(self.to_string(), None))
305 }
306}
307
308impl IntoNode for i32 {
310 fn into_node(self) -> VirtualNode {
311 VirtualNode::Text(TextNode::new(self.to_string(), None))
312 }
313}
314
315impl IntoNode for usize {
317 fn into_node(self) -> VirtualNode {
318 VirtualNode::Text(TextNode::new(self.to_string(), None))
319 }
320}
321
322impl IntoNode for bool {
324 fn into_node(self) -> VirtualNode {
325 VirtualNode::Text(TextNode::new(self.to_string(), None))
326 }
327}
328
329impl<T> IntoNode for Signal<T>
331where
332 T: Clone + PartialEq + std::fmt::Display + 'static,
333{
334 fn into_node(self) -> VirtualNode {
335 self.as_reactive_text()
336 }
337}
338
339impl VirtualNode {
341 pub fn needs_patch(old: &VirtualNode, new: &VirtualNode) -> bool {
349 match (old, new) {
350 (VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
351 old_text.get_content() != new_text.get_content()
352 }
353 (
354 VirtualNode::Element {
355 tag: old_tag,
356 attributes: old_attrs,
357 children: old_children,
358 key: _old_key,
359 },
360 VirtualNode::Element {
361 tag: new_tag,
362 attributes: new_attrs,
363 children: new_children,
364 key: _new_key,
365 },
366 ) => {
367 if old_tag != new_tag {
368 return true;
369 }
370 if old_attrs.len() != new_attrs.len() {
371 return true;
372 }
373 for (old_attr, new_attr) in old_attrs.iter().zip(new_attrs.iter()) {
374 if old_attr.get_name() != new_attr.get_name()
375 || old_attr.get_value() != new_attr.get_value()
376 {
377 return true;
378 }
379 }
380 if old_children.len() != new_children.len() {
381 return true;
382 }
383 for (old_child, new_child) in old_children.iter().zip(new_children.iter()) {
384 if Self::needs_patch(old_child, new_child) {
385 return true;
386 }
387 }
388 false
389 }
390 (VirtualNode::Fragment(old_children), VirtualNode::Fragment(new_children)) => {
391 if old_children.len() != new_children.len() {
392 return true;
393 }
394 for (old_child, new_child) in old_children.iter().zip(new_children.iter()) {
395 if Self::needs_patch(old_child, new_child) {
396 return true;
397 }
398 }
399 false
400 }
401 (VirtualNode::Dynamic(old_dyn), VirtualNode::Dynamic(new_dyn)) => {
402 old_dyn.get_id() != new_dyn.get_id()
403 }
404 (VirtualNode::Empty, VirtualNode::Empty) => false,
405 _ => true,
406 }
407 }
408
409 pub fn get_element_node(tag_name: &str) -> Self {
411 VirtualNode::Element {
412 tag: Tag::Element(tag_name.to_string()),
413 attributes: Vec::new(),
414 children: Vec::new(),
415 key: None,
416 }
417 }
418
419 pub fn get_text_node(content: &str) -> Self {
421 VirtualNode::Text(TextNode::new(content.to_string(), None))
422 }
423
424 pub fn with_attribute(mut self, name: &str, value: AttributeValue) -> Self {
426 if let VirtualNode::Element {
427 ref mut attributes, ..
428 } = self
429 {
430 attributes.push(AttributeEntry::new(name.to_string(), value));
431 }
432 self
433 }
434
435 pub fn with_child(mut self, child: VirtualNode) -> Self {
437 if let VirtualNode::Element {
438 ref mut children, ..
439 } = self
440 {
441 children.push(child);
442 }
443 self
444 }
445
446 pub fn is_component(&self) -> bool {
448 matches!(
449 self,
450 VirtualNode::Element {
451 tag: Tag::Component(_),
452 ..
453 }
454 )
455 }
456
457 pub fn tag_name(&self) -> Option<String> {
459 match self {
460 VirtualNode::Element { tag, .. } => match tag {
461 Tag::Element(name) => Some(name.clone()),
462 Tag::Component(name) => Some(name.clone()),
463 },
464 _ => None,
465 }
466 }
467
468 pub fn try_get_prop(&self, name: &Attribute) -> Option<String> {
470 let name_str: String = name.as_str();
471 if let VirtualNode::Element { attributes, .. } = self {
472 for attr in attributes {
473 if attr.get_name() == &name_str {
474 match attr.get_value() {
475 AttributeValue::Text(value) => return Some(value.clone()),
476 AttributeValue::Signal(signal) => return Some(signal.get()),
477 _ => {}
478 }
479 }
480 }
481 }
482 None
483 }
484
485 pub fn try_get_signal_prop(&self, name: &Attribute) -> Option<Signal<String>> {
490 let name_str: String = name.as_str();
491 if let VirtualNode::Element { attributes, .. } = self {
492 for attr in attributes {
493 if attr.get_name() == &name_str
494 && let AttributeValue::Signal(signal) = attr.get_value()
495 {
496 return Some(*signal);
497 }
498 }
499 }
500 None
501 }
502
503 pub fn get_children(&self) -> Vec<VirtualNode> {
505 if let VirtualNode::Element { children, .. } = self {
506 children.clone()
507 } else {
508 Vec::new()
509 }
510 }
511
512 pub fn try_get_text(&self) -> Option<String> {
514 match self {
515 VirtualNode::Text(text_node) => Some(text_node.get_content().clone()),
516 VirtualNode::Element { children, .. } => {
517 children.first().and_then(VirtualNode::try_get_text)
518 }
519 _ => None,
520 }
521 }
522
523 pub fn try_get_event(
525 &self,
526 name: &NativeEventName,
527 ) -> Option<crate::event::NativeEventHandler> {
528 let name_str: String = name.as_str();
529 if let VirtualNode::Element { attributes, .. } = self {
530 for attr in attributes {
531 if attr.get_name() == &name_str
532 && let AttributeValue::Event(handler) = attr.get_value()
533 {
534 return Some(handler.clone());
535 }
536 }
537 }
538 None
539 }
540
541 pub fn try_get_callback(&self, name: &str) -> Option<crate::event::NativeEventHandler> {
543 if let VirtualNode::Element { attributes, .. } = self {
544 for attr in attributes {
545 if attr.get_name() == name
546 && let AttributeValue::Event(handler) = attr.get_value()
547 {
548 return Some(handler.clone());
549 }
550 }
551 }
552 None
553 }
554}
555
556impl<T> AsReactiveText for Signal<T>
558where
559 T: Clone + PartialEq + std::fmt::Display + 'static,
560{
561 fn as_reactive_text(&self) -> VirtualNode {
562 let signal: Signal<T> = *self;
563 let initial: String = signal.get().to_string();
564 let string_signal: Signal<String> = {
565 let boxed: Box<SignalInner<String>> = Box::new(SignalInner::new(initial.clone()));
566 Signal::from_inner(Box::leak(boxed) as *mut SignalInner<String>)
567 };
568 let source_signal: Signal<T> = *self;
569 let string_signal_clone: Signal<String> = string_signal;
570 source_signal.subscribe({
571 let source_signal: Signal<T> = source_signal;
572 move || {
573 let new_value: String = source_signal.get().to_string();
574 string_signal_clone.set(new_value);
575 }
576 });
577 VirtualNode::Text(TextNode::new(initial, Some(string_signal)))
578 }
579}
580
581impl Style {
583 pub fn property<N, V>(mut self, name: N, value: V) -> Self
588 where
589 N: AsRef<str>,
590 V: AsRef<str>,
591 {
592 self.get_mut_properties().push(StyleProperty::new(
593 name.as_ref().replace('_', "-"),
594 value.as_ref().to_string(),
595 ));
596 self
597 }
598
599 pub fn to_css_string(&self) -> String {
601 self.get_properties()
602 .iter()
603 .map(|p| format!("{}: {};", p.get_name(), p.get_value()))
604 .collect::<Vec<String>>()
605 .join(" ")
606 }
607}
608
609impl Default for Style {
611 fn default() -> Self {
612 Self::new(Vec::new())
613 }
614}
615
616impl CssClass {
618 pub fn new(name: String, style: String) -> Self {
622 let mut css_class: CssClass = CssClass::default();
623 css_class.set_name(name);
624 css_class.set_style(style);
625 css_class.inject_style();
626 css_class
627 }
628
629 pub fn inject_style(&self) {
636 #[cfg(target_arch = "wasm32")]
637 {
638 let style_id: &str = "euv-css-injected";
639 let document: web_sys::Document = web_sys::window()
640 .expect("no global window exists")
641 .document()
642 .expect("no document exists");
643 let style_element: web_sys::HtmlStyleElement = match document
644 .get_element_by_id(style_id)
645 {
646 Some(el) => el.dyn_into::<web_sys::HtmlStyleElement>().unwrap(),
647 None => {
648 let el: web_sys::HtmlStyleElement = document
649 .create_element("style")
650 .unwrap()
651 .dyn_into::<web_sys::HtmlStyleElement>()
652 .unwrap();
653 el.set_id(style_id);
654 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); } }";
655 let global: &str = "html, body, #app { height: 100%; margin: 0; padding: 0; overflow: hidden; } * { -webkit-tap-highlight-color: transparent; }";
656 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 16px 0 0; position: fixed; bottom: 0; left: 16px; height: 80vh; animation: euv-slide-up 0.25s ease; } .c_modal_overlay { align-items: flex-end; } .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; } }";
657 el.set_inner_text(&format!("{} {} {}", global, keyframes, media_queries));
658 document.head().unwrap().append_child(&el).unwrap();
659 el
660 }
661 };
662 let existing_css: String = style_element.inner_text();
663 let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
664 if !existing_css.contains(&class_rule) {
665 let new_css: String = if existing_css.is_empty() {
666 class_rule
667 } else {
668 format!("{}\n{}", existing_css, class_rule)
669 };
670 style_element.set_inner_text(&new_css);
671 }
672 }
673 }
674}
675
676impl std::fmt::Display for CssClass {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 write!(f, "{}", self.get_name())
683 }
684}