oxide_mvu/runtime.rs
1//! The MVU runtime that orchestrates the event loop.
2
3#[cfg(feature = "no_std")]
4use alloc::boxed::Box;
5#[cfg(feature = "no_std")]
6use alloc::vec::Vec;
7
8use portable_atomic_util::Arc;
9use spin::Mutex;
10
11use crate::{Emitter, MvuLogic, Renderer, Spawner};
12
13#[cfg(any(test, feature = "testing"))]
14use crate::Effect;
15
16/// Internal state for the MVU runtime.
17struct RuntimeState<Event: Send, Model: Clone + Send> {
18 model: Model,
19 event_queue: Vec<Event>,
20}
21
22/// The MVU runtime that orchestrates the event loop.
23///
24/// This is the core of the framework. It:
25/// 1. Initializes the Model and initial Effects via [`MvuLogic::init`]
26/// 2. Processes events through [`MvuLogic::update`]
27/// 3. Reduces the Model to Props via [`MvuLogic::view`]
28/// 4. Delivers Props to the [`Renderer`] for rendering
29///
30/// The runtime creates a single [`Emitter`] that automatically processes events
31/// when [`Emitter::emit`] is called, regardless of which thread it's called from.
32/// Events are processed synchronously in a thread-safe manner.
33///
34/// For testing with manual control, use [`TestMvuRuntime`] with a [`crate::TestRenderer`].
35///
36/// See the [crate-level documentation](crate) for a complete example.
37pub struct MvuRuntime<Event: Send, Model: Clone + Send, Props> {
38 logic: Box<dyn MvuLogic<Event, Model, Props> + Send>,
39 renderer: Box<dyn Renderer<Props> + Send>,
40 state: Arc<Mutex<RuntimeState<Event, Model>>>,
41 emitter: Emitter<Event>,
42 spawner: Spawner,
43}
44
45impl<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static>
46 MvuRuntime<Event, Model, Props>
47{
48 /// Create a new runtime.
49 ///
50 /// The runtime will not be started until MvuRuntime::run is called.
51 ///
52 /// # Arguments
53 ///
54 /// * `init_model` - The initial state
55 /// * `logic` - Application logic implementing MvuLogic
56 /// * `renderer` - Platform rendering implementation for rendering Props
57 /// * `spawner` - Function to spawn async effects on your chosen runtime
58 pub fn new(
59 init_model: Model,
60 logic: Box<dyn MvuLogic<Event, Model, Props> + Send>,
61 renderer: Box<dyn Renderer<Props> + Send>,
62 spawner: Spawner,
63 ) -> Self {
64 // Create state and emitter that enqueues to the state's event queue
65 let state = Arc::new(Mutex::new(RuntimeState {
66 model: init_model,
67 event_queue: Vec::new(),
68 }));
69
70 let state_clone = state.clone();
71 let emitter = Emitter::new(move |event| {
72 state_clone.lock().event_queue.push(event);
73 });
74
75 MvuRuntime {
76 logic,
77 renderer,
78 state,
79 emitter,
80 spawner,
81 }
82 }
83
84 /// Initialize the runtime loop.
85 ///
86 /// - Uses the MvuLogic::init function to create and enqueue initial side effects.
87 /// - Reduces the initial Model provided at construction to Props via MvuLogic::view.
88 /// - Renders the initial Props.
89 pub fn run(mut self) {
90 // Initialize the model and get initial effects
91 let (init_model, init_effect) = {
92 let mut runtime_state = self.state.lock();
93 let (init_model, init_effect) = {
94 let model = runtime_state.model.clone();
95 self.logic.init(model)
96 };
97
98 // Update model
99 runtime_state.model = init_model.clone();
100
101 (init_model, init_effect)
102 };
103
104 let initial_props = {
105 let emitter = &self.emitter;
106 self.logic.view(&init_model, emitter)
107 };
108
109 self.renderer.render(initial_props);
110
111 // Execute initial effect by spawning it
112 let emitter = self.emitter.clone();
113 let future = init_effect.execute(&emitter);
114 (self.spawner)(Box::pin(future));
115 }
116
117 #[cfg(any(test, feature = "testing"))]
118 fn step(&mut self, event: Event) {
119 // Reduce event and render props
120 let (model, effect, props) = self.reduce_event(event);
121
122 self.renderer.render(props);
123
124 // Update model
125 {
126 let state_mutex = self.state.clone();
127 let mut runtime_state = state_mutex.lock();
128 runtime_state.model = model;
129 }
130
131 // Execute the effect (which may enqueue more events)
132 let emitter = self.emitter.clone();
133 let future = effect.execute(&emitter);
134 (self.spawner)(Box::pin(future));
135
136 // Process any newly queued events
137 self.process_queued_events()
138 }
139
140 #[cfg(any(test, feature = "testing"))]
141 /// Dispatch a single event through update -> view -> render.
142 fn reduce_event(&self, event: Event) -> (Model, Effect<Event>, Props) {
143 // Update model just event
144 let (new_model, effect) = {
145 let runtime_state = self.state.lock();
146 self.logic.update(event, &runtime_state.model)
147 };
148
149 // Reduce the new model and emitter to props
150 let emitter = &self.emitter;
151 let props = self.logic.view(&new_model, emitter);
152
153 (new_model, effect, props)
154 }
155
156 #[cfg(any(test, feature = "testing"))]
157 /// Process all queued events (for testing).
158 ///
159 /// This is exposed for TestMvuRuntime to manually drive event processing.
160 fn process_queued_events(&mut self) {
161 loop {
162 let state_mutex = self.state.clone();
163 let next_event = {
164 let mut runtime_state = state_mutex.lock();
165 if runtime_state.event_queue.is_empty() {
166 break;
167 }
168 runtime_state.event_queue.remove(0)
169 }; // Lock is dropped here
170 self.step(next_event);
171 }
172 }
173}
174
175#[cfg(any(test, feature = "testing"))]
176/// Creates a test spawner that executes futures synchronously.
177///
178/// This is useful for testing - it blocks on the future immediately rather
179/// than spawning it on an async runtime. Use this with [`TestMvuRuntime`]
180/// or [`MvuRuntime`] in test scenarios.
181pub fn create_test_spawner() -> Spawner {
182 Box::new(|fut| {
183 // Execute the future synchronously for deterministic testing
184 futures::executor::block_on(fut);
185 })
186}
187
188#[cfg(any(test, feature = "testing"))]
189/// Test runtime driver for manual event processing control.
190///
191/// Only available with the `testing` feature or during tests.
192///
193/// Returned by [`TestMvuRuntime::run`]. Provides methods to manually
194/// emit events and process the event queue for precise control in tests.
195///
196/// See [`TestMvuRuntime`] for usage.
197pub struct TestMvuDriver<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static> {
198 _runtime: MvuRuntime<Event, Model, Props>,
199}
200
201#[cfg(any(test, feature = "testing"))]
202impl<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static>
203 TestMvuDriver<Event, Model, Props>
204{
205 /// Process all queued events.
206 ///
207 /// This processes events until the queue is empty. Call this after emitting
208 /// events to drive the event loop in tests.
209 pub fn process_events(&mut self) {
210 self._runtime.process_queued_events();
211 }
212}
213
214#[cfg(any(test, feature = "testing"))]
215/// Test runtime for MVU with manual event processing control.
216///
217/// Only available with the `testing` feature or during tests.
218///
219/// Unlike [`MvuRuntime`], this runtime does not automatically
220/// process events when they are emitted. Instead, tests must manually call
221/// [`process_events`](TestMvuDriver::process_events) on the returned driver
222/// to process the event queue.
223///
224/// This provides precise control over event timing in tests.
225///
226/// ```rust
227/// use oxide_mvu::{Emitter, Effect, Renderer, MvuLogic, TestMvuRuntime};
228/// # enum Event { Increment }
229/// # #[derive(Clone)]
230/// # struct Model { count: i32 }
231/// # struct Props { count: i32, on_click: Box<dyn Fn() + Send> }
232/// # struct MyApp;
233/// # impl MvuLogic<Event, Model, Props> for MyApp {
234/// # fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
235/// # fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
236/// # (Model { count: model.count + 1 }, Effect::none())
237/// # }
238/// # fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
239/// # let e = emitter.clone();
240/// # Props { count: model.count, on_click: Box::new(move || e.emit(Event::Increment)) }
241/// # }
242/// # }
243/// # struct TestRenderer;
244/// # impl Renderer<Props> for TestRenderer { fn render(&mut self, _props: Props) {} }
245/// use oxide_mvu::create_test_spawner;
246///
247/// let runtime = TestMvuRuntime::new(
248/// Model { count: 0 },
249/// Box::new(MyApp),
250/// Box::new(TestRenderer),
251/// create_test_spawner()
252/// );
253/// let mut driver = runtime.run();
254/// driver.process_events(); // Manually process events
255/// ```
256pub struct TestMvuRuntime<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static> {
257 runtime: MvuRuntime<Event, Model, Props>,
258}
259
260#[cfg(any(test, feature = "testing"))]
261impl<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static>
262 TestMvuRuntime<Event, Model, Props>
263{
264 /// Create a new test runtime.
265 ///
266 /// Creates an emitter that enqueues events without automatically processing them.
267 ///
268 /// # Arguments
269 ///
270 /// * `init_model` - The initial state
271 /// * `logic` - Application logic implementing MvuLogic
272 /// * `renderer` - Platform rendering implementation for rendering Props
273 /// * `spawner` - Function to spawn async effects on your chosen runtime
274 pub fn new(
275 init_model: Model,
276 logic: Box<dyn MvuLogic<Event, Model, Props> + Send>,
277 renderer: Box<dyn Renderer<Props> + Send>,
278 spawner: Spawner,
279 ) -> Self {
280 // Create state and emitter that enqueues to the state's event queue
281 let state = Arc::new(Mutex::new(RuntimeState {
282 model: init_model,
283 event_queue: Vec::new(),
284 }));
285
286 let state_clone = state.clone();
287 let emitter = Emitter::new(move |event| {
288 state_clone.lock().event_queue.push(event);
289 });
290
291 TestMvuRuntime {
292 runtime: MvuRuntime {
293 logic,
294 renderer,
295 state,
296 emitter,
297 spawner,
298 },
299 }
300 }
301
302 /// Initializes the runtime and returns a driver for manual event processing.
303 ///
304 /// This processes initial effects and renders the initial state, then returns
305 /// a [`TestMvuDriver`] that provides manual control over event processing.
306 pub fn run(mut self) -> TestMvuDriver<Event, Model, Props> {
307 // Initialize the model and get initial effects
308 let (init_model, init_effect) = {
309 let mut runtime_state = self.runtime.state.lock();
310 let (init_model, init_effect) = {
311 let model = runtime_state.model.clone();
312 self.runtime.logic.init(model)
313 };
314
315 // Update model
316 runtime_state.model = init_model.clone();
317
318 (init_model, init_effect)
319 };
320
321 let initial_props = {
322 let emitter = &self.runtime.emitter;
323 self.runtime.logic.view(&init_model, emitter)
324 };
325
326 self.runtime.renderer.render(initial_props);
327
328 // Execute initial effect by spawning it
329 {
330 let emitter = self.runtime.emitter.clone();
331 let spawner = &self.runtime.spawner;
332 let future = init_effect.execute(&emitter);
333 spawner(Box::pin(future));
334 }
335
336 TestMvuDriver {
337 _runtime: self.runtime,
338 }
339 }
340}