use super::selector::{parse_selector, Combinator, Selector, SelectorPart};
use super::{DomId, DomNode};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
pub struct QueryResult<'a> {
nodes: Vec<&'a DomNode>,
}
impl<'a> QueryResult<'a> {
pub fn empty() -> Self {
Self { nodes: Vec::new() }
}
pub fn from_nodes(nodes: Vec<&'a DomNode>) -> Self {
Self { nodes }
}
pub fn first(&self) -> Option<&'a DomNode> {
self.nodes.first().copied()
}
pub fn all(&self) -> &[&'a DomNode] {
&self.nodes
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &'a DomNode> + '_ {
self.nodes.iter().copied()
}
}
impl<'a> IntoIterator for QueryResult<'a> {
type Item = &'a DomNode;
type IntoIter = std::vec::IntoIter<&'a DomNode>;
fn into_iter(self) -> Self::IntoIter {
self.nodes.into_iter()
}
}
pub trait Query {
fn query_one(&self, selector: &str) -> Option<&DomNode>;
fn query_all(&self, selector: &str) -> QueryResult<'_>;
fn get_by_id(&self, id: &str) -> Option<&DomNode>;
fn get_by_class(&self, class: &str) -> QueryResult<'_>;
fn get_by_type(&self, widget_type: &str) -> QueryResult<'_>;
}
#[derive(Debug, Default)]
pub struct DomTree {
nodes: HashMap<DomId, DomNode>,
root: Option<DomId>,
id_map: HashMap<Arc<str>, DomId>,
type_index: HashMap<Arc<str>, Vec<DomId>>,
class_index: HashMap<Arc<str>, Vec<DomId>>,
selector_cache: RwLock<HashMap<String, Selector>>,
}
impl DomTree {
pub fn new() -> Self {
Self::default()
}
pub fn create_root(&mut self, meta: super::node::WidgetMeta) -> DomId {
let id = DomId::new(super::generate_node_id());
let mut node = DomNode::new(id, meta);
node.state.dirty = true;
if let Some(ref element_id) = node.meta.id {
self.id_map.insert(Arc::from(element_id.as_str()), id);
}
self.type_index
.entry(Arc::from(node.widget_type()))
.or_default()
.push(id);
for class in &node.meta.classes {
self.class_index
.entry(Arc::from(class.as_str()))
.or_default()
.push(id);
}
self.nodes.insert(id, node);
self.root = Some(id);
id
}
pub fn add_child(&mut self, parent_id: DomId, meta: super::node::WidgetMeta) -> DomId {
let id = DomId::new(super::generate_node_id());
let mut node = DomNode::new(id, meta);
node.parent = Some(parent_id);
node.state.dirty = true;
if let Some(ref element_id) = node.meta.id {
self.id_map.insert(Arc::from(element_id.as_str()), id);
}
self.type_index
.entry(Arc::from(node.widget_type()))
.or_default()
.push(id);
for class in &node.meta.classes {
self.class_index
.entry(Arc::from(class.as_str()))
.or_default()
.push(id);
}
let children_to_update: Vec<(DomId, usize, usize)> =
if let Some(parent) = self.nodes.get_mut(&parent_id) {
parent.children.push(id);
let child_count = parent.children.len();
parent
.children
.iter()
.enumerate()
.map(|(idx, &child_id)| (child_id, idx, child_count))
.collect()
} else {
Vec::new()
};
self.nodes.insert(id, node);
for (child_id, idx, child_count) in children_to_update {
if let Some(child) = self.nodes.get_mut(&child_id) {
child.state.update_position(idx, child_count);
}
}
id
}
pub fn remove(&mut self, id: DomId) {
let (parent_id, element_id, widget_type, classes, children) =
if let Some(node) = self.nodes.get(&id) {
(
node.parent,
node.meta.id.as_ref().map(|s| Arc::from(s.as_str())),
Arc::from(node.widget_type()),
node.meta
.classes
.iter()
.map(|s| Arc::from(s.as_str()))
.collect::<Vec<_>>(),
node.children.clone(),
)
} else {
return;
};
if let Some(parent_id) = parent_id {
if let Some(parent) = self.nodes.get_mut(&parent_id) {
parent.remove_child(id);
}
}
if let Some(element_id) = element_id {
self.id_map.remove(&element_id);
}
if let Some(type_ids) = self.type_index.get_mut(&widget_type) {
type_ids.retain(|&x| x != id);
if type_ids.is_empty() {
self.type_index.remove(&widget_type);
}
}
for class in &classes {
if let Some(class_ids) = self.class_index.get_mut(class) {
class_ids.retain(|&x| x != id);
if class_ids.is_empty() {
self.class_index.remove(class);
}
}
}
for child_id in children {
self.remove(child_id);
}
self.nodes.remove(&id);
}
pub fn get(&self, id: DomId) -> Option<&DomNode> {
self.nodes.get(&id)
}
pub fn get_mut(&mut self, id: DomId) -> Option<&mut DomNode> {
self.nodes.get_mut(&id)
}
pub fn root(&self) -> Option<&DomNode> {
self.root.and_then(|id| self.nodes.get(&id))
}
pub fn root_id(&self) -> Option<DomId> {
self.root
}
pub fn nodes(&self) -> impl Iterator<Item = &DomNode> {
self.nodes.values()
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn get_dirty_nodes(&self) -> Vec<DomId> {
self.nodes
.values()
.filter(|node| node.state.dirty)
.map(|node| node.id)
.collect()
}
pub fn clear_dirty_flags(&mut self) {
for node in self.nodes.values_mut() {
node.state.dirty = false;
}
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn set_state(&mut self, id: DomId, state: super::node::NodeState) {
if let Some(node) = self.nodes.get_mut(&id) {
node.state = state;
}
}
pub fn set_focused(&mut self, id: Option<DomId>) {
for node in self.nodes.values_mut() {
node.state.focused = false;
}
if let Some(focus_id) = id {
if let Some(node) = self.nodes.get_mut(&focus_id) {
node.state.focused = true;
}
}
}
pub fn set_hovered(&mut self, id: Option<DomId>) {
for node in self.nodes.values_mut() {
node.state.hovered = false;
}
if let Some(hover_id) = id {
if let Some(node) = self.nodes.get_mut(&hover_id) {
node.state.hovered = true;
}
}
}
fn matches_selector(&self, node: &DomNode, selector: &Selector) -> bool {
if selector.parts.is_empty() {
return false;
}
self.matches_selector_from(node, selector, selector.parts.len() - 1)
}
fn matches_selector_from(&self, node: &DomNode, selector: &Selector, part_idx: usize) -> bool {
let (part, _) = &selector.parts[part_idx];
if !self.matches_part(part, node) {
return false;
}
if part_idx == 0 {
return true;
}
let prev_combinator = selector.parts[part_idx - 1].1;
match prev_combinator {
Some(Combinator::Descendant) => {
let mut current = node.parent;
while let Some(parent_id) = current {
if let Some(parent) = self.nodes.get(&parent_id) {
if self.matches_selector_from(parent, selector, part_idx - 1) {
return true;
}
current = parent.parent;
} else {
break;
}
}
false
}
Some(Combinator::Child) => {
if let Some(parent_id) = node.parent {
if let Some(parent) = self.nodes.get(&parent_id) {
return self.matches_selector_from(parent, selector, part_idx - 1);
}
}
false
}
Some(Combinator::AdjacentSibling) => {
if let Some(prev_sibling) = self.get_previous_sibling(node) {
return self.matches_selector_from(prev_sibling, selector, part_idx - 1);
}
false
}
Some(Combinator::GeneralSibling) => {
let mut current = self.get_previous_sibling(node);
while let Some(sibling) = current {
if self.matches_selector_from(sibling, selector, part_idx - 1) {
return true;
}
current = self.get_previous_sibling(sibling);
}
false
}
None => {
true
}
}
}
fn matches_part(&self, part: &SelectorPart, node: &DomNode) -> bool {
if part.universal
&& part.id.is_none()
&& part.classes.is_empty()
&& part.pseudo_classes.is_empty()
&& part.element.is_none()
{
return true;
}
if let Some(ref elem) = part.element {
if node.widget_type() != elem {
return false;
}
}
if let Some(id) = &part.id {
if node.element_id() != Some(id.as_str()) {
return false;
}
}
for class in &part.classes {
if !node.has_class(class) {
return false;
}
}
for pseudo in &part.pseudo_classes {
if !node.matches_pseudo(pseudo) {
return false;
}
}
true
}
fn get_previous_sibling(&self, node: &DomNode) -> Option<&DomNode> {
let parent_id = node.parent?;
let parent = self.nodes.get(&parent_id)?;
let idx = parent.children.iter().position(|&id| id == node.id)?;
if idx > 0 {
self.nodes.get(&parent.children[idx - 1])
} else {
None
}
}
fn get_or_parse_selector(&self, selector_str: &str) -> Option<Selector> {
{
let cache = self.selector_cache.read().ok()?;
if let Some(selector) = cache.get(selector_str) {
return Some(selector.clone());
}
}
let parsed = parse_selector(selector_str).ok()?;
if let Ok(mut cache) = self.selector_cache.write() {
cache.insert(selector_str.to_string(), parsed.clone());
}
Some(parsed)
}
pub fn clear_selector_cache(&self) {
if let Ok(mut cache) = self.selector_cache.write() {
cache.clear();
}
}
}
impl Query for DomTree {
fn query_one(&self, selector_str: &str) -> Option<&DomNode> {
let selector = self.get_or_parse_selector(selector_str)?;
self.nodes
.values()
.find(|node| self.matches_selector(node, &selector))
}
fn query_all(&self, selector_str: &str) -> QueryResult<'_> {
let selector = match self.get_or_parse_selector(selector_str) {
Some(s) => s,
None => return QueryResult::empty(),
};
let nodes: Vec<_> = self
.nodes
.values()
.filter(|node| self.matches_selector(node, &selector))
.collect();
QueryResult::from_nodes(nodes)
}
fn get_by_id(&self, id: &str) -> Option<&DomNode> {
self.id_map
.get(id)
.and_then(|dom_id| self.nodes.get(dom_id))
}
fn get_by_class(&self, class: &str) -> QueryResult<'_> {
let node_ids = self.class_index.get(class);
if let Some(ids) = node_ids {
let nodes: Vec<_> = ids.iter().filter_map(|id| self.nodes.get(id)).collect();
QueryResult::from_nodes(nodes)
} else {
QueryResult::empty()
}
}
fn get_by_type(&self, widget_type: &str) -> QueryResult<'_> {
let node_ids = self.type_index.get(widget_type);
if let Some(ids) = node_ids {
let nodes: Vec<_> = ids.iter().filter_map(|id| self.nodes.get(id)).collect();
QueryResult::from_nodes(nodes)
} else {
QueryResult::empty()
}
}
}
#[cfg(test)]
mod tests {
use super::super::node::WidgetMeta;
use super::*;
fn create_test_tree() -> DomTree {
let mut tree = DomTree::new();
let root = tree.create_root(WidgetMeta::new("App").id("app"));
let sidebar = tree.add_child(root, WidgetMeta::new("Container").class("sidebar"));
tree.add_child(
sidebar,
WidgetMeta::new("Button").class("primary").id("nav-home"),
);
tree.add_child(sidebar, WidgetMeta::new("Button").id("nav-settings"));
let content = tree.add_child(root, WidgetMeta::new("Container").class("content"));
tree.add_child(content, WidgetMeta::new("Container").class("card"));
tree.add_child(
content,
WidgetMeta::new("Container").class("card").class("featured"),
);
tree
}
#[test]
fn test_query_by_id() {
let tree = create_test_tree();
let node = tree.get_by_id("app");
assert!(node.is_some());
assert_eq!(node.unwrap().widget_type(), "App");
let node = tree.get_by_id("nav-home");
assert!(node.is_some());
assert_eq!(node.unwrap().widget_type(), "Button");
}
#[test]
fn test_query_by_type() {
let tree = create_test_tree();
let buttons = tree.get_by_type("Button");
assert_eq!(buttons.len(), 2);
}
#[test]
fn test_query_by_class() {
let tree = create_test_tree();
let cards = tree.get_by_class("card");
assert_eq!(cards.len(), 2);
let featured = tree.get_by_class("featured");
assert_eq!(featured.len(), 1);
}
#[test]
fn test_query_one() {
let tree = create_test_tree();
let node = tree.query_one("Button");
assert!(node.is_some());
let node = tree.query_one("#nav-home");
assert!(node.is_some());
let node = tree.query_one(".primary");
assert!(node.is_some());
let node = tree.query_one("#nonexistent");
assert!(node.is_none());
}
#[test]
fn test_query_all() {
let tree = create_test_tree();
let results = tree.query_all("Button");
assert_eq!(results.len(), 2);
let results = tree.query_all(".card");
assert_eq!(results.len(), 2);
let results = tree.query_all("Container");
assert_eq!(results.len(), 4); }
#[test]
fn test_query_combined() {
let tree = create_test_tree();
let results = tree.query_all("Button.primary");
assert_eq!(results.len(), 1);
let results = tree.query_all("Container.card.featured");
assert_eq!(results.len(), 1);
}
#[test]
fn test_sibling_positions() {
let tree = create_test_tree();
let home = tree.get_by_id("nav-home").unwrap();
let settings = tree.get_by_id("nav-settings").unwrap();
assert!(home.state.first_child);
assert!(!home.state.last_child);
assert!(!settings.state.first_child);
assert!(settings.state.last_child);
}
#[test]
fn test_query_result_methods() {
let tree = create_test_tree();
let results = tree.query_all("Button");
assert_eq!(results.len(), 2);
assert!(!results.is_empty());
let first = results.first();
assert!(first.is_some());
let all = results.all();
assert_eq!(all.len(), 2);
let count = results.iter().count();
assert_eq!(count, 2);
let empty = tree.query_all("NonExistent");
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert!(empty.first().is_none());
}
#[test]
fn test_dom_tree_basic_methods() {
let mut tree = create_test_tree();
let root_id = tree.root_id().unwrap();
assert!(tree.get(root_id).is_some());
assert!(tree.get_mut(root_id).is_some());
let root = tree.root();
assert!(root.is_some());
assert_eq!(root.unwrap().widget_type(), "App");
assert!(!tree.is_empty());
assert!(tree.len() > 0);
let count = tree.nodes().count();
assert_eq!(count, 7); }
#[test]
fn test_dom_tree_state_methods() {
let mut tree = create_test_tree();
let root_id = tree.root_id().unwrap();
let mut new_state = tree.get(root_id).unwrap().state.clone();
new_state.focused = true;
tree.set_state(root_id, new_state);
assert!(tree.get(root_id).unwrap().state.focused);
let button_id = tree.get_by_id("nav-home").unwrap().id;
tree.set_focused(Some(button_id));
assert!(tree.get(button_id).unwrap().state.focused);
assert!(!tree.get(root_id).unwrap().state.focused);
tree.set_hovered(Some(button_id));
assert!(tree.get(button_id).unwrap().state.hovered);
tree.set_focused(None);
assert!(!tree.get(button_id).unwrap().state.focused);
}
#[test]
fn test_dom_tree_dirty_nodes() {
let mut tree = create_test_tree();
let dirty = tree.get_dirty_nodes();
assert!(!dirty.is_empty());
tree.clear_dirty_flags();
let dirty = tree.get_dirty_nodes();
assert!(dirty.is_empty());
}
#[test]
fn test_query_combinators() {
let mut tree = DomTree::new();
let root = tree.create_root(WidgetMeta::new("App").id("app"));
let container = tree.add_child(root, WidgetMeta::new("Container").id("container"));
let _button = tree.add_child(container, WidgetMeta::new("Button").id("btn"));
let result = tree.query_one("Container Button");
assert!(result.is_some());
let result = tree.query_one("Container > Button");
assert!(result.is_some());
let result = tree.query_one("App > Button");
assert!(result.is_none());
}
#[test]
fn test_remove_node() {
let mut tree = create_test_tree();
let content_id = tree.get_by_type("Container").first().unwrap().id;
tree.remove(content_id);
assert!(tree.get(content_id).is_none());
}
}