1use crate::time::ChangeTick;
8
9#[non_exhaustive]
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub enum StateError {
13 ConflictingTransition,
15}
16
17pub struct State<S: Eq + 'static> {
19 current: S,
20 previous: Option<S>,
21 pending: Option<S>,
22 transition_tick: Option<ChangeTick>,
23}
24
25impl<S: Eq + 'static> State<S> {
26 pub fn new(initial: S) -> Self {
28 Self {
29 current: initial,
30 previous: None,
31 pending: None,
32 transition_tick: None,
33 }
34 }
35
36 pub fn current(&self) -> &S {
38 &self.current
39 }
40
41 pub fn previous(&self) -> Option<&S> {
43 self.previous.as_ref()
44 }
45
46 pub fn pending(&self) -> Option<&S> {
48 self.pending.as_ref()
49 }
50
51 pub fn transition_tick(&self) -> Option<ChangeTick> {
53 self.transition_tick
54 }
55
56 pub fn request(&mut self, next: S) -> Result<(), StateError> {
58 if let Some(pending) = &self.pending {
59 if *pending == next {
60 return Ok(());
61 }
62 return Err(StateError::ConflictingTransition);
63 }
64 if self.current == next {
65 return Ok(());
66 }
67 self.pending = Some(next);
68 Ok(())
69 }
70
71 pub(crate) fn apply_pending(&mut self, tick: ChangeTick) {
72 let Some(next) = self.pending.take() else {
73 return;
74 };
75 self.previous = Some(core::mem::replace(&mut self.current, next));
76 self.transition_tick = Some(tick);
77 }
78}
79
80pub fn apply<S: Eq + 'static>(
82 name: impl Into<alloc::string::String>,
83 stage_label: impl Into<alloc::string::String>,
84) -> crate::schedule::System {
85 let label = name.into();
86 crate::schedule::System::try_new(
87 label,
88 stage_label,
89 move |world: &mut crate::world::World, _dt| {
90 let tick = world
91 .issue_change_tick_for_state()
92 .map_err(|error| alloc::format!("{error:?}"))?;
93 let state = world
94 .resource_mut::<State<S>>()
95 .map_err(|error| alloc::format!("{error:?}"))?
96 .expect("required state resource remains present while the schedule lease is live");
97 state.apply_pending(tick);
98 Ok(())
99 },
100 )
101 .requires_resource::<State<S>>()
102}
103
104pub fn on_exit<S: Eq + 'static>(
108 name: impl Into<alloc::string::String>,
109 stage_label: impl Into<alloc::string::String>,
110 body: impl FnMut(&mut crate::world::World, f32) + 'static,
111) -> crate::schedule::System {
112 crate::schedule::System::new(name, stage_label, body)
113 .run_if(crate::schedule::Condition::state_pending::<S>())
114 .requires_resource::<State<S>>()
115}
116
117pub fn on_transition<S: Eq + 'static>(
120 name: impl Into<alloc::string::String>,
121 stage_label: impl Into<alloc::string::String>,
122 body: impl FnMut(&mut crate::world::World, f32) + 'static,
123) -> crate::schedule::System {
124 crate::schedule::System::new(name, stage_label, body)
125 .run_if(crate::schedule::Condition::state_changed::<S>())
126 .requires_resource::<State<S>>()
127}
128
129pub fn on_enter<S: Eq + 'static>(
132 name: impl Into<alloc::string::String>,
133 stage_label: impl Into<alloc::string::String>,
134 body: impl FnMut(&mut crate::world::World, f32) + 'static,
135) -> crate::schedule::System {
136 on_transition::<S>(name, stage_label, body)
137}
138
139#[cfg(feature = "std")]
140impl core::fmt::Display for StateError {
141 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
142 match self {
143 Self::ConflictingTransition => f.write_str("conflicting state transition request"),
144 }
145 }
146}
147
148#[cfg(feature = "std")]
149impl std::error::Error for StateError {}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::time::ChangeTick;
155 #[cfg(feature = "std")]
156 use alloc::string::ToString;
157
158 #[derive(Clone, Debug, Eq, PartialEq)]
159 enum Phase {
160 A,
161 B,
162 C,
163 }
164
165 #[test]
166 fn state_request_idempotent_and_conflicting() {
167 let mut state = State::new(Phase::A);
168 state.request(Phase::B).expect("first");
169 state.request(Phase::B).expect("repeat");
170 assert!(matches!(
171 state.request(Phase::C),
172 Err(StateError::ConflictingTransition)
173 ));
174 assert_eq!(state.pending(), Some(&Phase::B));
175 #[cfg(feature = "std")]
176 assert_eq!(
177 StateError::ConflictingTransition.to_string(),
178 "conflicting state transition request"
179 );
180 }
181
182 #[test]
183 fn apply_system_wiring_requires_state_and_runs_transitions() {
184 use crate::app::AppBuilder;
185 use crate::schedule::{stage, BuildError};
186
187 #[derive(Clone, Debug, Eq, PartialEq)]
188 enum Menu {
189 Open,
190 Closed,
191 }
192
193 let mut missing = AppBuilder::new();
194 missing
195 .add_system(apply::<Menu>("apply", stage::UPDATE))
196 .expect("system");
197 assert!(matches!(
198 missing.build(),
199 Err(BuildError::MissingRequiredResource { .. })
200 ));
201
202 let mut builder = AppBuilder::new();
203 builder.insert_state(Menu::Open);
204 builder
205 .add_system(apply::<Menu>("apply", stage::UPDATE))
206 .expect("system");
207 let mut app = builder.build().expect("app");
208 app.world_mut()
209 .resource_mut::<State<Menu>>()
210 .expect("state access")
211 .expect("state resource")
212 .request(Menu::Closed)
213 .expect("request");
214 app.update(0.0).expect("update");
215 let state = app
216 .world()
217 .resource::<State<Menu>>()
218 .expect("state access")
219 .expect("state resource");
220 assert_eq!(state.current(), &Menu::Closed);
221 assert_eq!(state.previous(), Some(&Menu::Open));
222 assert!(state.pending().is_none());
223 assert!(state.transition_tick().is_some());
224 }
225
226 #[test]
227 fn apply_pending_moves_current_and_records_tick() {
228 let mut state = State::new(Phase::A);
229 state.request(Phase::B).expect("request");
230 let tick = ChangeTick::from_raw(9);
231 state.apply_pending(tick);
232 assert_eq!(state.current(), &Phase::B);
233 assert_eq!(state.previous(), Some(&Phase::A));
234 assert_eq!(state.transition_tick(), Some(tick));
235 state.apply_pending(tick);
236 assert_eq!(state.current(), &Phase::B);
237 }
238
239 #[test]
240 fn lifecycle_helpers_observe_the_ordered_transition_boundary() {
241 use alloc::rc::Rc;
242 use core::cell::RefCell;
243
244 use crate::app::AppBuilder;
245 use crate::schedule::stage;
246
247 #[derive(Debug, Eq, PartialEq)]
248 enum Mode {
249 Menu,
250 Playing,
251 }
252
253 let order = Rc::new(RefCell::new(alloc::vec::Vec::new()));
254 let exit_order = Rc::clone(&order);
255 let transition_order = Rc::clone(&order);
256 let enter_order = Rc::clone(&order);
257 let mut builder = AppBuilder::new();
258 builder.insert_resource(State::new(Mode::Menu));
261 builder
262 .add_system(on_exit::<Mode>("exit", stage::UPDATE, move |world, _| {
263 let state = world
264 .resource::<State<Mode>>()
265 .expect("state")
266 .expect("present");
267 assert_eq!(state.current(), &Mode::Menu);
268 assert_eq!(state.pending(), Some(&Mode::Playing));
269 exit_order.borrow_mut().push("exit");
270 }))
271 .expect("exit");
272 builder
273 .add_system(apply::<Mode>("apply", stage::UPDATE).after("exit"))
274 .expect("apply");
275 builder
276 .add_system(
277 on_transition::<Mode>("transition", stage::UPDATE, move |world, _| {
278 let state = world
279 .resource::<State<Mode>>()
280 .expect("state")
281 .expect("present");
282 assert_eq!(state.previous(), Some(&Mode::Menu));
283 assert_eq!(state.current(), &Mode::Playing);
284 transition_order.borrow_mut().push("transition");
285 })
286 .after("apply"),
287 )
288 .expect("transition");
289 builder
290 .add_system(
291 on_enter::<Mode>("enter", stage::UPDATE, move |_, _| {
292 enter_order.borrow_mut().push("enter");
293 })
294 .after("transition"),
295 )
296 .expect("enter");
297 let mut app = builder.build().expect("app");
298 app.world_mut()
299 .resource_mut::<State<Mode>>()
300 .expect("state")
301 .expect("present")
302 .request(Mode::Playing)
303 .expect("request");
304
305 app.update(0.0).expect("update");
306 assert_eq!(&*order.borrow(), &["exit", "transition", "enter"]);
307 }
308}