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