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}