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}