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