Skip to main content

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}