Skip to main content

revue/runtime/dom/node/
mod.rs

1//! DOM node representation
2
3use super::NodeId;
4use crate::style::Style;
5use std::collections::HashSet;
6
7/// Widget metadata for CSS matching
8#[derive(Debug, Clone, Default)]
9pub struct WidgetMeta {
10    /// Widget type name (e.g., "Button", "Input", "Text")
11    pub widget_type: String,
12    /// Element ID (unique identifier, e.g., "submit-btn")
13    pub id: Option<String>,
14    /// CSS classes (e.g., ["primary", "large"])
15    pub classes: HashSet<String>,
16}
17
18impl WidgetMeta {
19    /// Create new widget metadata
20    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    /// Set element ID
29    pub fn id(mut self, id: impl Into<String>) -> Self {
30        self.id = Some(id.into());
31        self
32    }
33
34    /// Add a CSS class
35    pub fn class(mut self, class: impl Into<String>) -> Self {
36        self.classes.insert(class.into());
37        self
38    }
39
40    /// Add multiple CSS classes
41    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    /// Check if has a class
53    pub fn has_class(&self, class: &str) -> bool {
54        self.classes.contains(class)
55    }
56}
57
58/// Node state for pseudo-class matching
59#[derive(Debug, Clone, Default)]
60pub struct NodeState {
61    /// Node has keyboard focus
62    pub focused: bool,
63    /// Mouse is hovering (if mouse support enabled)
64    pub hovered: bool,
65    /// Node is disabled
66    pub disabled: bool,
67    /// Node is selected
68    pub selected: bool,
69    /// Node is checked (for checkboxes, radio buttons)
70    pub checked: bool,
71    /// Node is active (being pressed)
72    pub active: bool,
73    /// Node is empty (no content)
74    pub empty: bool,
75    /// Node's content, style or layout is dirty and needs repaint
76    pub dirty: bool,
77    /// Node is first child of parent
78    pub first_child: bool,
79    /// Node is last child of parent
80    pub last_child: bool,
81    /// Node is only child of parent
82    pub only_child: bool,
83    /// Node index among siblings (0-based)
84    pub child_index: usize,
85    /// Total sibling count
86    pub sibling_count: usize,
87}
88
89// Macro to generate boolean state setters (reduces code duplication)
90macro_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    /// Create a new default state
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    // Boolean state setters generated by macro to reduce code duplication
109    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    /// Update positional states based on sibling info
120    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/// DOM node identifier (for parent/child references)
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct DomId(pub NodeId);
132
133impl DomId {
134    /// Create a new DOM ID
135    pub fn new(id: NodeId) -> Self {
136        Self(id)
137    }
138
139    /// Get the inner ID value
140    pub fn inner(&self) -> NodeId {
141        self.0
142    }
143}
144
145/// A node in the DOM tree
146#[derive(Debug, Clone)]
147pub struct DomNode {
148    /// Unique node identifier
149    pub id: DomId,
150    /// Widget metadata
151    pub meta: WidgetMeta,
152    /// Current state
153    pub state: NodeState,
154    /// Parent node ID (None for root)
155    pub parent: Option<DomId>,
156    /// Child node IDs
157    pub children: Vec<DomId>,
158    /// Computed style (after cascade)
159    pub computed_style: Style,
160    /// Inline style (highest priority)
161    pub inline_style: Option<Style>,
162}
163
164impl DomNode {
165    /// Create a new DOM node
166    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    /// Get widget type
179    pub fn widget_type(&self) -> &str {
180        &self.meta.widget_type
181    }
182
183    /// Get element ID
184    pub fn element_id(&self) -> Option<&str> {
185        self.meta.id.as_deref()
186    }
187
188    /// Check if node has a class
189    pub fn has_class(&self, class: &str) -> bool {
190        self.meta.has_class(class)
191    }
192
193    /// Get all classes
194    pub fn classes(&self) -> impl Iterator<Item = &str> {
195        self.meta.classes.iter().map(|s| s.as_str())
196    }
197
198    /// Check if this node matches a pseudo-class
199    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    /// Set inline style
223    pub fn set_inline_style(&mut self, style: Style) {
224        self.inline_style = Some(style);
225    }
226
227    /// Add a child
228    pub fn add_child(&mut self, child_id: DomId) {
229        self.children.push(child_id);
230    }
231
232    /// Remove a child
233    pub fn remove_child(&mut self, child_id: DomId) {
234        self.children.retain(|&id| id != child_id);
235    }
236
237    /// Check if has children
238    pub fn has_children(&self) -> bool {
239        !self.children.is_empty()
240    }
241
242    /// Get child count
243    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    // =========================================================================
288    // WidgetMeta tests
289    // =========================================================================
290
291    #[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"); // Duplicate
333
334        // HashSet deduplicates
335        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    // =========================================================================
349    // NodeState tests
350    // =========================================================================
351
352    #[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    // =========================================================================
462    // DomId tests
463    // =========================================================================
464
465    #[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)); // Duplicate
494
495        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; // Copy
502        assert_eq!(id1, id2);
503    }
504
505    // =========================================================================
506    // DomNode tests
507    // =========================================================================
508
509    #[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)); // Non-existent
605
606        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); // 3rd child (0-indexed: 2)
720
721        assert!(node.matches_pseudo(&PseudoClass::NthChild(NthExpr::new(0, 3)))); // 1-indexed
722        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); // 4th child, 2nd from last
732
733        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        // Not disabled (node is enabled by default)
744        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}