ixa/triggers/property_change.rs
1//! Trigger criterion for writes to an entity property with particular previous and/or current
2//! values.
3//!
4//! [`PropertyChangeTrigger`] observes
5//! [`PropertyChangeEvent`](crate::entity::events::PropertyChangeEvent) for a specific
6//! entity/property pair and emits when a property write matches its configured previous value,
7//! current value, or both.
8//!
9//! ## Construction
10//!
11//! ```rust,ignore
12//! PropertyChangeTrigger::<E, P>::from(from)
13//! PropertyChangeTrigger::<E, P>::to(to)
14//! PropertyChangeTrigger::<E, P>::from_to(from, to)
15//! PropertyChangeTrigger::<E, P>::from(from).once()
16//! PropertyChangeTrigger::<E, P>::from(from).repeating()
17//! ```
18//!
19//! ## Observation
20//!
21//! The observation data passed to
22//! [`TriggerCriterion::emit_with`](super::TriggerCriterion::emit_with) is
23//! [`PropertyChangeTriggerEvent`]. It contains the entity ID, the previous property value, the
24//! current property value, and the selected [`TriggerMode`](super::TriggerMode) with which the
25//! trigger was created:
26//!
27//! ```rust,ignore
28//! pub struct PropertyChangeTriggerEvent<E, P>
29//! where
30//! E: Entity,
31//! P: Property<E>,
32//! {
33//! pub entity_id: EntityId<E>,
34//! pub previous: P,
35//! pub current: P,
36//! pub mode: TriggerMode,
37//! }
38//! ```
39//!
40//! ## Semantics
41//!
42//! By default, the criterion uses [`TriggerMode::Repeating`](super::TriggerMode::Repeating) and
43//! emits for every matching property write. Call [`PropertyChangeTrigger::once`] to emit only for
44//! the first matching write, or [`PropertyChangeTrigger::repeating`] to return to the default
45//! repeating behavior.
46//!
47//! A `from` constraint matches `event.previous`; a `to` constraint matches `event.current`;
48//! `from_to` requires both. Property writes are eventful even when the old and new values are
49//! equal. For example, `PropertyChangeTrigger::to(Alive(false))` can match a write that sets
50//! `Alive(false)` when the entity was already `Alive(false)`, and
51//! `PropertyChangeTrigger::from_to(Alive(false), Alive(false))` matches that no-op write exactly.
52//!
53//! ## Example
54//!
55//! ```rust
56//! use ixa::{Context, ContextEntitiesExt, define_entity, define_property, IxaEvent};
57//! use ixa::entity::EntityId;
58//! use ixa::triggers::{ContextTriggersExt, PropertyChangeTrigger, TriggerCriterion};
59//!
60//! define_entity!(Person);
61//! define_property!(struct Alive(bool), Person, default_const = Alive(true));
62//!
63//! #[derive(IxaEvent)]
64//! struct FirstDeath {
65//! person: EntityId<Person>
66//! }
67//!
68//! let mut context = Context::new();
69//!
70//! context.register_trigger(
71//! PropertyChangeTrigger::from_to(Alive(true), Alive(false))
72//! .once()
73//! .emit_with(|observation| FirstDeath {
74//! person: observation.entity_id
75//! }),
76//! );
77//!
78//! context.subscribe_to_event(|_context, _event: FirstDeath| {
79//! // respond when a person changes from alive to dead
80//! });
81//! ```
82//!
83use std::cell::Cell;
84use std::marker::PhantomData;
85use std::rc::Rc;
86
87use super::{TriggerCriterion, TriggerMode};
88use crate::entity::events::PropertyChangeEvent;
89use crate::entity::property::Property;
90use crate::entity::{Entity, EntityId};
91use crate::Context;
92
93pub struct PropertyChangeTrigger<E, P>
94where
95 E: Entity,
96 P: Property<E>,
97{
98 from: Option<P>,
99 to: Option<P>,
100 mode: TriggerMode,
101 _entity: PhantomData<fn() -> E>,
102}
103
104#[derive(Clone, Copy, Debug)]
105pub struct PropertyChangeTriggerEvent<E, P>
106where
107 E: Entity,
108 P: Property<E>,
109{
110 pub entity_id: EntityId<E>,
111 pub previous: P,
112 pub current: P,
113 pub mode: TriggerMode,
114}
115
116impl<E, P> PropertyChangeTrigger<E, P>
117where
118 E: Entity,
119 P: Property<E>,
120{
121 #[must_use]
122 pub fn from(from: P) -> Self {
123 Self {
124 from: Some(from),
125 to: None,
126 mode: TriggerMode::Repeating,
127 _entity: PhantomData,
128 }
129 }
130
131 #[must_use]
132 pub fn to(to: P) -> Self {
133 Self {
134 from: None,
135 to: Some(to),
136 mode: TriggerMode::Repeating,
137 _entity: PhantomData,
138 }
139 }
140
141 #[must_use]
142 pub fn from_to(from: P, to: P) -> Self {
143 Self {
144 from: Some(from),
145 to: Some(to),
146 mode: TriggerMode::Repeating,
147 _entity: PhantomData,
148 }
149 }
150
151 #[must_use]
152 pub fn once(mut self) -> Self {
153 self.mode = TriggerMode::Once;
154 self
155 }
156
157 #[must_use]
158 pub fn repeating(mut self) -> Self {
159 self.mode = TriggerMode::Repeating;
160 self
161 }
162}
163
164impl<E, P> TriggerCriterion for PropertyChangeTrigger<E, P>
165where
166 E: Entity,
167 P: Property<E>,
168{
169 type Observation = PropertyChangeTriggerEvent<E, P>;
170
171 fn install<F>(self, context: &mut Context, on_match: F)
172 where
173 F: Fn(&mut Context, Self::Observation) + 'static,
174 {
175 match self.mode {
176 TriggerMode::Once => {
177 let active = Rc::new(Cell::new(true));
178 context.subscribe_to_event(move |context, event: PropertyChangeEvent<E, P>| {
179 if !active.get() {
180 return;
181 }
182 let from_matches = self.from.is_none_or(|from| event.previous == from);
183 let to_matches = self.to.is_none_or(|to| event.current == to);
184 if from_matches && to_matches {
185 on_match(
186 context,
187 PropertyChangeTriggerEvent {
188 entity_id: event.entity_id,
189 previous: event.previous,
190 current: event.current,
191 mode: self.mode,
192 },
193 );
194 active.set(false);
195 }
196 });
197 }
198 TriggerMode::Repeating => {
199 context.subscribe_to_event(move |context, event: PropertyChangeEvent<E, P>| {
200 let from_matches = self.from.is_none_or(|from| event.previous == from);
201 let to_matches = self.to.is_none_or(|to| event.current == to);
202 if from_matches && to_matches {
203 on_match(
204 context,
205 PropertyChangeTriggerEvent {
206 entity_id: event.entity_id,
207 previous: event.previous,
208 current: event.current,
209 mode: self.mode,
210 },
211 );
212 }
213 });
214 }
215 }
216 }
217}