azul_core/
style.rs

1//! DOM tree to CSS style tree cascading
2
3use alloc::vec::Vec;
4
5use azul_css::{
6    CssContentGroup, CssNthChildSelector::*, CssPath, CssPathPseudoSelector, CssPathSelector,
7};
8
9use crate::{
10    dom::NodeData,
11    id_tree::{NodeDataContainer, NodeDataContainerRef, NodeHierarchyRef, NodeId},
12    styled_dom::NodeHierarchyItem,
13};
14
15/// Has all the necessary information about the style CSS path
16#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[repr(C)]
18pub struct CascadeInfo {
19    pub index_in_parent: u32,
20    pub is_last_child: bool,
21}
22
23impl_vec!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor);
24impl_vec_mut!(CascadeInfo, CascadeInfoVec);
25impl_vec_debug!(CascadeInfo, CascadeInfoVec);
26impl_vec_partialord!(CascadeInfo, CascadeInfoVec);
27impl_vec_clone!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor);
28impl_vec_partialeq!(CascadeInfo, CascadeInfoVec);
29
30impl CascadeInfoVec {
31    pub fn as_container<'a>(&'a self) -> NodeDataContainerRef<'a, CascadeInfo> {
32        NodeDataContainerRef {
33            internal: self.as_ref(),
34        }
35    }
36}
37
38/// Returns if the style CSS path matches the DOM node (i.e. if the DOM node should be styled by
39/// that element)
40pub(crate) fn matches_html_element(
41    css_path: &CssPath,
42    node_id: NodeId,
43    node_hierarchy: &NodeDataContainerRef<NodeHierarchyItem>,
44    node_data: &NodeDataContainerRef<NodeData>,
45    html_node_tree: &NodeDataContainerRef<CascadeInfo>,
46    expected_path_ending: Option<CssPathPseudoSelector>,
47) -> bool {
48    use self::CssGroupSplitReason::*;
49
50    if css_path.selectors.is_empty() {
51        return false;
52    }
53
54    let mut current_node = Some(node_id);
55    let mut direct_parent_has_to_match = false;
56    let mut last_selector_matched = true;
57
58    let mut iterator = CssGroupIterator::new(css_path.selectors.as_ref());
59    while let Some((content_group, reason)) = iterator.next() {
60        let is_last_content_group = iterator.is_last_content_group();
61        let cur_node_id = match current_node {
62            Some(c) => c,
63            None => {
64                // The node has no parent, but the CSS path
65                // still has an extra limitation - only valid if the
66                // next content group is a "*" element
67                return *content_group == [&CssPathSelector::Global];
68            }
69        };
70
71        let current_selector_matches = selector_group_matches(
72            &content_group,
73            &html_node_tree[cur_node_id],
74            &node_data[cur_node_id],
75            expected_path_ending,
76            is_last_content_group,
77        );
78
79        if direct_parent_has_to_match && !current_selector_matches {
80            // If the element was a ">" element and the current,
81            // direct parent does not match, return false
82            return false; // not executed (maybe this is the bug)
83        }
84
85        // If the current selector matches, but the previous one didn't,
86        // that means that the CSS path chain is broken and therefore doesn't match the element
87        if current_selector_matches && !last_selector_matched {
88            return false;
89        }
90
91        // Important: Set if the current selector has matched the element
92        last_selector_matched = current_selector_matches;
93        // Select if the next content group has to exactly match or if it can potentially be skipped
94        direct_parent_has_to_match = reason == DirectChildren;
95        current_node = node_hierarchy[cur_node_id].parent_id();
96    }
97
98    last_selector_matched
99}
100
101/// A CSS group is a group of css selectors in a path that specify the rule that a
102/// certain node has to match, i.e. "div.main.foo" has to match three requirements:
103///
104/// - the node has to be of type div
105/// - the node has to have the class "main"
106/// - the node has to have the class "foo"
107///
108/// If any of these requirements are not met, the CSS block is discarded.
109///
110/// The CssGroupIterator splits the CSS path into semantic blocks, i.e.:
111///
112/// "body > .foo.main > #baz" will be split into ["body", ".foo.main" and "#baz"]
113pub(crate) struct CssGroupIterator<'a> {
114    pub css_path: &'a [CssPathSelector],
115    pub current_idx: usize,
116    pub last_reason: CssGroupSplitReason,
117}
118
119#[derive(Debug, Copy, Clone, PartialEq, Eq)]
120pub(crate) enum CssGroupSplitReason {
121    /// ".foo .main" - match any children
122    Children,
123    /// ".foo > .main" - match only direct children
124    DirectChildren,
125}
126
127impl<'a> CssGroupIterator<'a> {
128    pub fn new(css_path: &'a [CssPathSelector]) -> Self {
129        let initial_len = css_path.len();
130        Self {
131            css_path,
132            current_idx: initial_len,
133            last_reason: CssGroupSplitReason::Children,
134        }
135    }
136    pub fn is_last_content_group(&self) -> bool {
137        self.current_idx.saturating_add(1) == self.css_path.len().saturating_sub(1)
138    }
139}
140
141impl<'a> Iterator for CssGroupIterator<'a> {
142    type Item = (CssContentGroup<'a>, CssGroupSplitReason);
143
144    fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
145        use self::CssPathSelector::*;
146
147        let mut new_idx = self.current_idx;
148
149        if new_idx == 0 {
150            return None;
151        }
152
153        let mut current_path = Vec::new();
154
155        while new_idx != 0 {
156            match self.css_path.get(new_idx - 1)? {
157                Children => {
158                    self.last_reason = CssGroupSplitReason::Children;
159                    break;
160                }
161                DirectChildren => {
162                    self.last_reason = CssGroupSplitReason::DirectChildren;
163                    break;
164                }
165                other => current_path.push(other),
166            }
167            new_idx -= 1;
168        }
169
170        // NOTE: Order inside of a ContentGroup is not important
171        // for matching elements, only important for testing
172        #[cfg(test)]
173        current_path.reverse();
174
175        if new_idx == 0 {
176            if current_path.is_empty() {
177                None
178            } else {
179                // Last element of path
180                self.current_idx = 0;
181                Some((current_path, self.last_reason))
182            }
183        } else {
184            // skip the "Children | DirectChildren" element itself
185            self.current_idx = new_idx - 1;
186            Some((current_path, self.last_reason))
187        }
188    }
189}
190
191pub(crate) fn construct_html_cascade_tree(
192    node_hierarchy: &NodeHierarchyRef,
193    node_depths_sorted: &[(usize, NodeId)],
194) -> NodeDataContainer<CascadeInfo> {
195    let mut nodes = (0..node_hierarchy.len())
196        .map(|_| CascadeInfo {
197            index_in_parent: 0,
198            is_last_child: false,
199        })
200        .collect::<Vec<_>>();
201
202    for (_depth, parent_id) in node_depths_sorted {
203        // Note: :nth-child() starts at 1 instead of 0
204        let index_in_parent = parent_id.preceding_siblings(node_hierarchy).count();
205
206        let parent_html_matcher = CascadeInfo {
207            index_in_parent: (index_in_parent - 1) as u32,
208            is_last_child: node_hierarchy[*parent_id].next_sibling.is_none(), /* Necessary for :last selectors */
209        };
210
211        nodes[parent_id.index()] = parent_html_matcher;
212
213        for (child_idx, child_id) in parent_id.children(node_hierarchy).enumerate() {
214            let child_html_matcher = CascadeInfo {
215                index_in_parent: child_idx as u32,
216                is_last_child: node_hierarchy[child_id].next_sibling.is_none(),
217            };
218
219            nodes[child_id.index()] = child_html_matcher;
220        }
221    }
222
223    NodeDataContainer { internal: nodes }
224}
225
226/// TODO: This is wrong, but it's fast
227#[inline]
228pub fn rule_ends_with(path: &CssPath, target: Option<CssPathPseudoSelector>) -> bool {
229    match target {
230        None => match path.selectors.as_ref().last() {
231            None => false,
232            Some(q) => match q {
233                CssPathSelector::PseudoSelector(_) => false,
234                _ => true,
235            },
236        },
237        Some(s) => match path.selectors.as_ref().last() {
238            None => false,
239            Some(q) => match q {
240                CssPathSelector::PseudoSelector(q) => *q == s,
241                _ => false,
242            },
243        },
244    }
245}
246
247/// Matches a single group of items, panics on Children or DirectChildren selectors
248///
249/// The intent is to "split" the CSS path into groups by selectors, then store and cache
250/// whether the direct or any parent has matched the path correctly
251pub(crate) fn selector_group_matches(
252    selectors: &[&CssPathSelector],
253    html_node: &CascadeInfo,
254    node_data: &NodeData,
255    expected_path_ending: Option<CssPathPseudoSelector>,
256    is_last_content_group: bool,
257) -> bool {
258    use self::CssPathSelector::*;
259
260    for selector in selectors {
261        match selector {
262            Global => {}
263            Type(t) => {
264                if node_data.get_node_type().get_path() != *t {
265                    return false;
266                }
267            }
268            Class(c) => {
269                if !node_data
270                    .get_ids_and_classes()
271                    .iter()
272                    .filter_map(|i| i.as_class())
273                    .any(|class| class == c.as_str())
274                {
275                    return false;
276                }
277            }
278            Id(id) => {
279                if !node_data
280                    .get_ids_and_classes()
281                    .iter()
282                    .filter_map(|i| i.as_id())
283                    .any(|html_id| html_id == id.as_str())
284                {
285                    return false;
286                }
287            }
288            PseudoSelector(p) => {
289                match p {
290                    CssPathPseudoSelector::First => {
291                        // Notice: index_in_parent is 1-indexed
292                        if html_node.index_in_parent != 0 {
293                            return false;
294                        }
295                    }
296                    CssPathPseudoSelector::Last => {
297                        // Notice: index_in_parent is 1-indexed
298                        if !html_node.is_last_child {
299                            return false;
300                        }
301                    }
302                    CssPathPseudoSelector::NthChild(x) => {
303                        use azul_css::CssNthChildPattern;
304                        let index_in_parent = html_node.index_in_parent + 1; // nth-child starts at 1!
305                        match *x {
306                            Number(value) => {
307                                if index_in_parent != value {
308                                    return false;
309                                }
310                            }
311                            Even => {
312                                if index_in_parent % 2 == 0 {
313                                    return false;
314                                }
315                            }
316                            Odd => {
317                                if index_in_parent % 2 == 1 {
318                                    return false;
319                                }
320                            }
321                            Pattern(CssNthChildPattern { repeat, offset }) => {
322                                if index_in_parent >= offset
323                                    && ((index_in_parent - offset) % repeat != 0)
324                                {
325                                    return false;
326                                }
327                            }
328                        }
329                    }
330
331                    // NOTE: for all other selectors such as :hover, :focus and :active,
332                    // we can only apply them if they appear in the last content group,
333                    // i.e. this will match "body > #main:hover", but not "body:hover > #main"
334                    CssPathPseudoSelector::Hover => {
335                        if !is_last_content_group {
336                            return false;
337                        }
338                        if expected_path_ending != Some(CssPathPseudoSelector::Hover) {
339                            return false;
340                        }
341                    }
342                    CssPathPseudoSelector::Active => {
343                        if !is_last_content_group {
344                            return false;
345                        }
346                        if expected_path_ending != Some(CssPathPseudoSelector::Active) {
347                            return false;
348                        }
349                    }
350                    CssPathPseudoSelector::Focus => {
351                        if !is_last_content_group {
352                            return false;
353                        }
354                        if expected_path_ending != Some(CssPathPseudoSelector::Focus) {
355                            return false;
356                        }
357                    }
358                }
359            }
360            DirectChildren | Children => {
361                // panic!("Unreachable: DirectChildren or Children in CSS path!");
362                return false;
363            }
364        }
365    }
366
367    true
368}