azul_core/
style.rs

1//! DOM tree to CSS style tree cascading
2
3use 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/// Has all the necessary information about the style CSS path
17#[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
26/// Returns if the style CSS path matches the DOM node (i.e. if the DOM node should be styled by that element)
27pub 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                // The node has no parent, but the CSS path
50                // still has an extra limitation - only valid if the
51                // next content group is a "*" element
52                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            // If the element was a ">" element and the current,
59            // direct parent does not match, return false
60            return false; // not executed (maybe this is the bug)
61        }
62
63        // If the current selector matches, but the previous one didn't,
64        // that means that the CSS path chain is broken and therefore doesn't match the element
65        if current_selector_matches && !last_selector_matched {
66            return false;
67        }
68
69        // Important: Set if the current selector has matched the element
70        last_selector_matched = current_selector_matches;
71        // Select if the next content group has to exactly match or if it can potentially be skipped
72        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    // First, apply all rules normally (no inheritance) of CSS values
102    // This is an O(n^2) operation, but it can be parallelized in the future
103    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    // Then, inherit all values of the parent to the children, but only if the property is
112    // inheritable and isn't yet set. NOTE: This step can't be parallelized!
113    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                // Only override the rule if the child already has an inherited rule, don't override it
123                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    // In order to hit-test :hover and :active nodes, need to select them
130    // first (to insert their TagId later)
131    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        // NOTE: this clone is necessary, otherwise we wouldn't be able to
141        // update the UiState
142        //
143        // WARNING: The UIState can modify the `arena` with its copy of the Rc !
144        // Be careful about appending things to the arena, since that could modify
145        // the UiDescription without you knowing!
146        //
147        // NOTE: This deep-clones the entire arena, which may be a
148        // performance-sensitive operation!
149        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        // NOTE: Order inside of a ContentGroup is not important
211        // for matching elements, only important for testing
212        #[cfg(test)]
213        current_path.reverse();
214
215        if new_idx == 0 {
216            if current_path.is_empty() {
217                None
218            } else {
219                // Last element of path
220                self.current_idx = 0;
221                Some((current_path, self.last_reason))
222            }
223        } else {
224            // skip the "Children | DirectChildren" element itself
225            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        // Note: :nth-child() starts at 1 instead of 0
250        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, // necessary for nth-child
255            is_last_child: node_hierarchy[*parent_id].next_sibling.is_none(), // Necessary for :last selectors
256            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, // necessary for nth-child
267                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
280/// Returns all CSS paths that have a `:hover` or `:active` in their path
281/// (since they need to have tags for hit-testing)
282pub 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    // Filter out all :hover and :active rules, since we need to create tags
289    // for them after the main CSS styling has been done
290    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
311/// In order to figure out on which nodes to insert the :hover and :active hit-test tags,
312/// we need to select all items that have a :hover or :active tag.
313fn 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
334/// Matches a single group of items, panics on Children or DirectChildren selectors
335///
336/// The intent is to "split" the CSS path into groups by selectors, then store and cache
337/// whether the direct or any parent has matched the path correctly
338pub 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                // Notice: index_in_parent is 1-indexed
366                if html_node.index_in_parent != 1 { return false; }
367            },
368            PseudoSelector(CssPathPseudoSelector::Last) => {
369                // Notice: index_in_parent is 1-indexed
370                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    //  rules: [
444    //    ".tabwidget-tab-label"                        : ColorU::BLACK,
445    //    ".tabwidget-tab.active .tabwidget-tab-label"  : ColorU::WHITE,
446    //    ".tabwidget-tab.active .tabwidget-tab-close"  : ColorU::RED,
447    //  ]
448
449    //  0: [div #editor-rooms ]
450    //   |-- 1: [div  .tabwidget-bar]
451    //   |    |-- 2: [div  .tabwidget-tab .active]
452    //   |    |    |-- 3: [p  .tabwidget-tab-label]
453    //   |    |    |-- 4: [p  .tabwidget-tab-close]
454    //   |    |-- 5: [div  .tabwidget-tab]
455    //   |    |    |-- 6: [p  .tabwidget-tab-label]
456    //   |    |    |-- 7: [p  .tabwidget-tab-close]
457    //   |    |-- 8: [div  .tabwidget-tab]
458    //   |    |    |-- 9: [p  .tabwidget-tab-label]
459    //   |    |    |-- 10: [p  .tabwidget-tab-close]
460    //   |    |-- 11: [div  .tabwidget-tab]
461    //   |    |    |-- 12: [p  .tabwidget-tab-label]
462    //   |    |    |-- 13: [p  .tabwidget-tab-close]
463
464    // Test 1:
465    // ".tabwidget-tab.active .tabwidget-tab-label"
466    // should not match
467    // ".tabwidget-tab.active .tabwidget-tab-close"
468    assert_eq!(matches_html_element(&tab_active_close, NodeId::new(3), &node_hierarchy, &node_data, &html_node_tree), false);
469
470    // Test 2:
471    // ".tabwidget-tab.active .tabwidget-tab-close"
472    // should match
473    // ".tabwidget-tab.active .tabwidget-tab-close"
474    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    // ".hello > #id_text.new_class div.content"
483    // -> ["div.content", "#id_text.new_class", ".hello"]
484    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))); // technically not correct
509
510    assert_eq!(it.next(), None);
511
512    // Test single class
513    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}