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 flume::Receiver;
10
11use crate::{Emitter, MvuLogic, Renderer};
12
13/// A spawner trait for executing futures on an async runtime.
14///
15/// This abstraction allows you to use whatever concurrency model you want (tokio, async-std, embassy, etc.).
16///
17/// Function pointers and closures automatically implement this trait via the blanket implementation.
18pub trait Spawner {
19    /// Spawn a future on the async runtime.
20    fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>);
21}
22
23/// Implement Spawner for any callable type that matches the signature.
24///
25/// This includes function pointers, closures, and function items.
26impl<F> Spawner for F
27where
28    F: Fn(Pin<Box<dyn Future<Output = ()> + Send>>),
29{
30    fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
31        self(future)
32    }
33}
34
35/// The MVU runtime that orchestrates the event loop.
36///
37/// This is the core of the framework. It:
38/// 1. Initializes the Model and initial Effects via [`MvuLogic::init`]
39/// 2. Processes events through [`MvuLogic::update`]
40/// 3. Reduces the Model to Props via [`MvuLogic::view`]
41/// 4. Delivers Props to the [`Renderer`] for rendering
42///
43/// The runtime creates a single [`Emitter`] that can send events from any thread.
44/// Events are queued via a lock-free channel and processed on the thread where
45/// [`MvuRuntime::run`] was called.
46///
47/// For testing with manual control, use [`TestMvuRuntime`] with a [`crate::TestRenderer`].
48///
49/// See the [crate-level documentation](crate) for a complete example.
50///
51/// # Type Parameters
52///
53/// * `Event` - The event type for your application
54/// * `Model` - The model/state type for your application
55/// * `Props` - The props type produced by the view function
56/// * `Logic` - The logic implementation type (implements [`MvuLogic`])
57/// * `Render` - The renderer implementation type (implements [`Renderer`])
58/// * `Spawn` - The spawner implementation type (implements [`Spawner`])
59pub struct MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
60where
61    Event: Send,
62    Model: Clone,
63    Logic: MvuLogic<Event, Model, Props>,
64    Render: Renderer<Props>,
65    Spawn: Spawner,
66{
67    logic: Logic,
68    renderer: Render,
69    event_receiver: Receiver<Event>,
70    model: Model,
71    emitter: Emitter<Event>,
72    spawner: Spawn,
73    _props: core::marker::PhantomData<Props>,
74}
75
76impl<Event, Model, Props, Logic, Render, Spawn>
77    MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
78where
79    Event: Send + 'static,
80    Model: Clone + 'static,
81    Props: 'static,
82    Logic: MvuLogic<Event, Model, Props>,
83    Render: Renderer<Props>,
84    Spawn: Spawner,
85{
86    /// Create a new runtime.
87    ///
88    /// The runtime will not be started until MvuRuntime::run is called.
89    ///
90    /// # Arguments
91    ///
92    /// * `init_model` - The initial state
93    /// * `logic` - Application logic implementing MvuLogic
94    /// * `renderer` - Platform rendering implementation for rendering Props
95    /// * `spawner` - Spawner to execute async effects on your chosen runtime
96    pub fn new(init_model: Model, logic: Logic, renderer: Render, spawner: Spawn) -> Self {
97        let (event_sender, event_receiver) = flume::unbounded();
98        let emitter = Emitter::new(event_sender);
99
100        MvuRuntime {
101            logic,
102            renderer,
103            event_receiver,
104            model: init_model,
105            emitter,
106            spawner,
107            _props: core::marker::PhantomData,
108        }
109    }
110
111    /// Initialize the runtime and run the event processing loop.
112    ///
113    /// - Uses the MvuLogic::init function to create and enqueue initial side effects.
114    /// - Reduces the initial Model provided at construction to Props via MvuLogic::view.
115    /// - Renders the initial Props.
116    /// - Processes events from the channel in a loop.
117    ///
118    /// This is an async function that runs the event loop. You can spawn it on your
119    /// chosen runtime using the spawner, or await it directly.
120    ///
121    /// Events can be emitted from any thread via the Emitter, but are always processed
122    /// sequentially on the thread where this future is awaited/polled.
123    pub async fn run(mut self) {
124        let (init_model, init_effect) = self.logic.init(self.model.clone());
125
126        let initial_props = {
127            let emitter = &self.emitter;
128            self.logic.view(&init_model, emitter)
129        };
130
131        self.renderer.render(initial_props);
132
133        // Execute initial effect by spawning it
134        let emitter = self.emitter.clone();
135        let future = init_effect.execute(&emitter);
136        self.spawner.spawn(Box::pin(future));
137
138        // Event processing loop
139        while let Ok(event) = self.event_receiver.recv_async().await {
140            self.step(event)
141        }
142    }
143
144    fn step(&mut self, event: Event) {
145        // Update model with event
146        let (new_model, effect) = self.logic.update(event, &self.model);
147
148        // Reduce to props and render
149        let props = self.logic.view(&new_model, &self.emitter);
150        self.renderer.render(props);
151
152        // Update model
153        self.model = new_model;
154
155        // Execute the effect
156        let emitter = self.emitter.clone();
157        let future = effect.execute(&emitter);
158        self.spawner.spawn(Box::pin(future));
159    }
160}
161
162#[cfg(any(test, feature = "testing"))]
163/// Test spawner function that executes futures synchronously.
164///
165/// This blocks on the future immediately rather than spawning it on an async runtime.
166pub fn test_spawner_fn(fut: Pin<Box<dyn Future<Output = ()> + Send>>) {
167    // Execute the future synchronously for deterministic testing
168    futures::executor::block_on(fut);
169}
170
171#[cfg(any(test, feature = "testing"))]
172/// Creates a test spawner that executes futures synchronously.
173///
174/// This is useful for testing - it blocks on the future immediately rather
175/// than spawning it on an async runtime. Use this with [`TestMvuRuntime`]
176/// or [`MvuRuntime`] in test scenarios.
177///
178/// Returns a function pointer that can be passed directly to runtime constructors
179/// without heap allocation.
180pub fn create_test_spawner() -> fn(Pin<Box<dyn Future<Output = ()> + Send>>) {
181    test_spawner_fn
182}
183
184#[cfg(any(test, feature = "testing"))]
185/// Test runtime driver for manual event processing control.
186///
187/// Only available with the `testing` feature or during tests.
188///
189/// Returned by [`TestMvuRuntime::run`]. Provides methods to manually
190/// emit events and process the event queue for precise control in tests.
191///
192/// See [`TestMvuRuntime`] for usage.
193pub struct TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
194where
195    Event: Send + 'static,
196    Model: Clone + 'static,
197    Props: 'static,
198    Logic: MvuLogic<Event, Model, Props>,
199    Render: Renderer<Props>,
200    Spawn: Spawner,
201{
202    _runtime: TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
203}
204
205#[cfg(any(test, feature = "testing"))]
206impl<Event, Model, Props, Logic, Render, Spawn>
207    TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
208where
209    Event: Send + 'static,
210    Model: Clone + 'static,
211    Props: 'static,
212    Logic: MvuLogic<Event, Model, Props>,
213    Render: Renderer<Props>,
214    Spawn: Spawner,
215{
216    /// Process all queued events.
217    ///
218    /// This processes events until the queue is empty. Call this after emitting
219    /// events to drive the event loop in tests.
220    pub fn process_events(&mut self) {
221        self._runtime.process_queued_events();
222    }
223}
224
225#[cfg(any(test, feature = "testing"))]
226/// Test runtime for MVU with manual event processing control.
227///
228/// Only available with the `testing` feature or during tests.
229///
230/// Unlike [`MvuRuntime`], this runtime does not automatically
231/// process events when they are emitted. Instead, tests must manually call
232/// [`process_events`](TestMvuDriver::process_events) on the returned driver
233/// to process the event queue.
234///
235/// This provides precise control over event timing in tests.
236///
237/// ```rust
238/// use oxide_mvu::{Emitter, Effect, Renderer, MvuLogic, TestMvuRuntime};
239/// # enum Event { Increment }
240/// # #[derive(Clone)]
241/// # struct Model { count: i32 }
242/// # struct Props { count: i32, on_click: Box<dyn Fn()> }
243/// # struct MyApp;
244/// # impl MvuLogic<Event, Model, Props> for MyApp {
245/// #     fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
246/// #     fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
247/// #         (Model { count: model.count + 1 }, Effect::none())
248/// #     }
249/// #     fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
250/// #         let e = emitter.clone();
251/// #         Props { count: model.count, on_click: Box::new(move || e.emit(Event::Increment)) }
252/// #     }
253/// # }
254/// # struct TestRenderer;
255/// # impl Renderer<Props> for TestRenderer { fn render(&mut self, _props: Props) {} }
256/// use oxide_mvu::create_test_spawner;
257///
258/// let runtime = TestMvuRuntime::new(
259///     Model { count: 0 },
260///     MyApp,
261///     TestRenderer,
262///     create_test_spawner()
263/// );
264/// let mut driver = runtime.run();
265/// driver.process_events(); // Manually process events
266/// ```
267pub struct TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
268where
269    Event: Send + 'static,
270    Model: Clone + 'static,
271    Props: 'static,
272    Logic: MvuLogic<Event, Model, Props>,
273    Render: Renderer<Props>,
274    Spawn: Spawner,
275{
276    runtime: MvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
277}
278
279#[cfg(any(test, feature = "testing"))]
280impl<Event, Model, Props, Logic, Render, Spawn>
281    TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
282where
283    Event: Send + 'static,
284    Model: Clone + 'static,
285    Props: 'static,
286    Logic: MvuLogic<Event, Model, Props>,
287    Render: Renderer<Props>,
288    Spawn: Spawner,
289{
290    /// Create a new test runtime.
291    ///
292    /// Creates an emitter that enqueues events without automatically processing them.
293    ///
294    /// # Arguments
295    ///
296    /// * `init_model` - The initial state
297    /// * `logic` - Application logic implementing MvuLogic
298    /// * `renderer` - Platform rendering implementation for rendering Props
299    /// * `spawner` - Spawner to execute async effects on your chosen runtime
300    pub fn new(init_model: Model, logic: Logic, renderer: Render, spawner: Spawn) -> Self {
301        // Create unbounded channel for event queue
302        let (event_sender, event_receiver) = flume::unbounded();
303
304        TestMvuRuntime {
305            runtime: MvuRuntime {
306                logic,
307                renderer,
308                event_receiver,
309                model: init_model,
310                emitter: Emitter::new(event_sender),
311                spawner,
312                _props: core::marker::PhantomData,
313            },
314        }
315    }
316
317    /// Initializes the runtime and returns a driver for manual event processing.
318    ///
319    /// This processes initial effects and renders the initial state, then returns
320    /// a [`TestMvuDriver`] that provides manual control over event processing.
321    pub fn run(mut self) -> TestMvuDriver<Event, Model, Props, Logic, Render, Spawn> {
322        let (init_model, init_effect) = self.runtime.logic.init(self.runtime.model.clone());
323
324        let initial_props = { self.runtime.logic.view(&init_model, &self.runtime.emitter) };
325
326        self.runtime.renderer.render(initial_props);
327
328        // Execute initial effect by spawning it
329        let future = init_effect.execute(&self.runtime.emitter);
330        self.runtime.spawner.spawn(Box::pin(future));
331
332        TestMvuDriver { _runtime: self }
333    }
334
335    /// Process all queued events (for testing).
336    ///
337    /// This is exposed for TestMvuRuntime to manually drive event processing.
338    fn process_queued_events(&mut self) {
339        while let Ok(event) = self.runtime.event_receiver.try_recv() {
340            self.runtime.step(event);
341        }
342    }
343}