Skip to main content

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}