use std::any::Any;
use bevy::{
ecs::query::{QueryData, QueryFilter, QueryItem},
log::{error, trace},
prelude::{
AssetId, AssetServer, Assets, Color, Commands, Deref, DerefMut, Entity, Local, Query, Res,
Resource,
},
ui::{UiRect, Val},
utils::HashMap,
};
use cssparser::Token;
use smallvec::SmallVec;
use crate::{selector::Selector, EcssError, SelectorElement, StyleSheetAsset};
mod colors;
pub(crate) mod impls;
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum PropertyToken {
Percentage(f32),
Dimension(f32),
Number(f32),
Identifier(String),
Hash(String),
String(String),
}
#[derive(Debug, Default, Clone, Deref)]
pub struct PropertyValues(pub(crate) SmallVec<[PropertyToken; 8]>);
impl PropertyValues {
pub fn string(&self) -> Option<String> {
self.0.iter().find_map(|token| match token {
PropertyToken::String(id) => {
if id.is_empty() {
None
} else {
Some(id.clone())
}
}
_ => None,
})
}
pub fn color(&self) -> Option<Color> {
if self.0.len() == 1 {
match &self.0[0] {
PropertyToken::Identifier(name) => colors::parse_named_color(name.as_str()),
PropertyToken::Hash(hash) => colors::parse_hex_color(hash.as_str()),
_ => None,
}
} else {
None
}
}
pub fn identifier(&self) -> Option<&str> {
self.0.iter().find_map(|token| match token {
PropertyToken::Identifier(id) => {
if id.is_empty() {
None
} else {
Some(id.as_str())
}
}
_ => None,
})
}
pub fn val(&self) -> Option<Val> {
self.0.iter().find_map(|token| match token {
PropertyToken::Percentage(val) => Some(Val::Percent(*val)),
PropertyToken::Dimension(val) => Some(Val::Px(*val)),
PropertyToken::Identifier(val) if val == "auto" => Some(Val::Auto),
_ => None,
})
}
pub fn f32(&self) -> Option<f32> {
self.0.iter().find_map(|token| match token {
PropertyToken::Percentage(val)
| PropertyToken::Dimension(val)
| PropertyToken::Number(val) => Some(*val),
_ => None,
})
}
pub fn option_f32(&self) -> Option<Option<f32>> {
self.0.iter().find_map(|token| match token {
PropertyToken::Percentage(val)
| PropertyToken::Dimension(val)
| PropertyToken::Number(val) => Some(Some(*val)),
PropertyToken::Identifier(ident) => match ident.as_str() {
"none" => Some(None),
_ => None,
},
_ => None,
})
}
pub fn rect(&self) -> Option<UiRect> {
if self.0.len() == 1 {
self.val().map(UiRect::all)
} else {
self.0
.iter()
.fold((None, 0), |(rect, idx), token| {
let val = match token {
PropertyToken::Percentage(val) => Val::Percent(*val),
PropertyToken::Dimension(val) => Val::Px(*val),
PropertyToken::Identifier(val) if val == "auto" => Val::Auto,
_ => return (rect, idx),
};
let mut rect: UiRect = rect.unwrap_or_default();
match idx {
0 => rect.top = val,
1 => rect.right = val,
2 => rect.bottom = val,
3 => rect.left = val,
_ => (),
}
(Some(rect), idx + 1)
})
.0
}
}
}
impl<'i> TryFrom<Token<'i>> for PropertyToken {
type Error = ();
fn try_from(token: Token<'i>) -> Result<Self, Self::Error> {
match token {
Token::Ident(val) => Ok(Self::Identifier(val.to_string())),
Token::Hash(val) => Ok(Self::Hash(val.to_string())),
Token::IDHash(val) => Ok(Self::Hash(val.to_string())),
Token::QuotedString(val) => Ok(Self::String(val.to_string())),
Token::Number { value, .. } => Ok(Self::Number(value)),
Token::Percentage { unit_value, .. } => Ok(Self::Percentage(unit_value * 100.0)),
Token::Dimension { value, .. } => Ok(Self::Dimension(value)),
_ => Err(()),
}
}
}
#[derive(Default, Debug, Clone)]
pub enum CacheState<T> {
#[default]
None,
Ok(T),
Error,
}
#[derive(Debug, Default, Deref, DerefMut)]
pub struct CachedProperties<T>(HashMap<Selector, CacheState<T>>);
#[derive(Debug, Default, Deref, DerefMut)]
pub struct PropertyMeta<T: Property>(HashMap<u64, CachedProperties<T::Cache>>);
impl<T: Property> PropertyMeta<T> {
fn get_or_parse(
&mut self,
rules: &StyleSheetAsset,
selector: &Selector,
) -> &CacheState<T::Cache> {
let cached_properties = self.entry(rules.hash()).or_default();
if cached_properties.contains_key(selector) {
cached_properties.get(selector).unwrap()
} else {
let new_cache = rules
.get_properties(selector, T::name())
.map(|values| match T::parse(values) {
Ok(cache) => CacheState::Ok(cache),
Err(err) => {
error!("Failed to parse property {}. Error: {}", T::name(), err);
CacheState::Error
}
})
.unwrap_or(CacheState::None);
cached_properties.insert(selector.clone(), new_cache);
cached_properties.get(selector).unwrap()
}
}
}
#[derive(Debug, Clone, Default, Deref, DerefMut)]
pub struct TrackedEntities(HashMap<SelectorElement, SmallVec<[Entity; 8]>>);
#[derive(Debug, Clone, Default, Deref, DerefMut)]
pub struct SelectedEntities(SmallVec<[(Selector, SmallVec<[Entity; 8]>); 8]>);
#[derive(Debug, Clone, Default, Resource, Deref, DerefMut)]
pub struct StyleSheetState(Vec<(AssetId<StyleSheetAsset>, TrackedEntities, SelectedEntities)>);
impl StyleSheetState {
pub(crate) fn has_any_selected_entities(&self) -> bool {
self.iter().any(|(_, _, v)| !v.is_empty())
}
pub(crate) fn clear_selected_entities(&mut self) {
self.iter_mut().for_each(|(_, _, v)| v.clear());
}
}
pub trait Property: Default + Sized + Send + Sync + 'static {
type Cache: Default + Any + Send + Sync;
type Components: QueryData;
type Filters: QueryFilter;
fn name() -> &'static str;
fn parse(values: &PropertyValues) -> Result<Self::Cache, EcssError>;
fn apply(
cache: &Self::Cache,
components: QueryItem<Self::Components>,
asset_server: &AssetServer,
commands: &mut Commands,
);
fn apply_system(
mut local: Local<PropertyMeta<Self>>,
assets: Res<Assets<StyleSheetAsset>>,
apply_sheets: Res<StyleSheetState>,
mut q_nodes: Query<Self::Components, Self::Filters>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
for (asset_id, _, selected) in apply_sheets.iter() {
if let Some(rules) = assets.get(*asset_id) {
for (selector, entities) in selected.iter() {
if let CacheState::Ok(cached) = local.get_or_parse(rules, selector) {
trace!(
r#"Applying property "{}" from sheet "{}" ({})"#,
Self::name(),
rules.path(),
selector
);
for entity in entities {
if let Ok(components) = q_nodes.get_mut(*entity) {
Self::apply(cached, components, &asset_server, &mut commands);
}
}
}
}
}
}
}
}