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}