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;
7
8use async_channel::{bounded, Receiver};
9
10use crate::{Effect, Event as EventTrait};
11use crate::{Emitter, MvuLogic, Renderer};
12
13/// Default event channel capacity.
14///
15/// This bounds the number of events that can be queued before `emit()` drops events.
16/// Sized for embedded systems with limited heap. Increase this via
17/// [`MvuRuntimeBuilder::capacity`] if your application generates high-frequency bursts of events.
18pub const DEFAULT_EVENT_CAPACITY: usize = 32;
19
20/// Builder for configuring and constructing an [`MvuRuntime`].
21///
22/// Created via [`MvuRuntime::builder`]. Allows customizing runtime parameters
23/// like event buffer capacity before building the runtime.
24///
25/// # Example
26///
27/// ```rust,no_run
28/// # use oxide_mvu::{Emitter, Effect, MvuLogic, MvuRuntime, Renderer};
29/// # #[derive(Clone)] enum Event {}
30/// # #[derive(Clone)] struct Model;
31/// # struct Props;
32/// # struct MyLogic;
33/// # impl MvuLogic<Event, Model, Props> for MyLogic {
34/// # fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
35/// # fn update(&self, _: Event, model: &Model) -> (Model, Effect<Event>) { (model.clone(), Effect::none()) }
36/// # fn view(&self, _: &Model, _: &Emitter<Event>) -> Props { Props }
37/// # }
38/// # struct MyRenderer;
39/// # impl Renderer<Props> for MyRenderer { fn render(&mut self, _: Props) {} }
40/// // Use builder for custom configuration
41/// let runtime = MvuRuntime::builder(Model, MyLogic, MyRenderer, |_| {})
42/// .capacity(64)
43/// .build();
44/// ```
45pub struct MvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
46where
47 Event: EventTrait,
48 Model: Clone,
49 Logic: MvuLogic<Event, Model, Props>,
50 Render: Renderer<Props>,
51 Spawn: Spawner,
52{
53 init_model: Model,
54 logic: Logic,
55 renderer: Render,
56 spawner: Spawn,
57 capacity: usize,
58 _event: core::marker::PhantomData<Event>,
59 _props: core::marker::PhantomData<Props>,
60}
61
62impl<Event, Model, Props, Logic, Render, Spawn>
63 MvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
64where
65 Event: EventTrait,
66 Model: Clone + 'static,
67 Props: 'static,
68 Logic: MvuLogic<Event, Model, Props>,
69 Render: Renderer<Props>,
70 Spawn: Spawner,
71{
72 /// Set the event buffer capacity.
73 ///
74 /// This bounds the number of events that can be queued before
75 /// [`Emitter::try_emit`](crate::Emitter::try_emit) returns `false`.
76 ///
77 /// Defaults to [`DEFAULT_EVENT_CAPACITY`] (32).
78 pub fn capacity(mut self, capacity: usize) -> Self {
79 self.capacity = capacity;
80 self
81 }
82
83 /// Build the runtime with the configured settings.
84 pub fn build(self) -> MvuRuntime<Event, Model, Props, Logic, Render, Spawn> {
85 let (event_sender, event_receiver) = bounded(self.capacity);
86 let emitter = Emitter::new(event_sender);
87
88 MvuRuntime {
89 logic: self.logic,
90 renderer: self.renderer,
91 event_receiver,
92 model: self.init_model,
93 emitter,
94 spawner: self.spawner,
95 _props: core::marker::PhantomData,
96 }
97 }
98}
99
100use core::pin::Pin;
101
102/// A spawner trait for executing futures on an async runtime.
103///
104/// This abstraction allows you to use whatever concurrency model you want (tokio, async-std, embassy, etc.).
105///
106/// Function pointers and closures automatically implement this trait via the blanket implementation.
107pub trait Spawner {
108 /// Spawn a future on the async runtime.
109 fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>);
110}
111
112/// Implement Spawner for any callable type that matches the signature.
113///
114/// This includes function pointers, closures, and function items.
115impl<F> Spawner for F
116where
117 F: Fn(Pin<Box<dyn Future<Output = ()> + Send>>),
118{
119 fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
120 self(future)
121 }
122}
123
124/// The MVU runtime that orchestrates the event loop.
125///
126/// This is the core of the framework. It:
127/// 1. Initializes the Model and initial Effects via [`MvuLogic::init`]
128/// 2. Processes events through [`MvuLogic::update`]
129/// 3. Reduces the Model to Props via [`MvuLogic::view`]
130/// 4. Delivers Props to the [`Renderer`] for rendering
131///
132/// The runtime creates a single [`Emitter`] that can send events from any thread.
133/// Events are queued via a lock-free MPMC channel and processed on the thread where
134/// [`MvuRuntime::run`] was called.
135///
136/// For testing with manual control, use `TestMvuRuntime` with `TestRenderer`
137/// (both available with the `testing` feature).
138///
139/// See the [crate-level documentation](crate) for a complete example.
140///
141/// # Type Parameters
142///
143/// * `Event` - The event type for your application
144/// * `Model` - The model/state type for your application
145/// * `Props` - The props type produced by the view function
146/// * `Logic` - The logic implementation type (implements [`MvuLogic`])
147/// * `Render` - The renderer implementation type (implements [`Renderer`])
148/// * `Spawn` - The spawner implementation type (implements [`Spawner`])
149pub struct MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
150where
151 Event: EventTrait,
152 Model: Clone,
153 Logic: MvuLogic<Event, Model, Props>,
154 Render: Renderer<Props>,
155 Spawn: Spawner,
156{
157 logic: Logic,
158 renderer: Render,
159 event_receiver: Receiver<Event>,
160 model: Model,
161 emitter: Emitter<Event>,
162 spawner: Spawn,
163 _props: core::marker::PhantomData<Props>,
164}
165
166impl<Event, Model, Props, Logic, Render, Spawn>
167 MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
168where
169 Event: EventTrait,
170 Model: Clone + 'static,
171 Props: 'static,
172 Logic: MvuLogic<Event, Model, Props>,
173 Render: Renderer<Props>,
174 Spawn: Spawner,
175{
176 /// Create a builder for configuring the runtime.
177 ///
178 /// Use this when you need to customize runtime parameters like event buffer capacity.
179 ///
180 /// # Arguments
181 ///
182 /// * `init_model` - The initial state
183 /// * `logic` - Application logic implementing MvuLogic
184 /// * `renderer` - Platform rendering implementation for rendering Props
185 /// * `spawner` - Spawner to execute async effects on your chosen runtime
186 ///
187 /// # Example
188 ///
189 /// ```rust,no_run
190 /// # use oxide_mvu::{Emitter, Effect, MvuLogic, MvuRuntime, Renderer};
191 /// # #[derive(Clone)] enum Event {}
192 /// # #[derive(Clone)] struct Model;
193 /// # struct Props;
194 /// # struct MyLogic;
195 /// # impl MvuLogic<Event, Model, Props> for MyLogic {
196 /// # fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
197 /// # fn update(&self, _: Event, model: &Model) -> (Model, Effect<Event>) { (model.clone(), Effect::none()) }
198 /// # fn view(&self, _: &Model, _: &Emitter<Event>) -> Props { Props }
199 /// # }
200 /// # struct MyRenderer;
201 /// # impl Renderer<Props> for MyRenderer { fn render(&mut self, _: Props) {} }
202 /// // For memory-constrained embedded systems
203 /// let runtime = MvuRuntime::builder(Model, MyLogic, MyRenderer, |_| {})
204 /// .capacity(8)
205 /// .build();
206 /// ```
207 pub fn builder(
208 init_model: Model,
209 logic: Logic,
210 renderer: Render,
211 spawner: Spawn,
212 ) -> MvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn> {
213 MvuRuntimeBuilder {
214 init_model,
215 logic,
216 renderer,
217 spawner,
218 capacity: DEFAULT_EVENT_CAPACITY,
219 _event: core::marker::PhantomData,
220 _props: core::marker::PhantomData,
221 }
222 }
223
224 /// Initialize the runtime and run the event processing loop.
225 ///
226 /// - Uses the MvuLogic::init function to create and enqueue initial side effects.
227 /// - Reduces the initial Model provided at construction to Props via MvuLogic::view.
228 /// - Renders the initial Props.
229 /// - Processes events from the channel in a loop.
230 ///
231 /// This is an async function that runs the event loop. You can spawn it on your
232 /// chosen runtime using the spawner, or await it directly.
233 ///
234 /// Events can be emitted from any thread via the Emitter, but are always processed
235 /// sequentially on the thread where this future is awaited/polled.
236 pub async fn run(mut self) {
237 let (init_model, init_effect) = self.logic.init(self.model.clone());
238
239 let initial_props = {
240 let emitter = &self.emitter;
241 self.logic.view(&init_model, emitter)
242 };
243
244 self.renderer.render(initial_props);
245
246 // Execute initial effect by spawning it
247 Self::spawn_effect(&self.spawner, &self.emitter, init_effect);
248
249 // Event processing loop
250 while let Ok(event) = self.event_receiver.recv().await {
251 self.step(event);
252 }
253 }
254
255 fn step(&mut self, event: Event) {
256 // Update model with event
257 let (new_model, effect) = self.logic.update(event, &self.model);
258
259 // Reduce to props and render
260 let props = self.logic.view(&new_model, &self.emitter);
261 self.renderer.render(props);
262
263 // Update model
264 self.model = new_model;
265
266 // Execute the effect
267 Self::spawn_effect(&self.spawner, &self.emitter, effect);
268 }
269
270 pub fn spawn_effect(spawner: &Spawn, emitter: &Emitter<Event>, effect: Effect<Event>) {
271 match effect {
272 Effect::None => {}
273 Effect::Just(event) => {
274 let emitter = emitter.clone();
275 spawner.spawn(Box::pin(async move { emitter.emit(event).await }));
276 }
277 Effect::Async(boxed_future) => spawner.spawn(boxed_future.call_box(emitter)),
278 Effect::Batch(effects) => {
279 for effect in effects {
280 Self::spawn_effect(spawner, emitter, effect);
281 }
282 }
283 }
284 }
285}
286
287#[cfg(any(test, feature = "testing"))]
288/// Test spawner function that executes futures synchronously.
289///
290/// This blocks on the future immediately rather than spawning it on an async runtime.
291pub fn test_spawner_fn(fut: Pin<Box<dyn Future<Output = ()> + Send>>) {
292 // Execute the future synchronously for deterministic testing
293 futures::executor::block_on(fut);
294}
295
296#[cfg(any(test, feature = "testing"))]
297/// Creates a test spawner that executes futures synchronously.
298///
299/// This is useful for testing - it blocks on the future immediately rather
300/// than spawning it on an async runtime. Use this with [`TestMvuRuntime`]
301/// or [`MvuRuntime`] in test scenarios.
302///
303/// Returns a function pointer that can be passed directly to runtime constructors
304/// without heap allocation.
305pub fn create_test_spawner() -> fn(Pin<Box<dyn Future<Output = ()> + Send>>) {
306 test_spawner_fn
307}
308
309#[cfg(any(test, feature = "testing"))]
310/// Test runtime driver for manual event processing control.
311///
312/// Only available with the `testing` feature or during tests.
313///
314/// Returned by [`TestMvuRuntime::run`]. Provides methods to manually
315/// emit events and process the event queue for precise control in tests.
316///
317/// See [`TestMvuRuntime`] for usage.
318pub struct TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
319where
320 Event: EventTrait,
321 Model: Clone + 'static,
322 Props: 'static,
323 Logic: MvuLogic<Event, Model, Props>,
324 Render: Renderer<Props>,
325 Spawn: Spawner,
326{
327 _runtime: TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
328}
329
330#[cfg(any(test, feature = "testing"))]
331impl<Event, Model, Props, Logic, Render, Spawn>
332 TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
333where
334 Event: EventTrait,
335 Model: Clone + 'static,
336 Props: 'static,
337 Logic: MvuLogic<Event, Model, Props>,
338 Render: Renderer<Props>,
339 Spawn: Spawner,
340{
341 /// Process all queued events.
342 ///
343 /// This processes events until the queue is empty. Call this after emitting
344 /// events to drive the event loop in tests.
345 pub fn process_events(&mut self) {
346 self._runtime.process_queued_events();
347 }
348}
349
350#[cfg(any(test, feature = "testing"))]
351/// Test runtime for MVU with manual event processing control.
352///
353/// Only available with the `testing` feature or during tests.
354///
355/// Unlike [`MvuRuntime`], this runtime does not automatically
356/// process events when they are emitted. Instead, tests must manually call
357/// [`TestMvuDriver::process_events`] on the returned driver
358/// to process the event queue.
359///
360/// This provides precise control over event timing in tests.
361///
362/// ```rust
363/// use oxide_mvu::{Emitter, Effect, Renderer, MvuLogic, TestMvuRuntime};
364/// # #[derive(Clone)]
365/// # enum Event { Increment }
366/// # #[derive(Clone)]
367/// # struct Model { count: i32 }
368/// # struct Props { count: i32, on_click: Box<dyn Fn()> }
369/// # struct MyApp;
370/// # impl MvuLogic<Event, Model, Props> for MyApp {
371/// # fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
372/// # fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
373/// # (Model { count: model.count + 1 }, Effect::none())
374/// # }
375/// # fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
376/// # let e = emitter.clone();
377/// # Props { count: model.count, on_click: Box::new(move || { e.try_emit(Event::Increment); }) }
378/// # }
379/// # }
380/// # struct TestRenderer;
381/// # impl Renderer<Props> for TestRenderer { fn render(&mut self, _props: Props) {} }
382/// use oxide_mvu::create_test_spawner;
383///
384/// let runtime = TestMvuRuntime::builder(
385/// Model { count: 0 },
386/// MyApp,
387/// TestRenderer,
388/// create_test_spawner()
389/// ).build();
390/// let mut driver = runtime.run();
391/// driver.process_events(); // Manually process events
392/// ```
393pub struct TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
394where
395 Event: EventTrait,
396 Model: Clone + 'static,
397 Props: 'static,
398 Logic: MvuLogic<Event, Model, Props>,
399 Render: Renderer<Props>,
400 Spawn: Spawner,
401{
402 runtime: MvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
403}
404
405#[cfg(any(test, feature = "testing"))]
406/// Builder for configuring and constructing a [`TestMvuRuntime`].
407///
408/// Created via [`TestMvuRuntime::builder`]. Allows customizing runtime parameters
409/// like event buffer capacity before building the test runtime.
410pub struct TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
411where
412 Event: EventTrait,
413 Model: Clone + 'static,
414 Props: 'static,
415 Logic: MvuLogic<Event, Model, Props>,
416 Render: Renderer<Props>,
417 Spawn: Spawner,
418{
419 init_model: Model,
420 logic: Logic,
421 renderer: Render,
422 spawner: Spawn,
423 capacity: usize,
424 _event: core::marker::PhantomData<Event>,
425 _props: core::marker::PhantomData<Props>,
426}
427
428#[cfg(any(test, feature = "testing"))]
429impl<Event, Model, Props, Logic, Render, Spawn>
430 TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
431where
432 Event: EventTrait,
433 Model: Clone + 'static,
434 Props: 'static,
435 Logic: MvuLogic<Event, Model, Props>,
436 Render: Renderer<Props>,
437 Spawn: Spawner,
438{
439 /// Set the event buffer capacity.
440 ///
441 /// This bounds the number of events that can be queued before
442 /// [`Emitter::try_emit`] returns `false`.
443 ///
444 /// Defaults to [`DEFAULT_EVENT_CAPACITY`].
445 pub fn capacity(mut self, capacity: usize) -> Self {
446 self.capacity = capacity;
447 self
448 }
449
450 /// Build the test runtime with the configured settings.
451 pub fn build(self) -> TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn> {
452 let (event_sender, event_receiver) = bounded(self.capacity);
453
454 TestMvuRuntime {
455 runtime: MvuRuntime {
456 logic: self.logic,
457 renderer: self.renderer,
458 event_receiver,
459 model: self.init_model,
460 emitter: Emitter::new(event_sender),
461 spawner: self.spawner,
462 _props: core::marker::PhantomData,
463 },
464 }
465 }
466}
467
468#[cfg(any(test, feature = "testing"))]
469impl<Event, Model, Props, Logic, Render, Spawn>
470 TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
471where
472 Event: EventTrait,
473 Model: Clone + 'static,
474 Props: 'static,
475 Logic: MvuLogic<Event, Model, Props>,
476 Render: Renderer<Props>,
477 Spawn: Spawner,
478{
479 /// Create a builder for configuring the test runtime.
480 ///
481 /// # Arguments
482 ///
483 /// * `init_model` - The initial state
484 /// * `logic` - Application logic implementing MvuLogic
485 /// * `renderer` - Platform rendering implementation for rendering Props
486 /// * `spawner` - Spawner to execute async effects on your chosen runtime
487 pub fn builder(
488 init_model: Model,
489 logic: Logic,
490 renderer: Render,
491 spawner: Spawn,
492 ) -> TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn> {
493 TestMvuRuntimeBuilder {
494 init_model,
495 logic,
496 renderer,
497 spawner,
498 capacity: DEFAULT_EVENT_CAPACITY,
499 _event: core::marker::PhantomData,
500 _props: core::marker::PhantomData,
501 }
502 }
503
504 /// Initializes the runtime and returns a driver for manual event processing.
505 ///
506 /// This processes initial effects and renders the initial state, then returns
507 /// a [`TestMvuDriver`] that provides manual control over event processing.
508 pub fn run(mut self) -> TestMvuDriver<Event, Model, Props, Logic, Render, Spawn> {
509 let (init_model, init_effect) = self.runtime.logic.init(self.runtime.model.clone());
510
511 let initial_props = { self.runtime.logic.view(&init_model, &self.runtime.emitter) };
512
513 self.runtime.renderer.render(initial_props);
514
515 // Execute initial effect by spawning it
516 MvuRuntime::<Event, Model, Props, Logic, Render, Spawn>::spawn_effect(
517 &self.runtime.spawner,
518 &self.runtime.emitter,
519 init_effect,
520 );
521
522 TestMvuDriver { _runtime: self }
523 }
524
525 /// Process all queued events (for testing).
526 ///
527 /// This is exposed for TestMvuRuntime to manually drive event processing.
528 fn process_queued_events(&mut self) {
529 while let Ok(event) = self.runtime.event_receiver.try_recv() {
530 self.runtime.step(event);
531 }
532 }
533}