float_pigment_css/
query.rs

1//! Utilities for style queries.
2
3use alloc::{rc::Rc, string::String, vec::Vec};
4use core::{hash::Hasher, num::NonZeroUsize};
5
6use crate::length_num::LengthNum;
7use crate::property::{
8    NodeProperties, NodePropertiesOrder, Property, PropertyMeta, PropertyValueWithGlobal,
9};
10use crate::sheet::{PseudoElements, Rule};
11use crate::sheet::{RuleWeight, Theme};
12use crate::typing::{Length, LengthType};
13
14/// The status of media query, i.e. screen size, screen type, etc.
15///
16/// This also contains some global environment values, such as `env(...)` values in CSS.
17///
18#[derive(Debug, Clone, PartialEq)]
19pub struct MediaQueryStatus<L: LengthNum> {
20    /// The viewport is a `screen` media type.
21    pub is_screen: bool,
22    /// The viewport width.
23    pub width: L,
24    /// The viewport height.
25    pub height: L,
26    /// The viewport pixel ratio.
27    pub pixel_ratio: f32,
28    /// The global font-size.
29    pub base_font_size: L,
30    /// The current theme, i.e. dark mode or not.
31    pub theme: Theme,
32    /// The `env(...)` expression value.
33    pub env: EnvValues<L>,
34}
35
36/// The values used in CSS `env()` functions
37#[derive(Debug, Clone, PartialEq)]
38#[allow(missing_docs)]
39pub struct EnvValues<L: LengthNum> {
40    pub safe_area_inset_left: L,
41    pub safe_area_inset_top: L,
42    pub safe_area_inset_right: L,
43    pub safe_area_inset_bottom: L,
44}
45
46impl<L: LengthNum> Default for EnvValues<L> {
47    fn default() -> Self {
48        Self {
49            safe_area_inset_left: L::zero(),
50            safe_area_inset_top: L::zero(),
51            safe_area_inset_right: L::zero(),
52            safe_area_inset_bottom: L::zero(),
53        }
54    }
55}
56
57impl<L: LengthNum> Default for MediaQueryStatus<L> {
58    fn default() -> Self {
59        Self::default_screen()
60    }
61}
62
63impl<L: LengthNum> MediaQueryStatus<L> {
64    /// Default screen settings (800x600).
65    pub fn default_screen() -> Self {
66        Self {
67            is_screen: true,
68            width: L::from_i32(800),
69            height: L::from_i32(600),
70            pixel_ratio: 1.,
71            base_font_size: L::from_i32(16),
72            theme: Theme::Light,
73            env: Default::default(),
74        }
75    }
76
77    /// Default screen settings with size specified.
78    pub fn default_screen_with_size(width: L, height: L) -> Self {
79        Self {
80            is_screen: true,
81            width,
82            height,
83            pixel_ratio: 1.,
84            base_font_size: L::from_i32(16),
85            theme: Theme::Light,
86            env: Default::default(),
87        }
88    }
89}
90
91/// The class for a `StyleNode`.
92pub trait StyleNodeClass {
93    /// The name of the class.
94    fn name(&self) -> &str;
95
96    /// The style scope of the class.
97    fn scope(&self) -> Option<NonZeroUsize>;
98}
99
100impl StyleNodeClass for (String, Option<NonZeroUsize>) {
101    fn name(&self) -> &str {
102        &self.0
103    }
104
105    fn scope(&self) -> Option<NonZeroUsize> {
106        self.1
107    }
108}
109
110/// The case-sensitivity for attribute matching.
111pub enum StyleNodeAttributeCaseSensitivity {
112    /// Case-sensitive.
113    CaseSensitive,
114
115    /// Case-insensitive.
116    CaseInsensitive,
117}
118
119impl StyleNodeAttributeCaseSensitivity {
120    /// Matches two strings with this case-sensitivity.
121    pub fn eq(&self, a: &str, b: &str) -> bool {
122        match self {
123            Self::CaseSensitive => a == b,
124            Self::CaseInsensitive => a.eq_ignore_ascii_case(b),
125        }
126    }
127
128    /// Check if `a` starts with `b` in this case-sensitivity.
129    pub fn starts_with(&self, a: &str, b: &str) -> bool {
130        // FIXME: reduce memory allocation
131        match self {
132            Self::CaseSensitive => a.starts_with(b),
133            Self::CaseInsensitive => a.to_ascii_lowercase().starts_with(&b.to_ascii_lowercase()),
134        }
135    }
136
137    /// Check if `a` ends with `b` in this case-sensitivity.
138    pub fn ends_with(&self, a: &str, b: &str) -> bool {
139        // FIXME: reduce memory allocation
140        match self {
141            Self::CaseSensitive => a.ends_with(b),
142            Self::CaseInsensitive => a.to_ascii_lowercase().ends_with(&b.to_ascii_lowercase()),
143        }
144    }
145
146    /// Check if `a` contains `b` in this case-sensitivity.
147    pub fn contains(&self, a: &str, b: &str) -> bool {
148        // FIXME: reduce memory allocation
149        match self {
150            Self::CaseSensitive => a.contains(b),
151            Self::CaseInsensitive => a.to_ascii_lowercase().contains(&b.to_ascii_lowercase()),
152        }
153    }
154}
155
156/// A node descriptor for a style query.
157pub trait StyleNode {
158    /// The type for a class.
159    type Class: StyleNodeClass;
160
161    /// The type for classes iteration.
162    type ClassIter<'a>: Iterator<Item = &'a Self::Class>
163    where
164        Self: 'a;
165
166    /// The style scope of the node itself.
167    fn style_scope(&self) -> Option<NonZeroUsize>;
168
169    /// The extra style scope of the node.
170    fn extra_style_scope(&self) -> Option<NonZeroUsize>;
171
172    /// The extra style scope for the `:host` selector.
173    fn host_style_scope(&self) -> Option<NonZeroUsize>;
174
175    /// The tag name of the node.
176    fn tag_name(&self) -> &str;
177
178    /// The id of the node.
179    fn id(&self) -> Option<&str>;
180
181    /// The classes of the node.
182    fn classes(&self) -> Self::ClassIter<'_>;
183
184    /// Get an attribute of the node.
185    fn attribute(&self, name: &str) -> Option<(&str, StyleNodeAttributeCaseSensitivity)>;
186
187    /// The pseudo element to query.
188    fn pseudo_element(&self) -> Option<PseudoElements>;
189
190    /// Check if the node has a specified scope.
191    fn contain_scope(&self, scope: Option<NonZeroUsize>) -> bool {
192        scope.is_none()
193            || self.style_scope() == scope
194            || self.extra_style_scope() == scope
195            || self.host_style_scope() == scope
196    }
197}
198
199/// Represents node information, used for matching rules.
200#[derive(Debug)]
201pub struct StyleQuery<'a> {
202    pub(super) style_scope: Option<NonZeroUsize>,
203    pub(super) extra_style_scope: Option<NonZeroUsize>,
204    pub(super) host_style_scope: Option<NonZeroUsize>,
205    pub(super) tag_name: &'a str,
206    pub(super) id: &'a str,
207    pub(super) classes: &'a [(String, Option<NonZeroUsize>)],
208}
209
210impl Clone for StyleQuery<'_> {
211    fn clone(&self) -> Self {
212        Self {
213            style_scope: self.style_scope,
214            extra_style_scope: self.extra_style_scope,
215            host_style_scope: self.host_style_scope,
216            tag_name: self.tag_name,
217            id: self.id,
218            classes: self.classes,
219        }
220    }
221}
222
223impl<'a> StyleQuery<'a> {
224    /// Constructs a style query from tag name, id, classes, etc.
225    pub fn single(
226        style_scope: Option<NonZeroUsize>,
227        extra_style_scope: Option<NonZeroUsize>,
228        host_style_scope: Option<NonZeroUsize>,
229        tag_name: &'a str,
230        id: &'a str,
231        classes: &'a [(String, Option<NonZeroUsize>)],
232    ) -> Self {
233        Self {
234            style_scope,
235            extra_style_scope,
236            host_style_scope,
237            tag_name,
238            id,
239            classes,
240        }
241    }
242}
243
244impl<'a> StyleNode for StyleQuery<'a> {
245    type Class = (String, Option<NonZeroUsize>);
246    type ClassIter<'c>
247        = core::slice::Iter<'c, Self::Class>
248    where
249        'a: 'c;
250
251    fn style_scope(&self) -> Option<NonZeroUsize> {
252        self.style_scope
253    }
254
255    fn extra_style_scope(&self) -> Option<NonZeroUsize> {
256        self.extra_style_scope
257    }
258
259    fn host_style_scope(&self) -> Option<NonZeroUsize> {
260        self.host_style_scope
261    }
262
263    fn tag_name(&self) -> &str {
264        self.tag_name
265    }
266
267    fn id(&self) -> Option<&str> {
268        Some(self.id)
269    }
270
271    fn classes(&self) -> Self::ClassIter<'_> {
272        self.classes.iter()
273    }
274
275    fn attribute(&self, _name: &str) -> Option<(&str, StyleNodeAttributeCaseSensitivity)> {
276        None
277    }
278
279    fn pseudo_element(&self) -> Option<PseudoElements> {
280        None
281    }
282}
283
284impl<'b, 'a: 'b> StyleNode for &'b StyleQuery<'a> {
285    type Class = (String, Option<NonZeroUsize>);
286    type ClassIter<'c>
287        = core::slice::Iter<'c, Self::Class>
288    where
289        'b: 'c;
290
291    fn style_scope(&self) -> Option<NonZeroUsize> {
292        self.style_scope
293    }
294
295    fn extra_style_scope(&self) -> Option<NonZeroUsize> {
296        self.extra_style_scope
297    }
298
299    fn host_style_scope(&self) -> Option<NonZeroUsize> {
300        self.host_style_scope
301    }
302
303    fn tag_name(&self) -> &str {
304        self.tag_name
305    }
306
307    fn id(&self) -> Option<&str> {
308        Some(self.id)
309    }
310
311    fn classes(&self) -> Self::ClassIter<'_> {
312        self.classes.iter()
313    }
314
315    fn attribute(&self, _name: &str) -> Option<(&str, StyleNodeAttributeCaseSensitivity)> {
316        None
317    }
318
319    fn pseudo_element(&self) -> Option<PseudoElements> {
320        None
321    }
322}
323
324/// Represents a matched rule (borrowed form).
325#[derive(Debug, Clone)]
326pub struct MatchedRuleRef<'a> {
327    /// The rule body.
328    pub rule: &'a Rc<Rule>,
329    /// The weight of the rule.
330    pub weight: RuleWeight,
331}
332
333/// Represents a matched rule.
334#[derive(Debug, Clone)]
335pub struct MatchedRule {
336    /// The rule body.
337    pub rule: Rc<Rule>,
338    /// The weight of the rule.
339    pub weight: RuleWeight,
340    /// The style scope of the rule.
341    pub style_scope: Option<NonZeroUsize>,
342}
343
344impl PartialEq for MatchedRule {
345    fn eq(&self, other: &Self) -> bool {
346        self.weight.normal() == other.weight.normal()
347    }
348}
349
350impl PartialOrd for MatchedRule {
351    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
352        Some(self.weight.normal().cmp(&other.weight.normal()))
353    }
354}
355
356impl Ord for MatchedRule {
357    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
358        self.weight.normal().cmp(&other.weight.normal())
359    }
360}
361
362impl Eq for MatchedRule {}
363
364/// Represents the matched rule list.
365#[derive(Debug, Clone)]
366pub struct MatchedRuleList {
367    /// The matched rules.
368    pub rules: Vec<MatchedRule>,
369}
370
371impl MatchedRuleList {
372    /// Create an empty list.
373    pub fn new_empty() -> Self {
374        Self {
375            rules: Vec::with_capacity(0),
376        }
377    }
378
379    /// Calculate the font size.
380    ///
381    /// Some methods like `merge_node_properties` requires it to resolve `em` values.
382    pub fn get_current_font_size<L: LengthNum>(
383        &self,
384        parent_font_size: f32,
385        parent_node_properties: Option<&NodeProperties>,
386        extra_styles: &[PropertyMeta],
387        media_query_status: &MediaQueryStatus<L>,
388    ) -> f32 {
389        // find font-size properties
390        let mut font_size_p = None;
391        let mut font_size_w: u64 = 0;
392        fn handle_property_meta<'a>(
393            font_size_p: &mut Option<&'a LengthType>,
394            font_size_w: &mut u64,
395            pm: &'a PropertyMeta,
396            rw: RuleWeight,
397        ) {
398            match pm {
399                PropertyMeta::Normal { property: p } => {
400                    if let Property::FontSize(x) = p {
401                        let w = rw.normal();
402                        if w >= *font_size_w {
403                            *font_size_w = w;
404                            *font_size_p = Some(x);
405                        }
406                    }
407                }
408                PropertyMeta::Important { property: p } => {
409                    if let Property::FontSize(x) = p {
410                        let w = rw.important();
411                        if w >= *font_size_w {
412                            *font_size_w = w;
413                            *font_size_p = Some(x);
414                        }
415                    }
416                }
417                PropertyMeta::DebugGroup {
418                    properties,
419                    important,
420                    disabled,
421                    ..
422                } => {
423                    if !disabled {
424                        let w = if *important {
425                            rw.important()
426                        } else {
427                            rw.normal()
428                        };
429                        if w >= *font_size_w {
430                            for p in &**properties {
431                                if let Property::FontSize(x) = p {
432                                    *font_size_w = w;
433                                    *font_size_p = Some(x);
434                                }
435                            }
436                        }
437                    }
438                }
439            }
440        }
441        for pm in extra_styles.iter() {
442            handle_property_meta(&mut font_size_p, &mut font_size_w, pm, RuleWeight::inline());
443        }
444        for matched_rule in self.rules.iter() {
445            let rw = matched_rule.weight;
446            if !matched_rule.rule.has_font_size {
447                continue;
448            };
449            for pm in matched_rule.rule.properties.iter() {
450                handle_property_meta(&mut font_size_p, &mut font_size_w, pm, rw);
451            }
452        }
453
454        // get current font-size
455        let default_font_size = media_query_status.base_font_size.to_f32();
456        let parent_font_size_p = parent_node_properties.map(|x| x.font_size_ref());
457        let parent_font_size = parent_font_size.to_f32();
458        let current_font_size = if let Some(p) = font_size_p {
459            p.to_inner(parent_font_size_p, Length::Px(default_font_size), true)
460                .and_then(|x| x.resolve_to_f32(media_query_status, parent_font_size, true))
461                .unwrap_or(parent_font_size)
462        } else {
463            parent_font_size
464        };
465
466        current_font_size
467    }
468
469    /// Merge the rule list into specified `NodeProperties` .
470    pub fn merge_node_properties(
471        &self,
472        node_properties: &mut NodeProperties,
473        parent_node_properties: Option<&NodeProperties>,
474        current_font_size: f32,
475        extra_styles: &[PropertyMeta],
476    ) {
477        let mut order = NodePropertiesOrder::new();
478        let mut merge_property_meta = |pm: &PropertyMeta, rw: RuleWeight| match pm {
479            PropertyMeta::Normal { property: p } => {
480                if order.compare_property(p, rw.normal()) {
481                    node_properties.merge_property(p, parent_node_properties, current_font_size)
482                }
483            }
484            PropertyMeta::Important { property: p } => {
485                if order.compare_property(p, rw.important()) {
486                    node_properties.merge_property(p, parent_node_properties, current_font_size)
487                }
488            }
489            PropertyMeta::DebugGroup {
490                properties,
491                important,
492                disabled,
493                ..
494            } => {
495                if !disabled {
496                    let w = if *important {
497                        rw.important()
498                    } else {
499                        rw.normal()
500                    };
501                    for p in &**properties {
502                        if order.compare_property(p, w) {
503                            node_properties.merge_property(
504                                p,
505                                parent_node_properties,
506                                current_font_size,
507                            )
508                        }
509                    }
510                }
511            }
512        };
513        for pm in extra_styles.iter() {
514            merge_property_meta(pm, RuleWeight::inline());
515        }
516        for matched_rule in self.rules.iter() {
517            for pm in matched_rule.rule.properties.iter() {
518                merge_property_meta(pm, matched_rule.weight);
519            }
520        }
521    }
522
523    /// Iterate properties with weights.
524    pub fn for_each_property(&self, mut f: impl FnMut(&Property, u64)) {
525        for matched_rule in self.rules.iter() {
526            let weight = matched_rule.weight;
527            for pm in matched_rule.rule.properties.iter() {
528                if pm.is_disabled() {
529                    continue;
530                }
531                let w = if pm.is_important() {
532                    weight.important()
533                } else {
534                    weight.normal()
535                };
536                for p in pm.iter() {
537                    f(p, w);
538                }
539            }
540        }
541    }
542
543    /// Find the style scope of the rule which contains the applied `animation-name` property.
544    ///
545    /// This call is designed for the search of keyframes with style scopes.
546    /// Returns `None` if there is no `animation-name` property or it is inside inline styles.
547    pub fn animation_name_style_scope(&self) -> Option<NonZeroUsize> {
548        let mut w = u64::MIN;
549        let mut ret = None;
550        let mut check_property_meta = |pm: &PropertyMeta, rw: RuleWeight, scope| {
551            for p in pm.iter() {
552                if let Property::AnimationName(..) = p {
553                    let self_w = if pm.is_important() {
554                        rw.important()
555                    } else {
556                        rw.normal()
557                    };
558                    if self_w >= w {
559                        w = self_w;
560                        ret = scope;
561                    }
562                }
563            }
564        };
565        for matched_rule in self.rules.iter() {
566            for pm in matched_rule.rule.properties.iter() {
567                check_property_meta(pm, matched_rule.weight, matched_rule.style_scope);
568            }
569        }
570        ret
571    }
572
573    /// Get a fast hash value of the list.
574    ///
575    /// The hash value can be used to identify the rule list is the same as the other one or not.
576    pub fn fast_hash_value(&self) -> u64 {
577        let mut hasher = ahash::AHasher::default();
578        for matched_rule in self.rules.iter() {
579            let rule: &Rule = &matched_rule.rule;
580            hasher.write_usize(rule as *const Rule as usize);
581            hasher.write_u64(matched_rule.weight.normal());
582        }
583        hasher.finish()
584    }
585}