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