bevy_ecss/property/
mod.rs

1use std::any::Any;
2
3use bevy::{
4    ecs::query::{QueryData, QueryFilter, QueryItem},
5    log::{error, trace},
6    prelude::{
7        AssetId, AssetServer, Assets, Color, Commands, Deref, DerefMut, Entity, Local, Query, Res,
8        Resource,
9    },
10    ui::{UiRect, Val},
11    utils::HashMap,
12};
13
14use cssparser::Token;
15use smallvec::SmallVec;
16
17use crate::{selector::Selector, EcssError, SelectorElement, StyleSheetAsset};
18
19mod colors;
20pub(crate) mod impls;
21
22/// A property value token which was parsed from a CSS rule.
23#[derive(Debug, Clone, PartialEq, PartialOrd)]
24pub enum PropertyToken {
25    /// A value which was parsed percent value, like `100%` or `73.23%`.
26    Percentage(f32),
27    /// A value which was parsed dimension value, like `10px` or `35em.
28    ///
29    /// Currently there is no distinction between [`length-values`](https://developer.mozilla.org/en-US/docs/Web/CSS/length).
30    Dimension(f32),
31    /// A numeric float value, like `31.1` or `43`.
32    Number(f32),
33    /// A plain identifier, like `none` or `center`.
34    Identifier(String),
35    /// A identifier prefixed by a hash, like `#001122`.
36    Hash(String),
37    /// A quoted string, like `"some value"`.
38    String(String),
39}
40
41/// A list of [`PropertyToken`] which was parsed from a single property.
42#[derive(Debug, Default, Clone, Deref)]
43pub struct PropertyValues(pub(crate) SmallVec<[PropertyToken; 8]>);
44
45impl PropertyValues {
46    /// Tries to parses the current values as a single [`String`].
47    pub fn string(&self) -> Option<String> {
48        self.0.iter().find_map(|token| match token {
49            PropertyToken::String(id) => {
50                if id.is_empty() {
51                    None
52                } else {
53                    Some(id.clone())
54                }
55            }
56            _ => None,
57        })
58    }
59
60    /// Tries to parses the current values as a single [`Color`].
61    ///
62    /// Currently only [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color)
63    /// and [hex-colors](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) are supported.
64    pub fn color(&self) -> Option<Color> {
65        if self.0.len() == 1 {
66            match &self.0[0] {
67                PropertyToken::Identifier(name) => colors::parse_named_color(name.as_str()),
68                PropertyToken::Hash(hash) => colors::parse_hex_color(hash.as_str()),
69                _ => None,
70            }
71        } else {
72            // TODO: Implement color function like rgba(255, 255, 255, 255)
73            // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
74            None
75        }
76    }
77
78    /// Tries to parses the current values as a single identifier.
79    pub fn identifier(&self) -> Option<&str> {
80        self.0.iter().find_map(|token| match token {
81            PropertyToken::Identifier(id) => {
82                if id.is_empty() {
83                    None
84                } else {
85                    Some(id.as_str())
86                }
87            }
88            _ => None,
89        })
90    }
91
92    /// Tries to parses the current values as a single [`Val`].
93    ///
94    /// Only [`Percentage`](PropertyToken::Percentage) and [`Dimension`](PropertyToken::Dimension`) are considered valid values,
95    /// where former is converted to [`Val::Percent`] and latter is converted to [`Val::Px`].
96    pub fn val(&self) -> Option<Val> {
97        self.0.iter().find_map(|token| match token {
98            PropertyToken::Percentage(val) => Some(Val::Percent(*val)),
99            PropertyToken::Dimension(val) => Some(Val::Px(*val)),
100            PropertyToken::Identifier(val) if val == "auto" => Some(Val::Auto),
101            _ => None,
102        })
103    }
104
105    /// Tries to parses the current values as a single [`f32`].
106    ///
107    /// Only [`Percentage`](PropertyToken::Percentage), [`Dimension`](PropertyToken::Dimension`) and [`Number`](PropertyToken::Number`)
108    /// are considered valid values.
109    pub fn f32(&self) -> Option<f32> {
110        self.0.iter().find_map(|token| match token {
111            PropertyToken::Percentage(val)
112            | PropertyToken::Dimension(val)
113            | PropertyToken::Number(val) => Some(*val),
114            _ => None,
115        })
116    }
117
118    /// Tries to parses the current values as a single [`Option<f32>`].
119    ///
120    /// This function is useful for properties where either a numeric value or a `none` value is expected.
121    ///
122    /// If a [`Option::None`] is returned, it means some invalid value was found.
123    ///
124    /// If there is a [`Percentage`](PropertyToken::Percentage), [`Dimension`](PropertyToken::Dimension`) or [`Number`](PropertyToken::Number`) token,
125    /// a [`Option::Some`] with parsed [`Option<f32>`] is returned.
126    /// If there is a identifier with a `none` value, then [`Option::Some`] with [`None`] is returned.
127    pub fn option_f32(&self) -> Option<Option<f32>> {
128        self.0.iter().find_map(|token| match token {
129            PropertyToken::Percentage(val)
130            | PropertyToken::Dimension(val)
131            | PropertyToken::Number(val) => Some(Some(*val)),
132            PropertyToken::Identifier(ident) => match ident.as_str() {
133                "none" => Some(None),
134                _ => None,
135            },
136            _ => None,
137        })
138    }
139
140    /// Tries to parses the current values as a single [`Option<UiRect<Val>>`].
141    ///
142    /// Optional values are handled by this function, so if only one value is present it is used as `top`, `right`, `bottom` and `left`,
143    /// otherwise values are applied in the following order: `top`, `right`, `bottom` and `left`.
144    ///
145    /// Note that it is not possible to create a [`UiRect`] with only `top` value, since it'll be understood to replicated it on all fields.
146    pub fn rect(&self) -> Option<UiRect> {
147        if self.0.len() == 1 {
148            self.val().map(UiRect::all)
149        } else {
150            self.0
151                .iter()
152                .fold((None, 0), |(rect, idx), token| {
153                    let val = match token {
154                        PropertyToken::Percentage(val) => Val::Percent(*val),
155                        PropertyToken::Dimension(val) => Val::Px(*val),
156                        PropertyToken::Identifier(val) if val == "auto" => Val::Auto,
157                        _ => return (rect, idx),
158                    };
159                    let mut rect: UiRect = rect.unwrap_or_default();
160
161                    match idx {
162                        0 => rect.top = val,
163                        1 => rect.right = val,
164                        2 => rect.bottom = val,
165                        3 => rect.left = val,
166                        _ => (),
167                    }
168                    (Some(rect), idx + 1)
169                })
170                .0
171        }
172    }
173}
174
175impl<'i> TryFrom<Token<'i>> for PropertyToken {
176    type Error = ();
177
178    fn try_from(token: Token<'i>) -> Result<Self, Self::Error> {
179        match token {
180            Token::Ident(val) => Ok(Self::Identifier(val.to_string())),
181            Token::Hash(val) => Ok(Self::Hash(val.to_string())),
182            Token::IDHash(val) => Ok(Self::Hash(val.to_string())),
183            Token::QuotedString(val) => Ok(Self::String(val.to_string())),
184            Token::Number { value, .. } => Ok(Self::Number(value)),
185            Token::Percentage { unit_value, .. } => Ok(Self::Percentage(unit_value * 100.0)),
186            Token::Dimension { value, .. } => Ok(Self::Dimension(value)),
187            _ => Err(()),
188        }
189    }
190}
191
192/// Internal cache state. Used by [`CachedProperties`] to avoid parsing properties of the same rule on same sheet.
193#[derive(Default, Debug, Clone)]
194pub enum CacheState<T> {
195    /// No parse was performed yet
196    #[default]
197    None,
198    /// Parse was performed and yielded a valid value.
199    Ok(T),
200    /// Parse was performed but returned an error.
201    Error,
202}
203
204/// Internal cache map. Used by [`PropertyMeta`] to keep track of which properties was already parsed.
205#[derive(Debug, Default, Deref, DerefMut)]
206pub struct CachedProperties<T>(HashMap<Selector, CacheState<T>>);
207
208/// Internal property cache map. Used by [`Property::apply_system`] to keep track of which properties was already parsed.
209#[derive(Debug, Default, Deref, DerefMut)]
210pub struct PropertyMeta<T: Property>(HashMap<u64, CachedProperties<T::Cache>>);
211
212impl<T: Property> PropertyMeta<T> {
213    /// Gets a cached property value or try to parse.
214    ///
215    /// If there are some error while parsing, a [`CacheState::Error`] is stored to avoid trying to parse again on next try.
216    fn get_or_parse(
217        &mut self,
218        rules: &StyleSheetAsset,
219        selector: &Selector,
220    ) -> &CacheState<T::Cache> {
221        let cached_properties = self.entry(rules.hash()).or_default();
222
223        // Avoid using HashMap::entry since it requires ownership of key
224        if cached_properties.contains_key(selector) {
225            cached_properties.get(selector).unwrap()
226        } else {
227            let new_cache = rules
228                .get_properties(selector, T::name())
229                .map(|values| match T::parse(values) {
230                    Ok(cache) => CacheState::Ok(cache),
231                    Err(err) => {
232                        error!("Failed to parse property {}. Error: {}", T::name(), err);
233                        // TODO: Clear cache state when the asset is reloaded, since values may be changed.
234                        CacheState::Error
235                    }
236                })
237                .unwrap_or(CacheState::None);
238
239            cached_properties.insert(selector.clone(), new_cache);
240            cached_properties.get(selector).unwrap()
241        }
242    }
243}
244
245#[derive(Debug, Clone, Default, Deref, DerefMut)]
246pub struct TrackedEntities(HashMap<SelectorElement, SmallVec<[Entity; 8]>>);
247
248/// Maps which entities was selected by a [`Selector`]
249#[derive(Debug, Clone, Default, Deref, DerefMut)]
250pub struct SelectedEntities(SmallVec<[(Selector, SmallVec<[Entity; 8]>); 8]>);
251
252/// Maps sheets for each [`StyleSheetAsset`].
253#[derive(Debug, Clone, Default, Resource, Deref, DerefMut)]
254pub struct StyleSheetState(Vec<(AssetId<StyleSheetAsset>, TrackedEntities, SelectedEntities)>);
255
256impl StyleSheetState {
257    pub(crate) fn has_any_selected_entities(&self) -> bool {
258        self.iter().any(|(_, _, v)| !v.is_empty())
259    }
260
261    pub(crate) fn clear_selected_entities(&mut self) {
262        self.iter_mut().for_each(|(_, _, v)| v.clear());
263    }
264}
265
266/// Determines how a property should interact and modify the [ecs world](`bevy::prelude::World`).
267///
268/// Each implementation of this trait should be registered with [`RegisterProperty`](crate::RegisterProperty) trait, where
269/// will be converted into a `system` and run whenever a matched, specified by [`name()`](`Property::name()`) property is found.
270///
271/// These are the associated types that must by specified by implementors:
272/// - [`Cache`](Property::Cache) is a cached value to be applied by this trait.
273/// On the first time the `system` runs it'll call [`parse`](`Property::parse`) and cache the value.
274/// Subsequential runs will only fetch the cached value.
275/// - [`Components`](Property::Components) is which components will be send to [`apply`](`Property::apply`) function whenever a
276/// valid cache exists and a matching property was found on any sheet rule. Check [`QueryData`] for more.
277/// - [`Filters`](Property::Filters) is used to filter which entities will be applied the property modification.
278/// Entities are first filtered by [`selectors`](`Selector`), but it can be useful to also ensure some behavior for safety reasons,
279/// like only inserting [`JustifyText`](bevy::prelude::JustifyText) if the entity also has a [`Text`](bevy::prelude::Text) component.
280///  Check [`WorldQuery`] for more.
281///
282/// These are tree functions required to be implemented:
283/// - [`name`](Property::name) indicates which property name should matched for.
284/// - [`parse`](Property::parse) parses the [`PropertyValues`] into the [`Cache`](Property::Cache) value to be reused across multiple entities.
285/// - [`apply`](Property::apply) applies on the given [`Components`](Property::Components) the [`Cache`](Property::Cache) value.
286/// Additionally, an [`AssetServer`] and [`Commands`] parameters are provided for more complex use cases.
287///
288/// Also, there one function which have default implementations:
289/// - [`apply_system`](Property::apply_system) is a [`system`](https://docs.rs/bevy_ecs/latest/bevy_ecs/system/index.html) which interacts with
290/// [ecs world](`bevy::prelude::World`) and call the [`apply`](Property::apply) function on every matched entity.
291pub trait Property: Default + Sized + Send + Sync + 'static {
292    /// The cached value type to be applied by property.
293    type Cache: Default + Any + Send + Sync;
294    /// Which components should be queried when applying the modification. Check [`QueryData`] for more.
295    type Components: QueryData;
296    /// Filters conditions to be applied when querying entities by this property. Check [`QueryFilter`] for more.
297    type Filters: QueryFilter;
298
299    /// Indicates which property name should matched for. Must match the same property name as on `css` file.
300    ///
301    /// For compliance, use always `lower-case` and `kebab-case` names.
302    fn name() -> &'static str;
303
304    /// Parses the [`PropertyValues`] into the [`Cache`](Property::Cache) value to be reused across multiple entities.
305    ///
306    /// This function is called only once, on the first time a matching property is found while applying style rule.
307    /// If an error is returned, it is also cached so no more attempt are made.
308    fn parse(values: &PropertyValues) -> Result<Self::Cache, EcssError>;
309
310    /// Applies on the given [`Components`](Property::Components) the [`Cache`](Property::Cache) value.
311    /// Additionally, an [`AssetServer`] and [`Commands`] parameters are provided for more complex use cases.
312    ///
313    /// If mutability is desired while applying the changes, declare [`Components`](Property::Components) as mutable.
314    fn apply(
315        cache: &Self::Cache,
316        components: QueryItem<Self::Components>,
317        asset_server: &AssetServer,
318        commands: &mut Commands,
319    );
320
321    /// The [`system`](https://docs.rs/bevy_ecs/latest/bevy_ecs/system/index.html) which interacts with
322    /// [ecs world](`bevy::prelude::World`) and call [`apply`](Property::apply) function on every matched entity.
323    ///
324    /// The default implementation will cover most use cases, by just implementing [`apply`](Property::apply)
325    fn apply_system(
326        mut local: Local<PropertyMeta<Self>>,
327        assets: Res<Assets<StyleSheetAsset>>,
328        apply_sheets: Res<StyleSheetState>,
329        mut q_nodes: Query<Self::Components, Self::Filters>,
330        asset_server: Res<AssetServer>,
331        mut commands: Commands,
332    ) {
333        for (asset_id, _, selected) in apply_sheets.iter() {
334            if let Some(rules) = assets.get(*asset_id) {
335                for (selector, entities) in selected.iter() {
336                    if let CacheState::Ok(cached) = local.get_or_parse(rules, selector) {
337                        trace!(
338                            r#"Applying property "{}" from sheet "{}" ({})"#,
339                            Self::name(),
340                            rules.path(),
341                            selector
342                        );
343                        for entity in entities {
344                            if let Ok(components) = q_nodes.get_mut(*entity) {
345                                Self::apply(cached, components, &asset_server, &mut commands);
346                            }
347                        }
348                    }
349                }
350            }
351        }
352    }
353}