use bevy::prelude::*;
use bevy::ui::{Checked, InteractionDisabled};
use crate::binding::find_context;
use crate::provider::{self, PfPropertyStore, PropertyTarget, StoredValue, ValueSource};
use crate::resources::ResourceKey;
#[derive(Debug, Clone)]
pub enum ResolvedCondition {
MouseOver(bool),
Pressed(bool),
Checked(bool),
Enabled(bool),
Selected(bool),
Focused(bool),
HasClass(String),
Data { path: String, expected: String },
}
#[derive(Debug, Clone)]
pub enum TriggerValue {
Static(StoredValue),
Dynamic(ResourceKey),
TemplatedParent(PropertyTarget),
AppTheme(crate::app_theme::ThemeArms),
}
#[derive(Debug, Clone)]
pub struct ResolvedTriggerSetter {
pub target: PropertyTarget,
pub value: TriggerValue,
pub dest: Option<Entity>,
pub tier: ValueSource,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedTrigger {
pub conditions: Vec<ResolvedCondition>,
pub setters: Vec<ResolvedTriggerSetter>,
pub enter_storyboards: Vec<std::sync::Arc<crate::animation::PfStoryboard>>,
pub exit_storyboards: Vec<std::sync::Arc<crate::animation::PfStoryboard>>,
}
#[derive(Resource, Default, PartialEq, Eq)]
pub(crate) struct LastTriggerGen(pub (u64, u64));
#[derive(Component, Debug, Default, Clone)]
pub struct PfTriggers {
pub triggers: Vec<ResolvedTrigger>,
pub active: Vec<bool>,
}
fn eval_condition(world: &World, entity: Entity, cond: &ResolvedCondition) -> bool {
match cond {
ResolvedCondition::MouseOver(expected) => {
let over = matches!(
world.get::<Interaction>(entity),
Some(Interaction::Hovered) | Some(Interaction::Pressed)
);
over == *expected
}
ResolvedCondition::Pressed(expected) => {
let pressed = matches!(world.get::<Interaction>(entity), Some(Interaction::Pressed));
pressed == *expected
}
ResolvedCondition::Checked(expected) => {
(world.get::<Checked>(entity).is_some()) == *expected
}
ResolvedCondition::Enabled(expected) => {
(world.get::<InteractionDisabled>(entity).is_none()) == *expected
}
ResolvedCondition::Focused(expected) => {
let focused = world
.get_resource::<bevy::input_focus::InputFocus>()
.and_then(|f| f.get())
.is_some_and(|f| {
let mut cursor = f;
loop {
if cursor == entity {
break true;
}
match world.get::<ChildOf>(cursor) {
Some(parent) => cursor = parent.parent(),
None => break false,
}
}
});
focused == *expected
}
ResolvedCondition::Selected(expected) => {
let list = world.get::<ChildOf>(entity).map(|p| {
world
.get::<crate::components::PfGeneratedItemsHost>(p.parent())
.map(|host| host.owner)
.unwrap_or(p.parent())
});
let selected = list
.and_then(|l| world.get::<crate::components::PfListBox>(l))
.and_then(|l| l.selected)
.is_some_and(|s| s == entity);
selected == *expected
}
ResolvedCondition::HasClass(name) => world
.get::<crate::components::PfClasses>(entity)
.is_some_and(|c| c.has(name)),
ResolvedCondition::Data { path, expected } => {
let Some(ctx) = find_context(world, entity) else {
return false;
};
let Some(value) = ctx.read_path(path) else {
return false;
};
let actual = value.to_display();
actual == *expected || actual.eq_ignore_ascii_case(expected)
}
}
}
pub(crate) fn evaluate_triggers(world: &mut World) {
let generation = (
world
.get_resource::<crate::dynamic::PfApplicationResources>()
.map(|r| r.revision)
.unwrap_or(0),
world
.get_resource::<crate::app_theme::PfAppTheme>()
.map(|t| t.generation())
.unwrap_or(0),
);
let refreshed = world.get_resource::<LastTriggerGen>().map(|l| l.0) != Some(generation);
if refreshed {
world.insert_resource(LastTriggerGen(generation));
}
let mut query = world.query_filtered::<Entity, With<PfTriggers>>();
let entities: Vec<Entity> = query.iter(world).collect();
for entity in entities {
let Some(triggers) = world.get::<PfTriggers>(entity) else {
continue;
};
let list = triggers.triggers.clone();
let old_active = triggers.active.clone();
let new_active: Vec<bool> = list
.iter()
.map(|t| t.conditions.iter().all(|c| eval_condition(world, entity, c)))
.collect();
if new_active == old_active && !refreshed {
continue;
}
let mut pairs: Vec<(Entity, ValueSource)> = Vec::new();
for trigger in &list {
for setter in &trigger.setters {
let dest = setter.dest.unwrap_or(entity);
if !pairs.contains(&(dest, setter.tier)) {
pairs.push((dest, setter.tier));
}
}
}
let mut affected: Vec<(Entity, PropertyTarget)> = Vec::new();
let mut stale: Vec<Entity> = Vec::new();
for &(dest, tier) in &pairs {
let Ok(mut e) = world.get_entity_mut(dest) else {
stale.push(dest);
continue;
};
if let Some(mut store) = e.get_mut::<PfPropertyStore>() {
for target in store.clear_tier(tier) {
if !affected.contains(&(dest, target)) {
affected.push((dest, target));
}
}
}
}
for (trigger, active) in list.iter().zip(&new_active) {
if !active {
continue;
}
for setter in &trigger.setters {
let dest = setter.dest.unwrap_or(entity);
if stale.contains(&dest) {
continue;
}
let value: StoredValue = match &setter.value {
TriggerValue::Static(v) => v.clone(),
TriggerValue::Dynamic(key) => {
match crate::dynamic::resolve_dynamic(world, entity, key) {
Some(v) => Some(v),
None => continue, }
}
TriggerValue::TemplatedParent(src) => {
match world.get::<PfPropertyStore>(entity).and_then(|s| s.effective(*src)) {
Some((_, v)) => v.clone(),
None => continue, }
}
TriggerValue::AppTheme(arms) => {
match arms.pick(crate::app_theme::app_theme(world)) {
Some(crate::app_theme::ThemeArm::Value(v)) => v.clone(),
Some(crate::app_theme::ThemeArm::Dynamic(key)) => {
match crate::dynamic::resolve_dynamic(world, entity, key) {
Some(v) => Some(v),
None => continue, }
}
None => None,
}
}
};
{
let Ok(mut e) = world.get_entity_mut(dest) else {
stale.push(dest);
continue;
};
if let Some(mut store) = e.get_mut::<PfPropertyStore>() {
store.set(setter.target, setter.tier, value);
} else {
let mut store = PfPropertyStore::default();
store.set(setter.target, setter.tier, value);
e.insert(store);
}
}
if !affected.contains(&(dest, setter.target)) {
affected.push((dest, setter.target));
}
}
}
let mut flips: Vec<std::sync::Arc<crate::animation::PfStoryboard>> = Vec::new();
for ((trigger, was), is) in list.iter().zip(&old_active).zip(&new_active) {
if was == is {
continue;
}
let launchers = if *is {
&trigger.enter_storyboards
} else {
&trigger.exit_storyboards
};
flips.extend(launchers.iter().cloned());
}
if let Some(mut triggers) = world.get_mut::<PfTriggers>(entity) {
triggers.active = new_active;
if !stale.is_empty() {
for trigger in &mut triggers.triggers {
trigger
.setters
.retain(|s| !s.dest.is_some_and(|d| stale.contains(&d)));
}
}
}
for (dest, target) in affected {
provider::apply_effective(world, dest, target);
}
for sb in flips {
crate::animation::begin_storyboard(world, entity, entity, &sb);
}
}
}