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 thingbuf::mpsc::{channel, 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 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    // Note: Events are wrapped in Option to satisfy thingbuf's Default requirement.
77    // The channel uses Option::default() (None) for recycling empty slots.
78    event_receiver: Receiver<Option<Event>>,
79    model: Model,
80    emitter: Emitter<Event>,
81    spawner: Spawn,
82    _props: core::marker::PhantomData<Props>,
83}
84
85impl<Event, Model, Props, Logic, Render, Spawn>
86    MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
87where
88    Event: EventTrait,
89    Model: Clone + 'static,
90    Props: 'static,
91    Logic: MvuLogic<Event, Model, Props>,
92    Render: Renderer<Props>,
93    Spawn: Spawner,
94{
95    /// Create a new runtime.
96    ///
97    /// The runtime will not be started until MvuRuntime::run is called.
98    ///
99    /// # Arguments
100    ///
101    /// * `init_model` - The initial state
102    /// * `logic` - Application logic implementing MvuLogic
103    /// * `renderer` - Platform rendering implementation for rendering Props
104    /// * `spawner` - Spawner to execute async effects on your chosen runtime
105    pub fn new(init_model: Model, logic: Logic, renderer: Render, spawner: Spawn) -> Self {
106        let (event_sender, event_receiver) = channel(DEFAULT_EVENT_CAPACITY);
107        let emitter = Emitter::new(event_sender);
108
109        MvuRuntime {
110            logic,
111            renderer,
112            event_receiver,
113            model: init_model,
114            emitter,
115            spawner,
116            _props: core::marker::PhantomData,
117        }
118    }
119
120    /// Initialize the runtime and run the event processing loop.
121    ///
122    /// - Uses the MvuLogic::init function to create and enqueue initial side effects.
123    /// - Reduces the initial Model provided at construction to Props via MvuLogic::view.
124    /// - Renders the initial Props.
125    /// - Processes events from the channel in a loop.
126    ///
127    /// This is an async function that runs the event loop. You can spawn it on your
128    /// chosen runtime using the spawner, or await it directly.
129    ///
130    /// Events can be emitted from any thread via the Emitter, but are always processed
131    /// sequentially on the thread where this future is awaited/polled.
132    pub async fn run(mut self) {
133        let (init_model, init_effect) = self.logic.init(self.model.clone());
134
135        let initial_props = {
136            let emitter = &self.emitter;
137            self.logic.view(&init_model, emitter)
138        };
139
140        self.renderer.render(initial_props);
141
142        // Execute initial effect by spawning it
143        let emitter = self.emitter.clone();
144        let future = init_effect.execute(&emitter);
145        self.spawner.spawn(Box::pin(future));
146
147        // Event processing loop
148        while let Some(Some(event)) = self.event_receiver.recv().await {
149            self.step(event)
150        }
151    }
152
153    fn step(&mut self, event: Event) {
154        // Update model with event
155        let (new_model, effect) = self.logic.update(event, &self.model);
156
157        // Reduce to props and render
158        let props = self.logic.view(&new_model, &self.emitter);
159        self.renderer.render(props);
160
161        // Update model
162        self.model = new_model;
163
164        // Execute the effect
165        let emitter = self.emitter.clone();
166        let future = effect.execute(&emitter);
167        self.spawner.spawn(Box::pin(future));
168    }
169}
170
171#[cfg(any(test, feature = "testing"))]
172/// Test spawner function that executes futures synchronously.
173///
174/// This blocks on the future immediately rather than spawning it on an async runtime.
175pub fn test_spawner_fn(fut: Pin<Box<dyn Future<Output = ()> + Send>>) {
176    // Execute the future synchronously for deterministic testing
177    futures::executor::block_on(fut);
178}
179
180#[cfg(any(test, feature = "testing"))]
181/// Creates a test spawner that executes futures synchronously.
182///
183/// This is useful for testing - it blocks on the future immediately rather
184/// than spawning it on an async runtime. Use this with [`TestMvuRuntime`]
185/// or [`MvuRuntime`] in test scenarios.
186///
187/// Returns a function pointer that can be passed directly to runtime constructors
188/// without heap allocation.
189pub fn create_test_spawner() -> fn(Pin<Box<dyn Future<Output = ()> + Send>>) {
190    test_spawner_fn
191}
192
193#[cfg(any(test, feature = "testing"))]
194/// Test runtime driver for manual event processing control.
195///
196/// Only available with the `testing` feature or during tests.
197///
198/// Returned by [`TestMvuRuntime::run`]. Provides methods to manually
199/// emit events and process the event queue for precise control in tests.
200///
201/// See [`TestMvuRuntime`] for usage.
202pub struct TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
203where
204    Event: EventTrait,
205    Model: Clone + 'static,
206    Props: 'static,
207    Logic: MvuLogic<Event, Model, Props>,
208    Render: Renderer<Props>,
209    Spawn: Spawner,
210{
211    _runtime: TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
212}
213
214#[cfg(any(test, feature = "testing"))]
215impl<Event, Model, Props, Logic, Render, Spawn>
216    TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
217where
218    Event: EventTrait,
219    Model: Clone + 'static,
220    Props: 'static,
221    Logic: MvuLogic<Event, Model, Props>,
222    Render: Renderer<Props>,
223    Spawn: Spawner,
224{
225    /// Process all queued events.
226    ///
227    /// This processes events until the queue is empty. Call this after emitting
228    /// events to drive the event loop in tests.
229    pub fn process_events(&mut self) {
230        self._runtime.process_queued_events();
231    }
232}
233
234#[cfg(any(test, feature = "testing"))]
235/// Test runtime for MVU with manual event processing control.
236///
237/// Only available with the `testing` feature or during tests.
238///
239/// Unlike [`MvuRuntime`], this runtime does not automatically
240/// process events when they are emitted. Instead, tests must manually call
241/// [`process_events`](TestMvuDriver::process_events) on the returned driver
242/// to process the event queue.
243///
244/// This provides precise control over event timing in tests.
245///
246/// ```rust
247/// use oxide_mvu::{Emitter, Effect, Renderer, MvuLogic, TestMvuRuntime};
248/// # #[derive(Clone)]
249/// # enum Event { Increment }
250/// # #[derive(Clone)]
251/// # struct Model { count: i32 }
252/// # struct Props { count: i32, on_click: Box<dyn Fn()> }
253/// # struct MyApp;
254/// # impl MvuLogic<Event, Model, Props> for MyApp {
255/// #     fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
256/// #     fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
257/// #         (Model { count: model.count + 1 }, Effect::none())
258/// #     }
259/// #     fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
260/// #         let e = emitter.clone();
261/// #         Props { count: model.count, on_click: Box::new(move || { e.try_emit(Event::Increment); }) }
262/// #     }
263/// # }
264/// # struct TestRenderer;
265/// # impl Renderer<Props> for TestRenderer { fn render(&mut self, _props: Props) {} }
266/// use oxide_mvu::create_test_spawner;
267///
268/// let runtime = TestMvuRuntime::new(
269///     Model { count: 0 },
270///     MyApp,
271///     TestRenderer,
272///     create_test_spawner()
273/// );
274/// let mut driver = runtime.run();
275/// driver.process_events(); // Manually process events
276/// ```
277pub struct TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
278where
279    Event: EventTrait,
280    Model: Clone + 'static,
281    Props: 'static,
282    Logic: MvuLogic<Event, Model, Props>,
283    Render: Renderer<Props>,
284    Spawn: Spawner,
285{
286    runtime: MvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
287}
288
289#[cfg(any(test, feature = "testing"))]
290impl<Event, Model, Props, Logic, Render, Spawn>
291    TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
292where
293    Event: EventTrait,
294    Model: Clone + 'static,
295    Props: 'static,
296    Logic: MvuLogic<Event, Model, Props>,
297    Render: Renderer<Props>,
298    Spawn: Spawner,
299{
300    /// Create a new test runtime.
301    ///
302    /// Creates an emitter that enqueues events without automatically processing them.
303    ///
304    /// # Arguments
305    ///
306    /// * `init_model` - The initial state
307    /// * `logic` - Application logic implementing MvuLogic
308    /// * `renderer` - Platform rendering implementation for rendering Props
309    /// * `spawner` - Spawner to execute async effects on your chosen runtime
310    pub fn new(init_model: Model, logic: Logic, renderer: Render, spawner: Spawn) -> Self {
311        // Create bounded channel for event queue
312        let (event_sender, event_receiver) = channel(DEFAULT_EVENT_CAPACITY);
313
314        TestMvuRuntime {
315            runtime: MvuRuntime {
316                logic,
317                renderer,
318                event_receiver,
319                model: init_model,
320                emitter: Emitter::new(event_sender),
321                spawner,
322                _props: core::marker::PhantomData,
323            },
324        }
325    }
326
327    /// Initializes the runtime and returns a driver for manual event processing.
328    ///
329    /// This processes initial effects and renders the initial state, then returns
330    /// a [`TestMvuDriver`] that provides manual control over event processing.
331    pub fn run(mut self) -> TestMvuDriver<Event, Model, Props, Logic, Render, Spawn> {
332        let (init_model, init_effect) = self.runtime.logic.init(self.runtime.model.clone());
333
334        let initial_props = { self.runtime.logic.view(&init_model, &self.runtime.emitter) };
335
336        self.runtime.renderer.render(initial_props);
337
338        // Execute initial effect by spawning it
339        let future = init_effect.execute(&self.runtime.emitter);
340        self.runtime.spawner.spawn(Box::pin(future));
341
342        TestMvuDriver { _runtime: self }
343    }
344
345    /// Process all queued events (for testing).
346    ///
347    /// This is exposed for TestMvuRuntime to manually drive event processing.
348    fn process_queued_events(&mut self) {
349        while let Ok(Some(event)) = self.runtime.event_receiver.try_recv() {
350            self.runtime.step(event);
351        }
352    }
353}