use std::collections::HashMap;
use bevy::prelude::*;
use crate::dp::DpValue;
use crate::render::{NoesisRenderState, NoesisSet};
#[derive(Debug, Clone, PartialEq)]
pub struct PropertyTrigger {
pub property: String,
pub value: DpValue,
pub setters: Vec<(String, DpValue)>,
}
impl PropertyTrigger {
#[must_use]
pub fn new(property: impl Into<String>, value: DpValue) -> Self {
Self {
property: property.into(),
value,
setters: Vec::new(),
}
}
#[must_use]
pub fn setter(mut self, property: impl Into<String>, value: DpValue) -> Self {
self.setters.push((property.into(), value));
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DataTriggerSpec {
pub binding_path: String,
pub relative_source_self: bool,
pub value: DpValue,
pub setters: Vec<(String, DpValue)>,
}
impl DataTriggerSpec {
#[must_use]
pub fn new(binding_path: impl Into<String>, value: DpValue) -> Self {
Self {
binding_path: binding_path.into(),
relative_source_self: false,
value,
setters: Vec::new(),
}
}
#[must_use]
pub fn relative_source_self(mut self) -> Self {
self.relative_source_self = true;
self
}
#[must_use]
pub fn setter(mut self, property: impl Into<String>, value: DpValue) -> Self {
self.setters.push((property.into(), value));
self
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MultiTriggerSpec {
pub conditions: Vec<(String, DpValue)>,
pub setters: Vec<(String, DpValue)>,
}
impl MultiTriggerSpec {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn condition(mut self, property: impl Into<String>, value: DpValue) -> Self {
self.conditions.push((property.into(), value));
self
}
#[must_use]
pub fn setter(mut self, property: impl Into<String>, value: DpValue) -> Self {
self.setters.push((property.into(), value));
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StyleSpec {
pub target_type: String,
pub based_on: Option<Box<StyleSpec>>,
pub setters: Vec<(String, DpValue)>,
pub triggers: Vec<PropertyTrigger>,
pub data_triggers: Vec<DataTriggerSpec>,
pub multi_triggers: Vec<MultiTriggerSpec>,
}
impl StyleSpec {
#[must_use]
pub fn new(target_type: impl Into<String>) -> Self {
Self {
target_type: target_type.into(),
based_on: None,
setters: Vec::new(),
triggers: Vec::new(),
data_triggers: Vec::new(),
multi_triggers: Vec::new(),
}
}
#[must_use]
pub fn based_on(mut self, base: StyleSpec) -> Self {
self.based_on = Some(Box::new(base));
self
}
#[must_use]
pub fn setter(mut self, property: impl Into<String>, value: DpValue) -> Self {
self.setters.push((property.into(), value));
self
}
#[must_use]
pub fn trigger(mut self, trigger: PropertyTrigger) -> Self {
self.triggers.push(trigger);
self
}
#[must_use]
pub fn data_trigger(mut self, trigger: DataTriggerSpec) -> Self {
self.data_triggers.push(trigger);
self
}
#[must_use]
pub fn multi_trigger(mut self, trigger: MultiTriggerSpec) -> Self {
self.multi_triggers.push(trigger);
self
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct NoesisStyles {
pub styles: HashMap<String, StyleSpec>,
}
impl NoesisStyles {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn apply(mut self, name: impl Into<String>, spec: StyleSpec) -> Self {
self.styles.insert(name.into(), spec);
self
}
pub fn restyle(&mut self, name: impl Into<String>, spec: StyleSpec) {
self.styles.insert(name.into(), spec);
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn sync_styles_bridge(
views: Query<(Entity, Ref<NoesisStyles>)>,
state: Option<NonSendMut<NoesisRenderState>>,
) {
let Some(mut state) = state else {
return;
};
for (entity, styles) in &views {
if styles.is_changed() || state.scene_rebuilt_this_frame(entity) {
state.apply_styles_for(entity, &styles.styles);
}
}
}
pub struct NoesisStylesPlugin;
impl Plugin for NoesisStylesPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostUpdate, sync_styles_bridge.in_set(NoesisSet::Apply));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_collects_styles() {
let s = NoesisStyles::new().apply(
"Panel",
StyleSpec::new("Border")
.setter("Opacity", DpValue::F32(0.5))
.trigger(
PropertyTrigger::new("IsEnabled", DpValue::Bool(false))
.setter("Opacity", DpValue::F32(0.25)),
),
);
let spec = s.styles.get("Panel").expect("Panel styled");
assert_eq!(spec.target_type, "Border");
assert_eq!(
spec.setters,
vec![("Opacity".to_string(), DpValue::F32(0.5))]
);
assert_eq!(spec.triggers.len(), 1);
assert_eq!(spec.triggers[0].property, "IsEnabled");
assert_eq!(
spec.triggers[0].setters,
vec![("Opacity".to_string(), DpValue::F32(0.25))],
);
}
#[test]
fn builder_collects_based_on_and_extended_triggers() {
let spec = StyleSpec::new("Border")
.based_on(StyleSpec::new("Border").setter("Width", DpValue::F32(40.0)))
.data_trigger(
DataTriggerSpec::new("Tag", DpValue::Str("active".into()))
.relative_source_self()
.setter("Opacity", DpValue::F32(0.5)),
)
.multi_trigger(
MultiTriggerSpec::new()
.condition("IsEnabled", DpValue::Bool(true))
.condition("IsHitTestVisible", DpValue::Bool(true))
.setter("Opacity", DpValue::F32(0.25)),
);
let base = spec.based_on.as_deref().expect("based_on set");
assert_eq!(
base.setters,
vec![("Width".to_string(), DpValue::F32(40.0))]
);
assert_eq!(spec.data_triggers.len(), 1);
let dt = &spec.data_triggers[0];
assert_eq!(dt.binding_path, "Tag");
assert!(dt.relative_source_self);
assert_eq!(dt.value, DpValue::Str("active".into()));
assert_eq!(dt.setters, vec![("Opacity".to_string(), DpValue::F32(0.5))]);
assert_eq!(spec.multi_triggers.len(), 1);
let mt = &spec.multi_triggers[0];
assert_eq!(mt.conditions.len(), 2);
assert_eq!(
mt.setters,
vec![("Opacity".to_string(), DpValue::F32(0.25))]
);
}
}