ori_core/style/
stylesheet.rs

1use std::{fmt::Display, fs, io, path::Path, str::FromStr};
2
3use crate::{StyleAttribute, StyleAttributes, StyleCache, StyleSelectors, StyleSpecificity};
4
5use super::parse::StyleParseError;
6
7/// An error that can occur when loading a style sheet.
8#[derive(Debug)]
9pub enum StyleLoadError {
10    /// An error occurred while parsing the style sheet.
11    Parse(StyleParseError),
12    /// An error occurred while reading the style sheet.
13    Io(io::Error),
14}
15
16impl From<StyleParseError> for StyleLoadError {
17    fn from(error: StyleParseError) -> Self {
18        Self::Parse(error)
19    }
20}
21
22impl From<io::Error> for StyleLoadError {
23    fn from(error: io::Error) -> Self {
24        Self::Io(error)
25    }
26}
27
28impl Display for StyleLoadError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::Parse(error) => write!(f, "Parse error: {}", error),
32            Self::Io(error) => write!(f, "IO error: {}", error),
33        }
34    }
35}
36
37#[macro_export]
38macro_rules! include_stylesheet {
39    ($($tt:tt)*) => {
40        <$crate::Stylesheet as ::std::str::FromStr>::from_str(include_str!($($tt)*)).unwrap()
41    };
42}
43
44macro_rules! theme {
45    ($name:ident, $folder:literal => $($style:literal),* $(,)?) => {
46        pub const $name: &str = concat!(
47            $(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/style/", $folder, "/", $style))),*
48        );
49    };
50}
51
52theme!(
53    DAY_THEME,
54    "day" =>
55    "default.css",
56    "button.css",
57    "checkbox.css",
58    "knob.css",
59    "scroll.css",
60    "text-input.css",
61    "text.css",
62);
63
64theme!(
65    NIGHT_THEME,
66    "night" =>
67    "default.css",
68    "button.css",
69    "checkbox.css",
70    "knob.css",
71    "scroll.css",
72    "text-input.css",
73    "text.css",
74);
75
76/// A style sheet.
77///
78/// A sheet is a list of [`StyleRule`]s.
79/// Rules are applied in the order they are defined.
80#[derive(Clone, Debug, Default)]
81pub struct Stylesheet {
82    pub rules: Vec<StyleRule>,
83    pub cache: StyleCache,
84}
85
86impl Stylesheet {
87    /// Creates a new style sheet.
88    pub fn new() -> Self {
89        Self {
90            rules: Vec::new(),
91            cache: StyleCache::new(),
92        }
93    }
94
95    pub fn day_theme() -> Self {
96        Self::from_str(DAY_THEME).expect("Failed to parse day theme, this is a bug with ori")
97    }
98
99    pub fn night_theme() -> Self {
100        Self::from_str(NIGHT_THEME).expect("Failed to parse night theme, this is a bug with ori")
101    }
102
103    /// Adds a [`StyleRule`] to the style sheet.
104    pub fn add_rule(&mut self, rule: StyleRule) {
105        self.cache.clear();
106        self.rules.push(rule);
107    }
108
109    /// Extends the style sheet with the given rules.
110    pub fn extend(&mut self, rules: impl IntoIterator<Item = StyleRule>) {
111        self.cache.clear();
112        self.rules.extend(rules);
113    }
114
115    pub fn get_attribute(&self, selectors: &StyleSelectors, name: &str) -> Option<StyleAttribute> {
116        let (attribute, _) = self.get_attribute_specificity(selectors, name)?;
117        Some(attribute.clone())
118    }
119
120    pub fn get_attribute_specificity(
121        &self,
122        selectors: &StyleSelectors,
123        name: &str,
124    ) -> Option<(StyleAttribute, StyleSpecificity)> {
125        if let Some(result) = self.cache.get_attribute(selectors, name) {
126            return Some(result);
127        }
128
129        let mut specificity = StyleSpecificity::default();
130        let mut result = None;
131
132        for rule in self.rules.iter() {
133            if selectors.select(&rule.selector) {
134                let s = rule.selector.specificity();
135
136                if s < specificity {
137                    continue;
138                }
139
140                if let Some(attribute) = rule.get_attribute(name) {
141                    specificity = s;
142                    result = Some((attribute, s));
143                }
144            }
145        }
146
147        let (attribute, specificity) = result?;
148        self.cache.insert(selectors, attribute.clone(), specificity);
149        Some((attribute.clone(), specificity))
150    }
151
152    /// Loads a style sheet from a file.
153    pub fn load(path: impl AsRef<Path>) -> Result<Self, StyleLoadError> {
154        let input = fs::read_to_string(path)?;
155        Ok(Self::from_str(&input)?)
156    }
157}
158
159impl IntoIterator for Stylesheet {
160    type Item = StyleRule;
161    type IntoIter = std::vec::IntoIter<Self::Item>;
162
163    fn into_iter(self) -> Self::IntoIter {
164        self.rules.into_iter()
165    }
166}
167
168/// A [`Stylesheet`] rule.
169///
170/// A rule is a selector and a list of attributes.
171/// The attributes are applied to the elements that match the selector.
172#[derive(Clone, Debug)]
173pub struct StyleRule {
174    pub selector: StyleSelectors,
175    pub attributes: StyleAttributes,
176}
177
178impl StyleRule {
179    /// Creates a new style rule from [`StyleSelectors`].
180    pub fn new(selector: StyleSelectors) -> Self {
181        Self {
182            selector,
183            attributes: StyleAttributes::new(),
184        }
185    }
186
187    /// Adds an [`StyleAttribute`] to the rule.
188    pub fn add_attribute(&mut self, attribute: StyleAttribute) {
189        self.attributes.add(attribute);
190    }
191
192    /// Adds a list of [`StyleAttribute`]s to the rule.
193    pub fn add_attributes(&mut self, attributes: Vec<StyleAttribute>) {
194        self.attributes.extend(attributes);
195    }
196
197    /// Gets the value of an attribute.
198    pub fn get_attribute(&self, name: &str) -> Option<&StyleAttribute> {
199        self.attributes.get(name)
200    }
201}