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}