1use super::NodeId;
4use crate::style::Style;
5use std::collections::HashSet;
6
7#[derive(Debug, Clone, Default)]
9pub struct WidgetMeta {
10 pub widget_type: String,
12 pub id: Option<String>,
14 pub classes: HashSet<String>,
16}
17
18impl WidgetMeta {
19 pub fn new(widget_type: impl Into<String>) -> Self {
21 Self {
22 widget_type: widget_type.into(),
23 id: None,
24 classes: HashSet::new(),
25 }
26 }
27
28 pub fn id(mut self, id: impl Into<String>) -> Self {
30 self.id = Some(id.into());
31 self
32 }
33
34 pub fn class(mut self, class: impl Into<String>) -> Self {
36 self.classes.insert(class.into());
37 self
38 }
39
40 pub fn classes<I, S>(mut self, classes: I) -> Self
42 where
43 I: IntoIterator<Item = S>,
44 S: Into<String>,
45 {
46 for class in classes {
47 self.classes.insert(class.into());
48 }
49 self
50 }
51
52 pub fn has_class(&self, class: &str) -> bool {
54 self.classes.contains(class)
55 }
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct NodeState {
61 pub focused: bool,
63 pub hovered: bool,
65 pub disabled: bool,
67 pub selected: bool,
69 pub checked: bool,
71 pub active: bool,
73 pub empty: bool,
75 pub dirty: bool,
77 pub first_child: bool,
79 pub last_child: bool,
81 pub only_child: bool,
83 pub child_index: usize,
85 pub sibling_count: usize,
87}
88
89macro_rules! setter {
91 ($($name:ident: $field:ident),* $(,)?) => {
92 $(
93 #[doc = concat!("Set ", stringify!($field), " state")]
94 pub fn $name(mut self, $field: bool) -> Self {
95 self.$field = $field;
96 self
97 }
98 )*
99 };
100}
101
102impl NodeState {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 setter! {
110 focused: focused,
111 hovered: hovered,
112 disabled: disabled,
113 selected: selected,
114 checked: checked,
115 active: active,
116 dirty: dirty,
117 }
118
119 pub fn update_position(&mut self, index: usize, total: usize) {
121 self.child_index = index;
122 self.sibling_count = total;
123 self.first_child = index == 0;
124 self.last_child = index == total.saturating_sub(1);
125 self.only_child = total == 1;
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct DomId(pub NodeId);
132
133impl DomId {
134 pub fn new(id: NodeId) -> Self {
136 Self(id)
137 }
138
139 pub fn inner(&self) -> NodeId {
141 self.0
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct DomNode {
148 pub id: DomId,
150 pub meta: WidgetMeta,
152 pub state: NodeState,
154 pub parent: Option<DomId>,
156 pub children: Vec<DomId>,
158 pub computed_style: Style,
160 pub inline_style: Option<Style>,
162}
163
164impl DomNode {
165 pub fn new(id: DomId, meta: WidgetMeta) -> Self {
167 Self {
168 id,
169 meta,
170 state: NodeState::default(),
171 parent: None,
172 children: Vec::new(),
173 computed_style: Style::default(),
174 inline_style: None,
175 }
176 }
177
178 pub fn widget_type(&self) -> &str {
180 &self.meta.widget_type
181 }
182
183 pub fn element_id(&self) -> Option<&str> {
185 self.meta.id.as_deref()
186 }
187
188 pub fn has_class(&self, class: &str) -> bool {
190 self.meta.has_class(class)
191 }
192
193 pub fn classes(&self) -> impl Iterator<Item = &str> {
195 self.meta.classes.iter().map(|s| s.as_str())
196 }
197
198 pub fn matches_pseudo(&self, pseudo: &super::PseudoClass) -> bool {
200 use super::PseudoClass::*;
201 match pseudo {
202 Focus => self.state.focused,
203 Hover => self.state.hovered,
204 Active => self.state.active,
205 Disabled => self.state.disabled,
206 Enabled => !self.state.disabled,
207 Checked => self.state.checked,
208 Selected => self.state.selected,
209 Empty => self.state.empty,
210 FirstChild => self.state.first_child,
211 LastChild => self.state.last_child,
212 OnlyChild => self.state.only_child,
213 NthChild(expr) => expr.matches(self.state.child_index + 1),
214 NthLastChild(expr) => {
215 let from_end = self.state.sibling_count - self.state.child_index;
216 expr.matches(from_end)
217 }
218 Not(inner) => !self.matches_pseudo(inner),
219 }
220 }
221
222 pub fn set_inline_style(&mut self, style: Style) {
224 self.inline_style = Some(style);
225 }
226
227 pub fn add_child(&mut self, child_id: DomId) {
229 self.children.push(child_id);
230 }
231
232 pub fn remove_child(&mut self, child_id: DomId) {
234 self.children.retain(|&id| id != child_id);
235 }
236
237 pub fn has_children(&self) -> bool {
239 !self.children.is_empty()
240 }
241
242 pub fn child_count(&self) -> usize {
244 self.children.len()
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_widget_meta() {
254 let meta = WidgetMeta::new("Button")
255 .id("submit")
256 .class("primary")
257 .class("large");
258
259 assert_eq!(meta.widget_type, "Button");
260 assert_eq!(meta.id, Some("submit".to_string()));
261 assert!(meta.has_class("primary"));
262 assert!(meta.has_class("large"));
263 assert!(!meta.has_class("small"));
264 }
265
266 #[test]
267 fn test_node_state() {
268 let mut state = NodeState::new().focused(true).disabled(false);
269 state.update_position(0, 3);
270
271 assert!(state.focused);
272 assert!(!state.disabled);
273 assert!(state.first_child);
274 assert!(!state.last_child);
275 assert!(!state.only_child);
276 }
277
278 #[test]
279 fn test_dom_node() {
280 let meta = WidgetMeta::new("Button").class("primary");
281 let node = DomNode::new(DomId::new(1), meta);
282
283 assert_eq!(node.widget_type(), "Button");
284 assert!(node.has_class("primary"));
285 }
286
287 #[test]
292 fn test_widget_meta_default() {
293 let meta = WidgetMeta::default();
294 assert!(meta.widget_type.is_empty());
295 assert!(meta.id.is_none());
296 assert!(meta.classes.is_empty());
297 }
298
299 #[test]
300 fn test_widget_meta_new() {
301 let meta = WidgetMeta::new("Input");
302 assert_eq!(meta.widget_type, "Input");
303 assert!(meta.id.is_none());
304 assert!(meta.classes.is_empty());
305 }
306
307 #[test]
308 fn test_widget_meta_id() {
309 let meta = WidgetMeta::new("Button").id("submit-btn");
310 assert_eq!(meta.id, Some("submit-btn".to_string()));
311 }
312
313 #[test]
314 fn test_widget_meta_class() {
315 let meta = WidgetMeta::new("Button").class("primary");
316 assert!(meta.has_class("primary"));
317 assert!(!meta.has_class("secondary"));
318 }
319
320 #[test]
321 fn test_widget_meta_classes_iterator() {
322 let meta = WidgetMeta::new("Button").classes(vec!["primary", "large", "rounded"]);
323
324 assert!(meta.has_class("primary"));
325 assert!(meta.has_class("large"));
326 assert!(meta.has_class("rounded"));
327 assert_eq!(meta.classes.len(), 3);
328 }
329
330 #[test]
331 fn test_widget_meta_duplicate_classes() {
332 let meta = WidgetMeta::new("Button").class("primary").class("primary"); assert_eq!(meta.classes.len(), 1);
336 }
337
338 #[test]
339 fn test_widget_meta_clone() {
340 let meta = WidgetMeta::new("Button").id("btn").class("primary");
341 let cloned = meta.clone();
342
343 assert_eq!(cloned.widget_type, "Button");
344 assert_eq!(cloned.id, Some("btn".to_string()));
345 assert!(cloned.has_class("primary"));
346 }
347
348 #[test]
353 fn test_node_state_default() {
354 let state = NodeState::default();
355 assert!(!state.focused);
356 assert!(!state.hovered);
357 assert!(!state.disabled);
358 assert!(!state.selected);
359 assert!(!state.checked);
360 assert!(!state.active);
361 assert!(!state.empty);
362 assert!(!state.dirty);
363 assert!(!state.first_child);
364 assert!(!state.last_child);
365 assert!(!state.only_child);
366 assert_eq!(state.child_index, 0);
367 assert_eq!(state.sibling_count, 0);
368 }
369
370 #[test]
371 fn test_node_state_focused() {
372 let state = NodeState::new().focused(true);
373 assert!(state.focused);
374
375 let state = state.focused(false);
376 assert!(!state.focused);
377 }
378
379 #[test]
380 fn test_node_state_hovered() {
381 let state = NodeState::new().hovered(true);
382 assert!(state.hovered);
383 }
384
385 #[test]
386 fn test_node_state_disabled() {
387 let state = NodeState::new().disabled(true);
388 assert!(state.disabled);
389 }
390
391 #[test]
392 fn test_node_state_selected() {
393 let state = NodeState::new().selected(true);
394 assert!(state.selected);
395 }
396
397 #[test]
398 fn test_node_state_checked() {
399 let state = NodeState::new().checked(true);
400 assert!(state.checked);
401 }
402
403 #[test]
404 fn test_node_state_dirty() {
405 let state = NodeState::new().dirty(true);
406 assert!(state.dirty);
407 }
408
409 #[test]
410 fn test_node_state_update_position_first() {
411 let mut state = NodeState::new();
412 state.update_position(0, 5);
413
414 assert_eq!(state.child_index, 0);
415 assert_eq!(state.sibling_count, 5);
416 assert!(state.first_child);
417 assert!(!state.last_child);
418 assert!(!state.only_child);
419 }
420
421 #[test]
422 fn test_node_state_update_position_last() {
423 let mut state = NodeState::new();
424 state.update_position(4, 5);
425
426 assert_eq!(state.child_index, 4);
427 assert!(state.last_child);
428 assert!(!state.first_child);
429 assert!(!state.only_child);
430 }
431
432 #[test]
433 fn test_node_state_update_position_only_child() {
434 let mut state = NodeState::new();
435 state.update_position(0, 1);
436
437 assert!(state.first_child);
438 assert!(state.last_child);
439 assert!(state.only_child);
440 }
441
442 #[test]
443 fn test_node_state_update_position_middle() {
444 let mut state = NodeState::new();
445 state.update_position(2, 5);
446
447 assert!(!state.first_child);
448 assert!(!state.last_child);
449 assert!(!state.only_child);
450 }
451
452 #[test]
453 fn test_node_state_clone() {
454 let state = NodeState::new().focused(true).disabled(true);
455 let cloned = state.clone();
456
457 assert!(cloned.focused);
458 assert!(cloned.disabled);
459 }
460
461 #[test]
466 fn test_dom_id_new() {
467 let id = DomId::new(42);
468 assert_eq!(id.inner(), 42);
469 }
470
471 #[test]
472 fn test_dom_id_inner() {
473 let id = DomId(100);
474 assert_eq!(id.inner(), 100);
475 }
476
477 #[test]
478 fn test_dom_id_equality() {
479 let id1 = DomId::new(1);
480 let id2 = DomId::new(1);
481 let id3 = DomId::new(2);
482
483 assert_eq!(id1, id2);
484 assert_ne!(id1, id3);
485 }
486
487 #[test]
488 fn test_dom_id_hash() {
489 use std::collections::HashSet;
490 let mut set = HashSet::new();
491 set.insert(DomId::new(1));
492 set.insert(DomId::new(2));
493 set.insert(DomId::new(1)); assert_eq!(set.len(), 2);
496 }
497
498 #[test]
499 fn test_dom_id_copy() {
500 let id1 = DomId::new(42);
501 let id2 = id1; assert_eq!(id1, id2);
503 }
504
505 #[test]
510 fn test_dom_node_new() {
511 let meta = WidgetMeta::new("Text");
512 let node = DomNode::new(DomId::new(1), meta);
513
514 assert_eq!(node.id.inner(), 1);
515 assert_eq!(node.widget_type(), "Text");
516 assert!(node.parent.is_none());
517 assert!(node.children.is_empty());
518 }
519
520 #[test]
521 fn test_dom_node_element_id() {
522 let meta = WidgetMeta::new("Button").id("submit");
523 let node = DomNode::new(DomId::new(1), meta);
524
525 assert_eq!(node.element_id(), Some("submit"));
526 }
527
528 #[test]
529 fn test_dom_node_element_id_none() {
530 let meta = WidgetMeta::new("Button");
531 let node = DomNode::new(DomId::new(1), meta);
532
533 assert_eq!(node.element_id(), None);
534 }
535
536 #[test]
537 fn test_dom_node_has_class() {
538 let meta = WidgetMeta::new("Button").class("primary").class("large");
539 let node = DomNode::new(DomId::new(1), meta);
540
541 assert!(node.has_class("primary"));
542 assert!(node.has_class("large"));
543 assert!(!node.has_class("small"));
544 }
545
546 #[test]
547 fn test_dom_node_classes_iterator() {
548 let meta = WidgetMeta::new("Button").class("primary").class("large");
549 let node = DomNode::new(DomId::new(1), meta);
550
551 let classes: Vec<&str> = node.classes().collect();
552 assert_eq!(classes.len(), 2);
553 assert!(classes.contains(&"primary"));
554 assert!(classes.contains(&"large"));
555 }
556
557 #[test]
558 fn test_dom_node_set_inline_style() {
559 let meta = WidgetMeta::new("Button");
560 let mut node = DomNode::new(DomId::new(1), meta);
561
562 assert!(node.inline_style.is_none());
563 node.set_inline_style(Style::default());
564 assert!(node.inline_style.is_some());
565 }
566
567 #[test]
568 fn test_dom_node_add_child() {
569 let meta = WidgetMeta::new("Container");
570 let mut node = DomNode::new(DomId::new(1), meta);
571
572 assert!(!node.has_children());
573 assert_eq!(node.child_count(), 0);
574
575 node.add_child(DomId::new(2));
576 node.add_child(DomId::new(3));
577
578 assert!(node.has_children());
579 assert_eq!(node.child_count(), 2);
580 assert_eq!(node.children, vec![DomId::new(2), DomId::new(3)]);
581 }
582
583 #[test]
584 fn test_dom_node_remove_child() {
585 let meta = WidgetMeta::new("Container");
586 let mut node = DomNode::new(DomId::new(1), meta);
587
588 node.add_child(DomId::new(2));
589 node.add_child(DomId::new(3));
590 node.add_child(DomId::new(4));
591
592 node.remove_child(DomId::new(3));
593
594 assert_eq!(node.child_count(), 2);
595 assert_eq!(node.children, vec![DomId::new(2), DomId::new(4)]);
596 }
597
598 #[test]
599 fn test_dom_node_remove_nonexistent_child() {
600 let meta = WidgetMeta::new("Container");
601 let mut node = DomNode::new(DomId::new(1), meta);
602
603 node.add_child(DomId::new(2));
604 node.remove_child(DomId::new(99)); assert_eq!(node.child_count(), 1);
607 }
608
609 #[test]
610 fn test_dom_node_matches_pseudo_focus() {
611 use crate::dom::PseudoClass;
612
613 let meta = WidgetMeta::new("Input");
614 let mut node = DomNode::new(DomId::new(1), meta);
615 node.state = NodeState::new().focused(true);
616
617 assert!(node.matches_pseudo(&PseudoClass::Focus));
618 }
619
620 #[test]
621 fn test_dom_node_matches_pseudo_hover() {
622 use crate::dom::PseudoClass;
623
624 let meta = WidgetMeta::new("Button");
625 let mut node = DomNode::new(DomId::new(1), meta);
626 node.state = NodeState::new().hovered(true);
627
628 assert!(node.matches_pseudo(&PseudoClass::Hover));
629 }
630
631 #[test]
632 fn test_dom_node_matches_pseudo_disabled() {
633 use crate::dom::PseudoClass;
634
635 let meta = WidgetMeta::new("Button");
636 let mut node = DomNode::new(DomId::new(1), meta);
637 node.state = NodeState::new().disabled(true);
638
639 assert!(node.matches_pseudo(&PseudoClass::Disabled));
640 assert!(!node.matches_pseudo(&PseudoClass::Enabled));
641 }
642
643 #[test]
644 fn test_dom_node_matches_pseudo_enabled() {
645 use crate::dom::PseudoClass;
646
647 let meta = WidgetMeta::new("Button");
648 let node = DomNode::new(DomId::new(1), meta);
649
650 assert!(node.matches_pseudo(&PseudoClass::Enabled));
651 assert!(!node.matches_pseudo(&PseudoClass::Disabled));
652 }
653
654 #[test]
655 fn test_dom_node_matches_pseudo_checked() {
656 use crate::dom::PseudoClass;
657
658 let meta = WidgetMeta::new("Checkbox");
659 let mut node = DomNode::new(DomId::new(1), meta);
660 node.state = NodeState::new().checked(true);
661
662 assert!(node.matches_pseudo(&PseudoClass::Checked));
663 }
664
665 #[test]
666 fn test_dom_node_matches_pseudo_selected() {
667 use crate::dom::PseudoClass;
668
669 let meta = WidgetMeta::new("ListItem");
670 let mut node = DomNode::new(DomId::new(1), meta);
671 node.state = NodeState::new().selected(true);
672
673 assert!(node.matches_pseudo(&PseudoClass::Selected));
674 }
675
676 #[test]
677 fn test_dom_node_matches_pseudo_first_child() {
678 use crate::dom::PseudoClass;
679
680 let meta = WidgetMeta::new("ListItem");
681 let mut node = DomNode::new(DomId::new(1), meta);
682 node.state.update_position(0, 5);
683
684 assert!(node.matches_pseudo(&PseudoClass::FirstChild));
685 assert!(!node.matches_pseudo(&PseudoClass::LastChild));
686 }
687
688 #[test]
689 fn test_dom_node_matches_pseudo_last_child() {
690 use crate::dom::PseudoClass;
691
692 let meta = WidgetMeta::new("ListItem");
693 let mut node = DomNode::new(DomId::new(1), meta);
694 node.state.update_position(4, 5);
695
696 assert!(node.matches_pseudo(&PseudoClass::LastChild));
697 assert!(!node.matches_pseudo(&PseudoClass::FirstChild));
698 }
699
700 #[test]
701 fn test_dom_node_matches_pseudo_only_child() {
702 use crate::dom::PseudoClass;
703
704 let meta = WidgetMeta::new("ListItem");
705 let mut node = DomNode::new(DomId::new(1), meta);
706 node.state.update_position(0, 1);
707
708 assert!(node.matches_pseudo(&PseudoClass::OnlyChild));
709 assert!(node.matches_pseudo(&PseudoClass::FirstChild));
710 assert!(node.matches_pseudo(&PseudoClass::LastChild));
711 }
712
713 #[test]
714 fn test_dom_node_matches_pseudo_nth_child() {
715 use crate::dom::{NthExpr, PseudoClass};
716
717 let meta = WidgetMeta::new("ListItem");
718 let mut node = DomNode::new(DomId::new(1), meta);
719 node.state.update_position(2, 5); assert!(node.matches_pseudo(&PseudoClass::NthChild(NthExpr::new(0, 3)))); assert!(!node.matches_pseudo(&PseudoClass::NthChild(NthExpr::new(0, 2))));
723 }
724
725 #[test]
726 fn test_dom_node_matches_pseudo_nth_last_child() {
727 use crate::dom::{NthExpr, PseudoClass};
728
729 let meta = WidgetMeta::new("ListItem");
730 let mut node = DomNode::new(DomId::new(1), meta);
731 node.state.update_position(3, 5); assert!(node.matches_pseudo(&PseudoClass::NthLastChild(NthExpr::new(0, 2))));
734 }
735
736 #[test]
737 fn test_dom_node_matches_pseudo_not() {
738 use crate::dom::PseudoClass;
739
740 let meta = WidgetMeta::new("Button");
741 let node = DomNode::new(DomId::new(1), meta);
742
743 assert!(node.matches_pseudo(&PseudoClass::Not(Box::new(PseudoClass::Disabled))));
745 }
746
747 #[test]
748 fn test_dom_node_matches_pseudo_empty() {
749 use crate::dom::PseudoClass;
750
751 let meta = WidgetMeta::new("Container");
752 let mut node = DomNode::new(DomId::new(1), meta);
753 node.state.empty = true;
754
755 assert!(node.matches_pseudo(&PseudoClass::Empty));
756 }
757
758 #[test]
759 fn test_dom_node_matches_pseudo_active() {
760 use crate::dom::PseudoClass;
761
762 let meta = WidgetMeta::new("Button");
763 let mut node = DomNode::new(DomId::new(1), meta);
764 node.state.active = true;
765
766 assert!(node.matches_pseudo(&PseudoClass::Active));
767 }
768
769 #[test]
770 fn test_dom_node_clone() {
771 let meta = WidgetMeta::new("Button").id("btn").class("primary");
772 let mut node = DomNode::new(DomId::new(1), meta);
773 node.add_child(DomId::new(2));
774
775 let cloned = node.clone();
776
777 assert_eq!(cloned.id, node.id);
778 assert_eq!(cloned.widget_type(), "Button");
779 assert_eq!(cloned.element_id(), Some("btn"));
780 assert!(cloned.has_class("primary"));
781 assert_eq!(cloned.child_count(), 1);
782 }
783}