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 a [`crate::TestRenderer`].
136///
137/// See the [crate-level documentation](crate) for a complete example.
138///
139/// # Type Parameters
140///
141/// * `Event` - The event type for your application
142/// * `Model` - The model/state type for your application
143/// * `Props` - The props type produced by the view function
144/// * `Logic` - The logic implementation type (implements [`MvuLogic`])
145/// * `Render` - The renderer implementation type (implements [`Renderer`])
146/// * `Spawn` - The spawner implementation type (implements [`Spawner`])
147pub struct MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
148where
149    Event: EventTrait,
150    Model: Clone,
151    Logic: MvuLogic<Event, Model, Props>,
152    Render: Renderer<Props>,
153    Spawn: Spawner,
154{
155    logic: Logic,
156    renderer: Render,
157    event_receiver: Receiver<Event>,
158    model: Model,
159    emitter: Emitter<Event>,
160    spawner: Spawn,
161    _props: core::marker::PhantomData<Props>,
162}
163
164impl<Event, Model, Props, Logic, Render, Spawn>
165    MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
166where
167    Event: EventTrait,
168    Model: Clone + 'static,
169    Props: 'static,
170    Logic: MvuLogic<Event, Model, Props>,
171    Render: Renderer<Props>,
172    Spawn: Spawner,
173{
174    /// Create a builder for configuring the runtime.
175    ///
176    /// Use this when you need to customize runtime parameters like event buffer capacity.
177    ///
178    /// # Arguments
179    ///
180    /// * `init_model` - The initial state
181    /// * `logic` - Application logic implementing MvuLogic
182    /// * `renderer` - Platform rendering implementation for rendering Props
183    /// * `spawner` - Spawner to execute async effects on your chosen runtime
184    ///
185    /// # Example
186    ///
187    /// ```rust,no_run
188    /// # use oxide_mvu::{Emitter, Effect, MvuLogic, MvuRuntime, Renderer};
189    /// # #[derive(Clone)] enum Event {}
190    /// # #[derive(Clone)] struct Model;
191    /// # struct Props;
192    /// # struct MyLogic;
193    /// # impl MvuLogic<Event, Model, Props> for MyLogic {
194    /// #     fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
195    /// #     fn update(&self, _: Event, model: &Model) -> (Model, Effect<Event>) { (model.clone(), Effect::none()) }
196    /// #     fn view(&self, _: &Model, _: &Emitter<Event>) -> Props { Props }
197    /// # }
198    /// # struct MyRenderer;
199    /// # impl Renderer<Props> for MyRenderer { fn render(&mut self, _: Props) {} }
200    /// // For memory-constrained embedded systems
201    /// let runtime = MvuRuntime::builder(Model, MyLogic, MyRenderer, |_| {})
202    ///     .capacity(8)
203    ///     .build();
204    /// ```
205    pub fn builder(
206        init_model: Model,
207        logic: Logic,
208        renderer: Render,
209        spawner: Spawn,
210    ) -> MvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn> {
211        MvuRuntimeBuilder {
212            init_model,
213            logic,
214            renderer,
215            spawner,
216            capacity: DEFAULT_EVENT_CAPACITY,
217            _event: core::marker::PhantomData,
218            _props: core::marker::PhantomData,
219        }
220    }
221
222    /// Initialize the runtime and run the event processing loop.
223    ///
224    /// - Uses the MvuLogic::init function to create and enqueue initial side effects.
225    /// - Reduces the initial Model provided at construction to Props via MvuLogic::view.
226    /// - Renders the initial Props.
227    /// - Processes events from the channel in a loop.
228    ///
229    /// This is an async function that runs the event loop. You can spawn it on your
230    /// chosen runtime using the spawner, or await it directly.
231    ///
232    /// Events can be emitted from any thread via the Emitter, but are always processed
233    /// sequentially on the thread where this future is awaited/polled.
234    pub async fn run(mut self) {
235        let (init_model, init_effect) = self.logic.init(self.model.clone());
236
237        let initial_props = {
238            let emitter = &self.emitter;
239            self.logic.view(&init_model, emitter)
240        };
241
242        self.renderer.render(initial_props);
243
244        // Execute initial effect by spawning it
245        let emitter = self.emitter.clone();
246        let future = init_effect.execute(&emitter);
247        self.spawner.spawn(Box::pin(future));
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        let emitter = self.emitter.clone();
268        let future = effect.execute(&emitter);
269        self.spawner.spawn(Box::pin(future));
270    }
271}
272
273#[cfg(any(test, feature = "testing"))]
274/// Test spawner function that executes futures synchronously.
275///
276/// This blocks on the future immediately rather than spawning it on an async runtime.
277pub fn test_spawner_fn(fut: Pin<Box<dyn Future<Output = ()> + Send>>) {
278    // Execute the future synchronously for deterministic testing
279    futures::executor::block_on(fut);
280}
281
282#[cfg(any(test, feature = "testing"))]
283/// Creates a test spawner that executes futures synchronously.
284///
285/// This is useful for testing - it blocks on the future immediately rather
286/// than spawning it on an async runtime. Use this with [`TestMvuRuntime`]
287/// or [`MvuRuntime`] in test scenarios.
288///
289/// Returns a function pointer that can be passed directly to runtime constructors
290/// without heap allocation.
291pub fn create_test_spawner() -> fn(Pin<Box<dyn Future<Output = ()> + Send>>) {
292    test_spawner_fn
293}
294
295#[cfg(any(test, feature = "testing"))]
296/// Test runtime driver for manual event processing control.
297///
298/// Only available with the `testing` feature or during tests.
299///
300/// Returned by [`TestMvuRuntime::run`]. Provides methods to manually
301/// emit events and process the event queue for precise control in tests.
302///
303/// See [`TestMvuRuntime`] for usage.
304pub struct TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
305where
306    Event: EventTrait,
307    Model: Clone + 'static,
308    Props: 'static,
309    Logic: MvuLogic<Event, Model, Props>,
310    Render: Renderer<Props>,
311    Spawn: Spawner,
312{
313    _runtime: TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
314}
315
316#[cfg(any(test, feature = "testing"))]
317impl<Event, Model, Props, Logic, Render, Spawn>
318    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    /// Process all queued events.
328    ///
329    /// This processes events until the queue is empty. Call this after emitting
330    /// events to drive the event loop in tests.
331    pub fn process_events(&mut self) {
332        self._runtime.process_queued_events();
333    }
334}
335
336#[cfg(any(test, feature = "testing"))]
337/// Test runtime for MVU with manual event processing control.
338///
339/// Only available with the `testing` feature or during tests.
340///
341/// Unlike [`MvuRuntime`], this runtime does not automatically
342/// process events when they are emitted. Instead, tests must manually call
343/// [`process_events`](TestMvuDriver::process_events) on the returned driver
344/// to process the event queue.
345///
346/// This provides precise control over event timing in tests.
347///
348/// ```rust
349/// use oxide_mvu::{Emitter, Effect, Renderer, MvuLogic, TestMvuRuntime};
350/// # #[derive(Clone)]
351/// # enum Event { Increment }
352/// # #[derive(Clone)]
353/// # struct Model { count: i32 }
354/// # struct Props { count: i32, on_click: Box<dyn Fn()> }
355/// # struct MyApp;
356/// # impl MvuLogic<Event, Model, Props> for MyApp {
357/// #     fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
358/// #     fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
359/// #         (Model { count: model.count + 1 }, Effect::none())
360/// #     }
361/// #     fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
362/// #         let e = emitter.clone();
363/// #         Props { count: model.count, on_click: Box::new(move || { e.try_emit(Event::Increment); }) }
364/// #     }
365/// # }
366/// # struct TestRenderer;
367/// # impl Renderer<Props> for TestRenderer { fn render(&mut self, _props: Props) {} }
368/// use oxide_mvu::create_test_spawner;
369///
370/// let runtime = TestMvuRuntime::builder(
371///     Model { count: 0 },
372///     MyApp,
373///     TestRenderer,
374///     create_test_spawner()
375/// ).build();
376/// let mut driver = runtime.run();
377/// driver.process_events(); // Manually process events
378/// ```
379pub struct TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
380where
381    Event: EventTrait,
382    Model: Clone + 'static,
383    Props: 'static,
384    Logic: MvuLogic<Event, Model, Props>,
385    Render: Renderer<Props>,
386    Spawn: Spawner,
387{
388    runtime: MvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
389}
390
391#[cfg(any(test, feature = "testing"))]
392/// Builder for configuring and constructing a [`TestMvuRuntime`].
393///
394/// Created via [`TestMvuRuntime::builder`]. Allows customizing runtime parameters
395/// like event buffer capacity before building the test runtime.
396pub struct TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
397where
398    Event: EventTrait,
399    Model: Clone + 'static,
400    Props: 'static,
401    Logic: MvuLogic<Event, Model, Props>,
402    Render: Renderer<Props>,
403    Spawn: Spawner,
404{
405    init_model: Model,
406    logic: Logic,
407    renderer: Render,
408    spawner: Spawn,
409    capacity: usize,
410    _event: core::marker::PhantomData<Event>,
411    _props: core::marker::PhantomData<Props>,
412}
413
414#[cfg(any(test, feature = "testing"))]
415impl<Event, Model, Props, Logic, Render, Spawn>
416    TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
417where
418    Event: EventTrait,
419    Model: Clone + 'static,
420    Props: 'static,
421    Logic: MvuLogic<Event, Model, Props>,
422    Render: Renderer<Props>,
423    Spawn: Spawner,
424{
425    /// Set the event buffer capacity.
426    ///
427    /// This bounds the number of events that can be queued before
428    /// [`Emitter::try_emit`](crate::Emitter::try_emit) returns `false`.
429    ///
430    /// Defaults to [`DEFAULT_EVENT_CAPACITY`].
431    pub fn capacity(mut self, capacity: usize) -> Self {
432        self.capacity = capacity;
433        self
434    }
435
436    /// Build the test runtime with the configured settings.
437    pub fn build(self) -> TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn> {
438        let (event_sender, event_receiver) = bounded(self.capacity);
439
440        TestMvuRuntime {
441            runtime: MvuRuntime {
442                logic: self.logic,
443                renderer: self.renderer,
444                event_receiver,
445                model: self.init_model,
446                emitter: Emitter::new(event_sender),
447                spawner: self.spawner,
448                _props: core::marker::PhantomData,
449            },
450        }
451    }
452}
453
454#[cfg(any(test, feature = "testing"))]
455impl<Event, Model, Props, Logic, Render, Spawn>
456    TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
457where
458    Event: EventTrait,
459    Model: Clone + 'static,
460    Props: 'static,
461    Logic: MvuLogic<Event, Model, Props>,
462    Render: Renderer<Props>,
463    Spawn: Spawner,
464{
465    /// Create a builder for configuring the test runtime.
466    ///
467    /// # Arguments
468    ///
469    /// * `init_model` - The initial state
470    /// * `logic` - Application logic implementing MvuLogic
471    /// * `renderer` - Platform rendering implementation for rendering Props
472    /// * `spawner` - Spawner to execute async effects on your chosen runtime
473    pub fn builder(
474        init_model: Model,
475        logic: Logic,
476        renderer: Render,
477        spawner: Spawn,
478    ) -> TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn> {
479        TestMvuRuntimeBuilder {
480            init_model,
481            logic,
482            renderer,
483            spawner,
484            capacity: DEFAULT_EVENT_CAPACITY,
485            _event: core::marker::PhantomData,
486            _props: core::marker::PhantomData,
487        }
488    }
489
490    /// Initializes the runtime and returns a driver for manual event processing.
491    ///
492    /// This processes initial effects and renders the initial state, then returns
493    /// a [`TestMvuDriver`] that provides manual control over event processing.
494    pub fn run(mut self) -> TestMvuDriver<Event, Model, Props, Logic, Render, Spawn> {
495        let (init_model, init_effect) = self.runtime.logic.init(self.runtime.model.clone());
496
497        let initial_props = { self.runtime.logic.view(&init_model, &self.runtime.emitter) };
498
499        self.runtime.renderer.render(initial_props);
500
501        // Execute initial effect by spawning it
502        let future = init_effect.execute(&self.runtime.emitter);
503        self.runtime.spawner.spawn(Box::pin(future));
504
505        TestMvuDriver { _runtime: self }
506    }
507
508    /// Process all queued events (for testing).
509    ///
510    /// This is exposed for TestMvuRuntime to manually drive event processing.
511    fn process_queued_events(&mut self) {
512        while let Ok(event) = self.runtime.event_receiver.try_recv() {
513            self.runtime.step(event);
514        }
515    }
516}