1use std::collections::BTreeMap;
4use azul_css::{
5 Css, CssContentGroup, CssPath,
6 CssPathSelector, CssPathPseudoSelector, CssNthChildSelector::*,
7};
8use crate::{
9 dom::{DomId, NodeData},
10 id_tree::{NodeId, NodeHierarchy, NodeDataContainer},
11 callbacks::HitTestItem,
12 ui_state::{UiState, HoverGroup, ActiveHover},
13 ui_description::{UiDescription, StyledNode},
14};
15
16#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct HtmlCascadeInfo {
19 pub index_in_parent: usize,
20 pub is_last_child: bool,
21 pub is_hovered_over: bool,
22 pub is_focused: bool,
23 pub is_active: bool,
24}
25
26pub fn matches_html_element(
28 css_path: &CssPath,
29 node_id: NodeId,
30 node_hierarchy: &NodeHierarchy,
31 node_data: &NodeDataContainer<NodeData>,
32 html_node_tree: &NodeDataContainer<HtmlCascadeInfo>,
33) -> bool {
34
35 use self::CssGroupSplitReason::*;
36
37 if css_path.selectors.is_empty() {
38 return false;
39 }
40
41 let mut current_node = Some(node_id);
42 let mut direct_parent_has_to_match = false;
43 let mut last_selector_matched = true;
44
45 for (content_group, reason) in CssGroupIterator::new(&css_path.selectors) {
46 let cur_node_id = match current_node {
47 Some(c) => c,
48 None => {
49 return *content_group == [&CssPathSelector::Global];
53 },
54 };
55 let current_selector_matches = selector_group_matches(&content_group, &html_node_tree[cur_node_id], &node_data[cur_node_id]);
56
57 if direct_parent_has_to_match && !current_selector_matches {
58 return false; }
62
63 if current_selector_matches && !last_selector_matched {
66 return false;
67 }
68
69 last_selector_matched = current_selector_matches;
71 direct_parent_has_to_match = reason == DirectChildren;
73 current_node = node_hierarchy[cur_node_id].parent;
74 }
75
76 last_selector_matched
77}
78
79pub fn match_dom_selectors(
80 ui_state: &UiState,
81 css: &Css,
82 focused_node: &Option<(DomId, NodeId)>,
83 hovered_nodes: &BTreeMap<NodeId, HitTestItem>,
84 is_mouse_down: bool,
85) -> UiDescription {
86
87 use azul_css::CssDeclaration;
88
89 let non_leaf_nodes = ui_state.dom.arena.node_hierarchy.get_parents_sorted_by_depth();
90
91 let html_tree = construct_html_cascade_tree(
92 &ui_state.dom.arena.node_hierarchy,
93 &non_leaf_nodes,
94 focused_node.as_ref().and_then(|(dom_id, node_id)| {
95 if *dom_id == ui_state.dom_id { Some(*node_id) } else { None }
96 }),
97 hovered_nodes,
98 is_mouse_down,
99 );
100
101 let mut styled_nodes = ui_state.dom.arena.node_data.transform(|_, node_id| StyledNode {
104 css_constraints: css
105 .rules()
106 .filter(|rule| matches_html_element(&rule.path, node_id, &ui_state.dom.arena.node_hierarchy, &ui_state.dom.arena.node_data, &html_tree))
107 .flat_map(|matched_rule| matched_rule.declarations.iter().map(|declaration| (declaration.get_type(), declaration.clone())))
108 .collect(),
109 });
110
111 for (_depth, parent_id) in non_leaf_nodes {
114
115 let inherited_rules: Vec<CssDeclaration> = styled_nodes[parent_id].css_constraints.values().filter(|prop| prop.is_inheritable()).cloned().collect();
116 if inherited_rules.is_empty() {
117 continue;
118 }
119
120 for child_id in parent_id.children(&ui_state.dom.arena.node_hierarchy) {
121 for inherited_rule in &inherited_rules {
122 let inherited_rule_type = inherited_rule.get_type();
124 styled_nodes[child_id].css_constraints.entry(inherited_rule_type).or_insert_with(|| inherited_rule.clone());
125 }
126 }
127 }
128
129 let selected_hover_nodes = match_hover_selectors(
132 collect_hover_groups(css),
133 &ui_state.dom.arena.node_hierarchy,
134 &ui_state.dom.arena.node_data,
135 &html_tree,
136 );
137
138 UiDescription {
139
140 dom_id: ui_state.dom_id.clone(),
150 html_tree,
151 dynamic_css_overrides: ui_state.dynamic_css_overrides.clone(),
152 ui_descr_root: ui_state.dom.root,
153 styled_nodes,
154 selected_hover_nodes,
155 }
156}
157
158pub struct CssGroupIterator<'a> {
159 pub css_path: &'a Vec<CssPathSelector>,
160 pub current_idx: usize,
161 pub last_reason: CssGroupSplitReason,
162}
163
164#[derive(Debug, Copy, Clone, PartialEq, Eq)]
165pub enum CssGroupSplitReason {
166 Children,
167 DirectChildren,
168}
169
170impl<'a> CssGroupIterator<'a> {
171 pub fn new(css_path: &'a Vec<CssPathSelector>) -> Self {
172 let initial_len = css_path.len();
173 Self {
174 css_path,
175 current_idx: initial_len,
176 last_reason: CssGroupSplitReason::Children,
177 }
178 }
179}
180
181impl<'a> Iterator for CssGroupIterator<'a> {
182 type Item = (CssContentGroup<'a>, CssGroupSplitReason);
183
184 fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
185 use self::CssPathSelector::*;
186
187 let mut new_idx = self.current_idx;
188
189 if new_idx == 0 {
190 return None;
191 }
192
193 let mut current_path = Vec::new();
194
195 while new_idx != 0 {
196 match self.css_path.get(new_idx - 1)? {
197 Children => {
198 self.last_reason = CssGroupSplitReason::Children;
199 break;
200 },
201 DirectChildren => {
202 self.last_reason = CssGroupSplitReason::DirectChildren;
203 break;
204 },
205 other => current_path.push(other),
206 }
207 new_idx -= 1;
208 }
209
210 #[cfg(test)]
213 current_path.reverse();
214
215 if new_idx == 0 {
216 if current_path.is_empty() {
217 None
218 } else {
219 self.current_idx = 0;
221 Some((current_path, self.last_reason))
222 }
223 } else {
224 self.current_idx = new_idx - 1;
226 Some((current_path, self.last_reason))
227 }
228 }
229}
230
231pub fn construct_html_cascade_tree(
232 node_hierarchy: &NodeHierarchy,
233 node_depths_sorted: &[(usize, NodeId)],
234 focused_item: Option<NodeId>,
235 hovered_items: &BTreeMap<NodeId, HitTestItem>,
236 is_mouse_down: bool
237) -> NodeDataContainer<HtmlCascadeInfo> {
238
239 let mut nodes = (0..node_hierarchy.len()).map(|_| HtmlCascadeInfo {
240 index_in_parent: 0,
241 is_last_child: false,
242 is_hovered_over: false,
243 is_active: false,
244 is_focused: false,
245 }).collect::<Vec<_>>();
246
247 for (_depth, parent_id) in node_depths_sorted {
248
249 let index_in_parent = parent_id.preceding_siblings(node_hierarchy).count();
251
252 let is_parent_hovered_over = hovered_items.contains_key(parent_id);
253 let parent_html_matcher = HtmlCascadeInfo {
254 index_in_parent: index_in_parent, is_last_child: node_hierarchy[*parent_id].next_sibling.is_none(), is_hovered_over: is_parent_hovered_over,
257 is_active: is_parent_hovered_over && is_mouse_down,
258 is_focused: focused_item == Some(*parent_id),
259 };
260
261 nodes[parent_id.index()] = parent_html_matcher;
262
263 for (child_idx, child_id) in parent_id.children(node_hierarchy).enumerate() {
264 let is_child_hovered_over = hovered_items.contains_key(&child_id);
265 let child_html_matcher = HtmlCascadeInfo {
266 index_in_parent: child_idx + 1, is_last_child: node_hierarchy[child_id].next_sibling.is_none(),
268 is_hovered_over: is_child_hovered_over,
269 is_active: is_child_hovered_over && is_mouse_down,
270 is_focused: focused_item == Some(child_id),
271 };
272
273 nodes[child_id.index()] = child_html_matcher;
274 }
275 }
276
277 NodeDataContainer { internal: nodes }
278}
279
280pub fn collect_hover_groups(css: &Css) -> BTreeMap<CssPath, HoverGroup> {
283 use azul_css::{CssPathSelector::*, CssPathPseudoSelector::*};
284
285 let hover_rule = PseudoSelector(Hover);
286 let active_rule = PseudoSelector(Active);
287
288 css.rules().filter_map(|rule_block| {
291 let pos = rule_block.path.selectors.iter().position(|x| *x == hover_rule || *x == active_rule)?;
292 if rule_block.declarations.is_empty() {
293 return None;
294 }
295
296 let active_or_hover = match rule_block.path.selectors.get(pos)? {
297 PseudoSelector(Hover) => ActiveHover::Hover,
298 PseudoSelector(Active) => ActiveHover::Active,
299 _ => return None,
300 };
301
302 let css_path = CssPath { selectors: rule_block.path.selectors.iter().cloned().take(pos).collect() };
303 let hover_group = HoverGroup {
304 affects_layout: rule_block.declarations.iter().any(|hover_rule| hover_rule.can_trigger_relayout()),
305 active_or_hover,
306 };
307 Some((css_path, hover_group))
308 }).collect()
309}
310
311fn match_hover_selectors(
314 hover_selectors: BTreeMap<CssPath, HoverGroup>,
315 node_hierarchy: &NodeHierarchy,
316 node_data: &NodeDataContainer<NodeData>,
317 html_node_tree: &NodeDataContainer<HtmlCascadeInfo>,
318) -> BTreeMap<NodeId, HoverGroup> {
319
320 let mut btree_map = BTreeMap::new();
321
322 for (css_path, hover_selector) in hover_selectors {
323 btree_map.extend(
324 html_node_tree
325 .linear_iter()
326 .filter(|node_id| matches_html_element(&css_path, *node_id, node_hierarchy, node_data, html_node_tree))
327 .map(|node_id| (node_id, hover_selector))
328 );
329 }
330
331 btree_map
332}
333
334pub fn selector_group_matches(
339 selectors: &[&CssPathSelector],
340 html_node: &HtmlCascadeInfo,
341 node_data: &NodeData,
342) -> bool {
343
344 use self::CssPathSelector::*;
345
346 for selector in selectors {
347 match selector {
348 Global => { },
349 Type(t) => {
350 if node_data.get_node_type().get_path() != *t {
351 return false;
352 }
353 },
354 Class(c) => {
355 if !node_data.get_classes().iter().any(|class| class.equals_str(c)) {
356 return false;
357 }
358 },
359 Id(id) => {
360 if !node_data.get_ids().iter().any(|html_id| html_id.equals_str(id)) {
361 return false;
362 }
363 },
364 PseudoSelector(CssPathPseudoSelector::First) => {
365 if html_node.index_in_parent != 1 { return false; }
367 },
368 PseudoSelector(CssPathPseudoSelector::Last) => {
369 if !html_node.is_last_child { return false; }
371 },
372 PseudoSelector(CssPathPseudoSelector::NthChild(x)) => {
373 match *x {
374 Number(value) => if html_node.index_in_parent != value { return false; },
375 Even => if html_node.index_in_parent % 2 == 0 { return false; },
376 Odd => if html_node.index_in_parent % 2 == 1 { return false; },
377 Pattern { repeat, offset } => if html_node.index_in_parent >= offset &&
378 ((html_node.index_in_parent - offset) % repeat != 0) { return false; },
379 }
380 },
381 PseudoSelector(CssPathPseudoSelector::Hover) => {
382 if !html_node.is_hovered_over { return false; }
383 },
384 PseudoSelector(CssPathPseudoSelector::Active) => {
385 if !html_node.is_active { return false; }
386 },
387 PseudoSelector(CssPathPseudoSelector::Focus) => {
388 if !html_node.is_focused { return false; }
389 },
390 DirectChildren | Children => {
391 panic!("Unreachable: DirectChildren or Children in CSS path!");
392 },
393 }
394 }
395
396 true
397}
398
399#[test]
400fn test_case_issue_93() {
401
402 use azul_css::CssPathSelector::*;
403 use azul_css::*;
404 use crate::dom::*;
405
406 struct DataModel;
407
408 fn render_tab() -> Dom<DataModel> {
409 Dom::div().with_class("tabwidget-tab")
410 .with_child(Dom::label("").with_class("tabwidget-tab-label"))
411 .with_child(Dom::label("").with_class("tabwidget-tab-close"))
412 }
413
414 let dom = Dom::div().with_id("editor-rooms")
415 .with_child(
416 Dom::div().with_class("tabwidget-bar")
417 .with_child(render_tab().with_class("active"))
418 .with_child(render_tab())
419 .with_child(render_tab())
420 .with_child(render_tab())
421 );
422
423 let dom = convert_dom_into_compact_dom(dom);
424
425 let tab_active_close = CssPath { selectors: vec![
426 Class("tabwidget-tab".into()),
427 Class("active".into()),
428 Children,
429 Class("tabwidget-tab-close".into())
430 ] };
431
432 let node_hierarchy = &dom.arena.node_hierarchy;
433 let node_data = &dom.arena.node_data;
434 let nodes_sorted: Vec<_> = node_hierarchy.get_parents_sorted_by_depth();
435 let html_node_tree = construct_html_cascade_tree(
436 &node_hierarchy,
437 &nodes_sorted,
438 None,
439 &BTreeMap::new(),
440 false,
441 );
442
443 assert_eq!(matches_html_element(&tab_active_close, NodeId::new(3), &node_hierarchy, &node_data, &html_node_tree), false);
469
470 assert_eq!(matches_html_element(&tab_active_close, NodeId::new(4), &node_hierarchy, &node_data, &html_node_tree), true);
475}
476
477#[test]
478fn test_css_group_iterator() {
479 use self::CssPathSelector::*;
480 use azul_css::*;
481
482 let selectors = vec![
485 Class("hello".into()),
486 DirectChildren,
487 Id("id_test".into()),
488 Class("new_class".into()),
489 Children,
490 Type(NodeTypePath::Div),
491 Class("content".into()),
492 ];
493
494 let mut it = CssGroupIterator::new(&selectors);
495
496 assert_eq!(it.next(), Some((vec![
497 &Type(NodeTypePath::Div),
498 &Class("content".into()),
499 ], CssGroupSplitReason::Children)));
500
501 assert_eq!(it.next(), Some((vec![
502 &Id("id_test".into()),
503 &Class("new_class".into()),
504 ], CssGroupSplitReason::DirectChildren)));
505
506 assert_eq!(it.next(), Some((vec![
507 &Class("hello".into()),
508 ], CssGroupSplitReason::DirectChildren))); assert_eq!(it.next(), None);
511
512 let selectors_2 = vec![
514 Class("content".into()),
515 ];
516
517 let mut it = CssGroupIterator::new(&selectors_2);
518
519 assert_eq!(it.next(), Some((vec![
520 &Class("content".into()),
521 ], CssGroupSplitReason::Children)));
522
523 assert_eq!(it.next(), None);
524}