Skip to main content

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}