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}