use super::NodeId;
use crate::style::Style;
use std::collections::HashSet;
#[derive(Debug, Clone, Default)]
pub struct WidgetMeta {
pub widget_type: String,
pub id: Option<String>,
pub classes: HashSet<String>,
}
impl WidgetMeta {
pub fn new(widget_type: impl Into<String>) -> Self {
Self {
widget_type: widget_type.into(),
id: None,
classes: HashSet::new(),
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn class(mut self, class: impl Into<String>) -> Self {
self.classes.insert(class.into());
self
}
pub fn classes<I, S>(mut self, classes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for class in classes {
self.classes.insert(class.into());
}
self
}
pub fn has_class(&self, class: &str) -> bool {
self.classes.contains(class)
}
}
#[derive(Debug, Clone, Default)]
pub struct NodeState {
pub focused: bool,
pub hovered: bool,
pub disabled: bool,
pub selected: bool,
pub checked: bool,
pub active: bool,
pub empty: bool,
pub dirty: bool,
pub first_child: bool,
pub last_child: bool,
pub only_child: bool,
pub child_index: usize,
pub sibling_count: usize,
}
macro_rules! setter {
($($name:ident: $field:ident),* $(,)?) => {
$(
#[doc = concat!("Set ", stringify!($field), " state")]
pub fn $name(mut self, $field: bool) -> Self {
self.$field = $field;
self
}
)*
};
}
impl NodeState {
pub fn new() -> Self {
Self::default()
}
setter! {
focused: focused,
hovered: hovered,
disabled: disabled,
selected: selected,
checked: checked,
active: active,
dirty: dirty,
}
pub fn update_position(&mut self, index: usize, total: usize) {
self.child_index = index;
self.sibling_count = total;
self.first_child = index == 0;
self.last_child = index == total.saturating_sub(1);
self.only_child = total == 1;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DomId(pub NodeId);
impl DomId {
pub fn new(id: NodeId) -> Self {
Self(id)
}
pub fn inner(&self) -> NodeId {
self.0
}
}
#[derive(Debug, Clone)]
pub struct DomNode {
pub id: DomId,
pub meta: WidgetMeta,
pub state: NodeState,
pub parent: Option<DomId>,
pub children: Vec<DomId>,
pub computed_style: Style,
pub inline_style: Option<Style>,
}
impl DomNode {
pub fn new(id: DomId, meta: WidgetMeta) -> Self {
Self {
id,
meta,
state: NodeState::default(),
parent: None,
children: Vec::new(),
computed_style: Style::default(),
inline_style: None,
}
}
pub fn widget_type(&self) -> &str {
&self.meta.widget_type
}
pub fn element_id(&self) -> Option<&str> {
self.meta.id.as_deref()
}
pub fn has_class(&self, class: &str) -> bool {
self.meta.has_class(class)
}
pub fn classes(&self) -> impl Iterator<Item = &str> {
self.meta.classes.iter().map(|s| s.as_str())
}
pub fn matches_pseudo(&self, pseudo: &super::PseudoClass) -> bool {
use super::PseudoClass::*;
match pseudo {
Focus => self.state.focused,
Hover => self.state.hovered,
Active => self.state.active,
Disabled => self.state.disabled,
Enabled => !self.state.disabled,
Checked => self.state.checked,
Selected => self.state.selected,
Empty => self.state.empty,
FirstChild => self.state.first_child,
LastChild => self.state.last_child,
OnlyChild => self.state.only_child,
NthChild(expr) => expr.matches(self.state.child_index + 1),
NthLastChild(expr) => {
let from_end = self.state.sibling_count - self.state.child_index;
expr.matches(from_end)
}
Not(inner) => !self.matches_pseudo(inner),
}
}
pub fn set_inline_style(&mut self, style: Style) {
self.inline_style = Some(style);
}
pub fn add_child(&mut self, child_id: DomId) {
self.children.push(child_id);
}
pub fn remove_child(&mut self, child_id: DomId) {
self.children.retain(|&id| id != child_id);
}
pub fn has_children(&self) -> bool {
!self.children.is_empty()
}
pub fn child_count(&self) -> usize {
self.children.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_widget_meta() {
let meta = WidgetMeta::new("Button")
.id("submit")
.class("primary")
.class("large");
assert_eq!(meta.widget_type, "Button");
assert_eq!(meta.id, Some("submit".to_string()));
assert!(meta.has_class("primary"));
assert!(meta.has_class("large"));
assert!(!meta.has_class("small"));
}
#[test]
fn test_node_state() {
let mut state = NodeState::new().focused(true).disabled(false);
state.update_position(0, 3);
assert!(state.focused);
assert!(!state.disabled);
assert!(state.first_child);
assert!(!state.last_child);
assert!(!state.only_child);
}
#[test]
fn test_dom_node() {
let meta = WidgetMeta::new("Button").class("primary");
let node = DomNode::new(DomId::new(1), meta);
assert_eq!(node.widget_type(), "Button");
assert!(node.has_class("primary"));
}
#[test]
fn test_widget_meta_default() {
let meta = WidgetMeta::default();
assert!(meta.widget_type.is_empty());
assert!(meta.id.is_none());
assert!(meta.classes.is_empty());
}
#[test]
fn test_widget_meta_new() {
let meta = WidgetMeta::new("Input");
assert_eq!(meta.widget_type, "Input");
assert!(meta.id.is_none());
assert!(meta.classes.is_empty());
}
#[test]
fn test_widget_meta_id() {
let meta = WidgetMeta::new("Button").id("submit-btn");
assert_eq!(meta.id, Some("submit-btn".to_string()));
}
#[test]
fn test_widget_meta_class() {
let meta = WidgetMeta::new("Button").class("primary");
assert!(meta.has_class("primary"));
assert!(!meta.has_class("secondary"));
}
#[test]
fn test_widget_meta_classes_iterator() {
let meta = WidgetMeta::new("Button").classes(vec!["primary", "large", "rounded"]);
assert!(meta.has_class("primary"));
assert!(meta.has_class("large"));
assert!(meta.has_class("rounded"));
assert_eq!(meta.classes.len(), 3);
}
#[test]
fn test_widget_meta_duplicate_classes() {
let meta = WidgetMeta::new("Button").class("primary").class("primary");
assert_eq!(meta.classes.len(), 1);
}
#[test]
fn test_widget_meta_clone() {
let meta = WidgetMeta::new("Button").id("btn").class("primary");
let cloned = meta.clone();
assert_eq!(cloned.widget_type, "Button");
assert_eq!(cloned.id, Some("btn".to_string()));
assert!(cloned.has_class("primary"));
}
#[test]
fn test_node_state_default() {
let state = NodeState::default();
assert!(!state.focused);
assert!(!state.hovered);
assert!(!state.disabled);
assert!(!state.selected);
assert!(!state.checked);
assert!(!state.active);
assert!(!state.empty);
assert!(!state.dirty);
assert!(!state.first_child);
assert!(!state.last_child);
assert!(!state.only_child);
assert_eq!(state.child_index, 0);
assert_eq!(state.sibling_count, 0);
}
#[test]
fn test_node_state_focused() {
let state = NodeState::new().focused(true);
assert!(state.focused);
let state = state.focused(false);
assert!(!state.focused);
}
#[test]
fn test_node_state_hovered() {
let state = NodeState::new().hovered(true);
assert!(state.hovered);
}
#[test]
fn test_node_state_disabled() {
let state = NodeState::new().disabled(true);
assert!(state.disabled);
}
#[test]
fn test_node_state_selected() {
let state = NodeState::new().selected(true);
assert!(state.selected);
}
#[test]
fn test_node_state_checked() {
let state = NodeState::new().checked(true);
assert!(state.checked);
}
#[test]
fn test_node_state_dirty() {
let state = NodeState::new().dirty(true);
assert!(state.dirty);
}
#[test]
fn test_node_state_update_position_first() {
let mut state = NodeState::new();
state.update_position(0, 5);
assert_eq!(state.child_index, 0);
assert_eq!(state.sibling_count, 5);
assert!(state.first_child);
assert!(!state.last_child);
assert!(!state.only_child);
}
#[test]
fn test_node_state_update_position_last() {
let mut state = NodeState::new();
state.update_position(4, 5);
assert_eq!(state.child_index, 4);
assert!(state.last_child);
assert!(!state.first_child);
assert!(!state.only_child);
}
#[test]
fn test_node_state_update_position_only_child() {
let mut state = NodeState::new();
state.update_position(0, 1);
assert!(state.first_child);
assert!(state.last_child);
assert!(state.only_child);
}
#[test]
fn test_node_state_update_position_middle() {
let mut state = NodeState::new();
state.update_position(2, 5);
assert!(!state.first_child);
assert!(!state.last_child);
assert!(!state.only_child);
}
#[test]
fn test_node_state_clone() {
let state = NodeState::new().focused(true).disabled(true);
let cloned = state.clone();
assert!(cloned.focused);
assert!(cloned.disabled);
}
#[test]
fn test_dom_id_new() {
let id = DomId::new(42);
assert_eq!(id.inner(), 42);
}
#[test]
fn test_dom_id_inner() {
let id = DomId(100);
assert_eq!(id.inner(), 100);
}
#[test]
fn test_dom_id_equality() {
let id1 = DomId::new(1);
let id2 = DomId::new(1);
let id3 = DomId::new(2);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_dom_id_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(DomId::new(1));
set.insert(DomId::new(2));
set.insert(DomId::new(1));
assert_eq!(set.len(), 2);
}
#[test]
fn test_dom_id_copy() {
let id1 = DomId::new(42);
let id2 = id1; assert_eq!(id1, id2);
}
#[test]
fn test_dom_node_new() {
let meta = WidgetMeta::new("Text");
let node = DomNode::new(DomId::new(1), meta);
assert_eq!(node.id.inner(), 1);
assert_eq!(node.widget_type(), "Text");
assert!(node.parent.is_none());
assert!(node.children.is_empty());
}
#[test]
fn test_dom_node_element_id() {
let meta = WidgetMeta::new("Button").id("submit");
let node = DomNode::new(DomId::new(1), meta);
assert_eq!(node.element_id(), Some("submit"));
}
#[test]
fn test_dom_node_element_id_none() {
let meta = WidgetMeta::new("Button");
let node = DomNode::new(DomId::new(1), meta);
assert_eq!(node.element_id(), None);
}
#[test]
fn test_dom_node_has_class() {
let meta = WidgetMeta::new("Button").class("primary").class("large");
let node = DomNode::new(DomId::new(1), meta);
assert!(node.has_class("primary"));
assert!(node.has_class("large"));
assert!(!node.has_class("small"));
}
#[test]
fn test_dom_node_classes_iterator() {
let meta = WidgetMeta::new("Button").class("primary").class("large");
let node = DomNode::new(DomId::new(1), meta);
let classes: Vec<&str> = node.classes().collect();
assert_eq!(classes.len(), 2);
assert!(classes.contains(&"primary"));
assert!(classes.contains(&"large"));
}
#[test]
fn test_dom_node_set_inline_style() {
let meta = WidgetMeta::new("Button");
let mut node = DomNode::new(DomId::new(1), meta);
assert!(node.inline_style.is_none());
node.set_inline_style(Style::default());
assert!(node.inline_style.is_some());
}
#[test]
fn test_dom_node_add_child() {
let meta = WidgetMeta::new("Container");
let mut node = DomNode::new(DomId::new(1), meta);
assert!(!node.has_children());
assert_eq!(node.child_count(), 0);
node.add_child(DomId::new(2));
node.add_child(DomId::new(3));
assert!(node.has_children());
assert_eq!(node.child_count(), 2);
assert_eq!(node.children, vec![DomId::new(2), DomId::new(3)]);
}
#[test]
fn test_dom_node_remove_child() {
let meta = WidgetMeta::new("Container");
let mut node = DomNode::new(DomId::new(1), meta);
node.add_child(DomId::new(2));
node.add_child(DomId::new(3));
node.add_child(DomId::new(4));
node.remove_child(DomId::new(3));
assert_eq!(node.child_count(), 2);
assert_eq!(node.children, vec![DomId::new(2), DomId::new(4)]);
}
#[test]
fn test_dom_node_remove_nonexistent_child() {
let meta = WidgetMeta::new("Container");
let mut node = DomNode::new(DomId::new(1), meta);
node.add_child(DomId::new(2));
node.remove_child(DomId::new(99));
assert_eq!(node.child_count(), 1);
}
#[test]
fn test_dom_node_matches_pseudo_focus() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Input");
let mut node = DomNode::new(DomId::new(1), meta);
node.state = NodeState::new().focused(true);
assert!(node.matches_pseudo(&PseudoClass::Focus));
}
#[test]
fn test_dom_node_matches_pseudo_hover() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Button");
let mut node = DomNode::new(DomId::new(1), meta);
node.state = NodeState::new().hovered(true);
assert!(node.matches_pseudo(&PseudoClass::Hover));
}
#[test]
fn test_dom_node_matches_pseudo_disabled() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Button");
let mut node = DomNode::new(DomId::new(1), meta);
node.state = NodeState::new().disabled(true);
assert!(node.matches_pseudo(&PseudoClass::Disabled));
assert!(!node.matches_pseudo(&PseudoClass::Enabled));
}
#[test]
fn test_dom_node_matches_pseudo_enabled() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Button");
let node = DomNode::new(DomId::new(1), meta);
assert!(node.matches_pseudo(&PseudoClass::Enabled));
assert!(!node.matches_pseudo(&PseudoClass::Disabled));
}
#[test]
fn test_dom_node_matches_pseudo_checked() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Checkbox");
let mut node = DomNode::new(DomId::new(1), meta);
node.state = NodeState::new().checked(true);
assert!(node.matches_pseudo(&PseudoClass::Checked));
}
#[test]
fn test_dom_node_matches_pseudo_selected() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("ListItem");
let mut node = DomNode::new(DomId::new(1), meta);
node.state = NodeState::new().selected(true);
assert!(node.matches_pseudo(&PseudoClass::Selected));
}
#[test]
fn test_dom_node_matches_pseudo_first_child() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("ListItem");
let mut node = DomNode::new(DomId::new(1), meta);
node.state.update_position(0, 5);
assert!(node.matches_pseudo(&PseudoClass::FirstChild));
assert!(!node.matches_pseudo(&PseudoClass::LastChild));
}
#[test]
fn test_dom_node_matches_pseudo_last_child() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("ListItem");
let mut node = DomNode::new(DomId::new(1), meta);
node.state.update_position(4, 5);
assert!(node.matches_pseudo(&PseudoClass::LastChild));
assert!(!node.matches_pseudo(&PseudoClass::FirstChild));
}
#[test]
fn test_dom_node_matches_pseudo_only_child() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("ListItem");
let mut node = DomNode::new(DomId::new(1), meta);
node.state.update_position(0, 1);
assert!(node.matches_pseudo(&PseudoClass::OnlyChild));
assert!(node.matches_pseudo(&PseudoClass::FirstChild));
assert!(node.matches_pseudo(&PseudoClass::LastChild));
}
#[test]
fn test_dom_node_matches_pseudo_nth_child() {
use crate::dom::{NthExpr, PseudoClass};
let meta = WidgetMeta::new("ListItem");
let mut node = DomNode::new(DomId::new(1), meta);
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))));
}
#[test]
fn test_dom_node_matches_pseudo_nth_last_child() {
use crate::dom::{NthExpr, PseudoClass};
let meta = WidgetMeta::new("ListItem");
let mut node = DomNode::new(DomId::new(1), meta);
node.state.update_position(3, 5);
assert!(node.matches_pseudo(&PseudoClass::NthLastChild(NthExpr::new(0, 2))));
}
#[test]
fn test_dom_node_matches_pseudo_not() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Button");
let node = DomNode::new(DomId::new(1), meta);
assert!(node.matches_pseudo(&PseudoClass::Not(Box::new(PseudoClass::Disabled))));
}
#[test]
fn test_dom_node_matches_pseudo_empty() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Container");
let mut node = DomNode::new(DomId::new(1), meta);
node.state.empty = true;
assert!(node.matches_pseudo(&PseudoClass::Empty));
}
#[test]
fn test_dom_node_matches_pseudo_active() {
use crate::dom::PseudoClass;
let meta = WidgetMeta::new("Button");
let mut node = DomNode::new(DomId::new(1), meta);
node.state.active = true;
assert!(node.matches_pseudo(&PseudoClass::Active));
}
#[test]
fn test_dom_node_clone() {
let meta = WidgetMeta::new("Button").id("btn").class("primary");
let mut node = DomNode::new(DomId::new(1), meta);
node.add_child(DomId::new(2));
let cloned = node.clone();
assert_eq!(cloned.id, node.id);
assert_eq!(cloned.widget_type(), "Button");
assert_eq!(cloned.element_id(), Some("btn"));
assert!(cloned.has_class("primary"));
assert_eq!(cloned.child_count(), 1);
}
}