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