use std::collections::BTreeMap;
use azul_css::{
Css, CssContentGroup, CssPath,
CssPathSelector, CssPathPseudoSelector, CssNthChildSelector::*,
};
use crate::{
dom::{DomId, NodeData},
id_tree::{NodeId, NodeHierarchy, NodeDataContainer},
callbacks::HitTestItem,
ui_state::{UiState, HoverGroup, ActiveHover},
ui_description::{UiDescription, StyledNode},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HtmlCascadeInfo {
pub index_in_parent: usize,
pub is_last_child: bool,
pub is_hovered_over: bool,
pub is_focused: bool,
pub is_active: bool,
}
pub fn matches_html_element(
css_path: &CssPath,
node_id: NodeId,
node_hierarchy: &NodeHierarchy,
node_data: &NodeDataContainer<NodeData>,
html_node_tree: &NodeDataContainer<HtmlCascadeInfo>,
) -> bool {
use self::CssGroupSplitReason::*;
if css_path.selectors.is_empty() {
return false;
}
let mut current_node = Some(node_id);
let mut direct_parent_has_to_match = false;
let mut last_selector_matched = true;
for (content_group, reason) in CssGroupIterator::new(&css_path.selectors) {
let cur_node_id = match current_node {
Some(c) => c,
None => {
return *content_group == [&CssPathSelector::Global];
},
};
let current_selector_matches = selector_group_matches(&content_group, &html_node_tree[cur_node_id], &node_data[cur_node_id]);
if direct_parent_has_to_match && !current_selector_matches {
return false;
}
if current_selector_matches && !last_selector_matched {
return false;
}
last_selector_matched = current_selector_matches;
direct_parent_has_to_match = reason == DirectChildren;
current_node = node_hierarchy[cur_node_id].parent;
}
last_selector_matched
}
pub fn match_dom_selectors(
ui_state: &UiState,
css: &Css,
focused_node: &Option<(DomId, NodeId)>,
hovered_nodes: &BTreeMap<NodeId, HitTestItem>,
is_mouse_down: bool,
) -> UiDescription {
use azul_css::CssDeclaration;
let non_leaf_nodes = ui_state.dom.arena.node_hierarchy.get_parents_sorted_by_depth();
let html_tree = construct_html_cascade_tree(
&ui_state.dom.arena.node_hierarchy,
&non_leaf_nodes,
focused_node.as_ref().and_then(|(dom_id, node_id)| {
if *dom_id == ui_state.dom_id { Some(*node_id) } else { None }
}),
hovered_nodes,
is_mouse_down,
);
let mut styled_nodes = ui_state.dom.arena.node_data.transform(|_, node_id| StyledNode {
css_constraints: css
.rules()
.filter(|rule| matches_html_element(&rule.path, node_id, &ui_state.dom.arena.node_hierarchy, &ui_state.dom.arena.node_data, &html_tree))
.flat_map(|matched_rule| matched_rule.declarations.iter().map(|declaration| (declaration.get_type(), declaration.clone())))
.collect(),
});
for (_depth, parent_id) in non_leaf_nodes {
let inherited_rules: Vec<CssDeclaration> = styled_nodes[parent_id].css_constraints.values().filter(|prop| prop.is_inheritable()).cloned().collect();
if inherited_rules.is_empty() {
continue;
}
for child_id in parent_id.children(&ui_state.dom.arena.node_hierarchy) {
for inherited_rule in &inherited_rules {
let inherited_rule_type = inherited_rule.get_type();
styled_nodes[child_id].css_constraints.entry(inherited_rule_type).or_insert_with(|| inherited_rule.clone());
}
}
}
let selected_hover_nodes = match_hover_selectors(
collect_hover_groups(css),
&ui_state.dom.arena.node_hierarchy,
&ui_state.dom.arena.node_data,
&html_tree,
);
UiDescription {
dom_id: ui_state.dom_id.clone(),
html_tree,
dynamic_css_overrides: ui_state.dynamic_css_overrides.clone(),
ui_descr_root: ui_state.dom.root,
styled_nodes,
selected_hover_nodes,
}
}
pub struct CssGroupIterator<'a> {
pub css_path: &'a Vec<CssPathSelector>,
pub current_idx: usize,
pub last_reason: CssGroupSplitReason,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CssGroupSplitReason {
Children,
DirectChildren,
}
impl<'a> CssGroupIterator<'a> {
pub fn new(css_path: &'a Vec<CssPathSelector>) -> Self {
let initial_len = css_path.len();
Self {
css_path,
current_idx: initial_len,
last_reason: CssGroupSplitReason::Children,
}
}
}
impl<'a> Iterator for CssGroupIterator<'a> {
type Item = (CssContentGroup<'a>, CssGroupSplitReason);
fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
use self::CssPathSelector::*;
let mut new_idx = self.current_idx;
if new_idx == 0 {
return None;
}
let mut current_path = Vec::new();
while new_idx != 0 {
match self.css_path.get(new_idx - 1)? {
Children => {
self.last_reason = CssGroupSplitReason::Children;
break;
},
DirectChildren => {
self.last_reason = CssGroupSplitReason::DirectChildren;
break;
},
other => current_path.push(other),
}
new_idx -= 1;
}
#[cfg(test)]
current_path.reverse();
if new_idx == 0 {
if current_path.is_empty() {
None
} else {
self.current_idx = 0;
Some((current_path, self.last_reason))
}
} else {
self.current_idx = new_idx - 1;
Some((current_path, self.last_reason))
}
}
}
pub fn construct_html_cascade_tree(
node_hierarchy: &NodeHierarchy,
node_depths_sorted: &[(usize, NodeId)],
focused_item: Option<NodeId>,
hovered_items: &BTreeMap<NodeId, HitTestItem>,
is_mouse_down: bool
) -> NodeDataContainer<HtmlCascadeInfo> {
let mut nodes = (0..node_hierarchy.len()).map(|_| HtmlCascadeInfo {
index_in_parent: 0,
is_last_child: false,
is_hovered_over: false,
is_active: false,
is_focused: false,
}).collect::<Vec<_>>();
for (_depth, parent_id) in node_depths_sorted {
let index_in_parent = parent_id.preceding_siblings(node_hierarchy).count();
let is_parent_hovered_over = hovered_items.contains_key(parent_id);
let parent_html_matcher = HtmlCascadeInfo {
index_in_parent: index_in_parent,
is_last_child: node_hierarchy[*parent_id].next_sibling.is_none(),
is_hovered_over: is_parent_hovered_over,
is_active: is_parent_hovered_over && is_mouse_down,
is_focused: focused_item == Some(*parent_id),
};
nodes[parent_id.index()] = parent_html_matcher;
for (child_idx, child_id) in parent_id.children(node_hierarchy).enumerate() {
let is_child_hovered_over = hovered_items.contains_key(&child_id);
let child_html_matcher = HtmlCascadeInfo {
index_in_parent: child_idx + 1,
is_last_child: node_hierarchy[child_id].next_sibling.is_none(),
is_hovered_over: is_child_hovered_over,
is_active: is_child_hovered_over && is_mouse_down,
is_focused: focused_item == Some(child_id),
};
nodes[child_id.index()] = child_html_matcher;
}
}
NodeDataContainer { internal: nodes }
}
pub fn collect_hover_groups(css: &Css) -> BTreeMap<CssPath, HoverGroup> {
use azul_css::{CssPathSelector::*, CssPathPseudoSelector::*};
let hover_rule = PseudoSelector(Hover);
let active_rule = PseudoSelector(Active);
css.rules().filter_map(|rule_block| {
let pos = rule_block.path.selectors.iter().position(|x| *x == hover_rule || *x == active_rule)?;
if rule_block.declarations.is_empty() {
return None;
}
let active_or_hover = match rule_block.path.selectors.get(pos)? {
PseudoSelector(Hover) => ActiveHover::Hover,
PseudoSelector(Active) => ActiveHover::Active,
_ => return None,
};
let css_path = CssPath { selectors: rule_block.path.selectors.iter().cloned().take(pos).collect() };
let hover_group = HoverGroup {
affects_layout: rule_block.declarations.iter().any(|hover_rule| hover_rule.can_trigger_relayout()),
active_or_hover,
};
Some((css_path, hover_group))
}).collect()
}
fn match_hover_selectors(
hover_selectors: BTreeMap<CssPath, HoverGroup>,
node_hierarchy: &NodeHierarchy,
node_data: &NodeDataContainer<NodeData>,
html_node_tree: &NodeDataContainer<HtmlCascadeInfo>,
) -> BTreeMap<NodeId, HoverGroup> {
let mut btree_map = BTreeMap::new();
for (css_path, hover_selector) in hover_selectors {
btree_map.extend(
html_node_tree
.linear_iter()
.filter(|node_id| matches_html_element(&css_path, *node_id, node_hierarchy, node_data, html_node_tree))
.map(|node_id| (node_id, hover_selector))
);
}
btree_map
}
pub fn selector_group_matches(
selectors: &[&CssPathSelector],
html_node: &HtmlCascadeInfo,
node_data: &NodeData,
) -> bool {
use self::CssPathSelector::*;
for selector in selectors {
match selector {
Global => { },
Type(t) => {
if node_data.get_node_type().get_path() != *t {
return false;
}
},
Class(c) => {
if !node_data.get_classes().iter().any(|class| class.equals_str(c)) {
return false;
}
},
Id(id) => {
if !node_data.get_ids().iter().any(|html_id| html_id.equals_str(id)) {
return false;
}
},
PseudoSelector(CssPathPseudoSelector::First) => {
if html_node.index_in_parent != 1 { return false; }
},
PseudoSelector(CssPathPseudoSelector::Last) => {
if !html_node.is_last_child { return false; }
},
PseudoSelector(CssPathPseudoSelector::NthChild(x)) => {
match *x {
Number(value) => if html_node.index_in_parent != value { return false; },
Even => if html_node.index_in_parent % 2 == 0 { return false; },
Odd => if html_node.index_in_parent % 2 == 1 { return false; },
Pattern { repeat, offset } => if html_node.index_in_parent >= offset &&
((html_node.index_in_parent - offset) % repeat != 0) { return false; },
}
},
PseudoSelector(CssPathPseudoSelector::Hover) => {
if !html_node.is_hovered_over { return false; }
},
PseudoSelector(CssPathPseudoSelector::Active) => {
if !html_node.is_active { return false; }
},
PseudoSelector(CssPathPseudoSelector::Focus) => {
if !html_node.is_focused { return false; }
},
DirectChildren | Children => {
panic!("Unreachable: DirectChildren or Children in CSS path!");
},
}
}
true
}
#[test]
fn test_case_issue_93() {
use azul_css::CssPathSelector::*;
use azul_css::*;
use crate::dom::*;
struct DataModel;
fn render_tab() -> Dom<DataModel> {
Dom::div().with_class("tabwidget-tab")
.with_child(Dom::label("").with_class("tabwidget-tab-label"))
.with_child(Dom::label("").with_class("tabwidget-tab-close"))
}
let dom = Dom::div().with_id("editor-rooms")
.with_child(
Dom::div().with_class("tabwidget-bar")
.with_child(render_tab().with_class("active"))
.with_child(render_tab())
.with_child(render_tab())
.with_child(render_tab())
);
let dom = convert_dom_into_compact_dom(dom);
let tab_active_close = CssPath { selectors: vec![
Class("tabwidget-tab".into()),
Class("active".into()),
Children,
Class("tabwidget-tab-close".into())
] };
let node_hierarchy = &dom.arena.node_hierarchy;
let node_data = &dom.arena.node_data;
let nodes_sorted: Vec<_> = node_hierarchy.get_parents_sorted_by_depth();
let html_node_tree = construct_html_cascade_tree(
&node_hierarchy,
&nodes_sorted,
None,
&BTreeMap::new(),
false,
);
assert_eq!(matches_html_element(&tab_active_close, NodeId::new(3), &node_hierarchy, &node_data, &html_node_tree), false);
assert_eq!(matches_html_element(&tab_active_close, NodeId::new(4), &node_hierarchy, &node_data, &html_node_tree), true);
}
#[test]
fn test_css_group_iterator() {
use self::CssPathSelector::*;
use azul_css::*;
let selectors = vec![
Class("hello".into()),
DirectChildren,
Id("id_test".into()),
Class("new_class".into()),
Children,
Type(NodeTypePath::Div),
Class("content".into()),
];
let mut it = CssGroupIterator::new(&selectors);
assert_eq!(it.next(), Some((vec![
&Type(NodeTypePath::Div),
&Class("content".into()),
], CssGroupSplitReason::Children)));
assert_eq!(it.next(), Some((vec![
&Id("id_test".into()),
&Class("new_class".into()),
], CssGroupSplitReason::DirectChildren)));
assert_eq!(it.next(), Some((vec![
&Class("hello".into()),
], CssGroupSplitReason::DirectChildren)));
assert_eq!(it.next(), None);
let selectors_2 = vec![
Class("content".into()),
];
let mut it = CssGroupIterator::new(&selectors_2);
assert_eq!(it.next(), Some((vec![
&Class("content".into()),
], CssGroupSplitReason::Children)));
assert_eq!(it.next(), None);
}