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/// Sized for embedded systems with limited heap. Increase this via
18/// [`MvuRuntimeBuilder::capacity`] if your application generates high-frequency bursts of events.
19pub const DEFAULT_EVENT_CAPACITY: usize = 32;
20
21/// Builder for configuring and constructing an [`MvuRuntime`].
22///
23/// Created via [`MvuRuntime::builder`]. Allows customizing runtime parameters
24/// like event buffer capacity before building the runtime.
25///
26/// # Example
27///
28/// ```rust,no_run
29/// # use oxide_mvu::{Emitter, Effect, MvuLogic, MvuRuntime, Renderer};
30/// # #[derive(Clone)] enum Event {}
31/// # #[derive(Clone)] struct Model;
32/// # struct Props;
33/// # struct MyLogic;
34/// # impl MvuLogic<Event, Model, Props> for MyLogic {
35/// # fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
36/// # fn update(&self, _: Event, model: &Model) -> (Model, Effect<Event>) { (model.clone(), Effect::none()) }
37/// # fn view(&self, _: &Model, _: &Emitter<Event>) -> Props { Props }
38/// # }
39/// # struct MyRenderer;
40/// # impl Renderer<Props> for MyRenderer { fn render(&mut self, _: Props) {} }
41/// // Use builder for custom configuration
42/// let runtime = MvuRuntime::builder(Model, MyLogic, MyRenderer, |_| {})
43/// .capacity(64)
44/// .build();
45/// ```
46pub struct MvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
47where
48 Event: EventTrait,
49 Model: Clone,
50 Logic: MvuLogic<Event, Model, Props>,
51 Render: Renderer<Props>,
52 Spawn: Spawner,
53{
54 init_model: Model,
55 logic: Logic,
56 renderer: Render,
57 spawner: Spawn,
58 capacity: usize,
59 _event: core::marker::PhantomData<Event>,
60 _props: core::marker::PhantomData<Props>,
61}
62
63impl<Event, Model, Props, Logic, Render, Spawn>
64 MvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
65where
66 Event: EventTrait,
67 Model: Clone + 'static,
68 Props: 'static,
69 Logic: MvuLogic<Event, Model, Props>,
70 Render: Renderer<Props>,
71 Spawn: Spawner,
72{
73 /// Set the event buffer capacity.
74 ///
75 /// This bounds the number of events that can be queued before
76 /// [`Emitter::try_emit`](crate::Emitter::try_emit) returns `false`.
77 ///
78 /// Defaults to [`DEFAULT_EVENT_CAPACITY`] (32).
79 pub fn capacity(mut self, capacity: usize) -> Self {
80 self.capacity = capacity;
81 self
82 }
83
84 /// Build the runtime with the configured settings.
85 pub fn build(self) -> MvuRuntime<Event, Model, Props, Logic, Render, Spawn> {
86 let (event_sender, event_receiver) = bounded(self.capacity);
87 let emitter = Emitter::new(event_sender);
88
89 MvuRuntime {
90 logic: self.logic,
91 renderer: self.renderer,
92 event_receiver,
93 model: self.init_model,
94 emitter,
95 spawner: self.spawner,
96 _props: core::marker::PhantomData,
97 }
98 }
99}
100
101/// A spawner trait for executing futures on an async runtime.
102///
103/// This abstraction allows you to use whatever concurrency model you want (tokio, async-std, embassy, etc.).
104///
105/// Function pointers and closures automatically implement this trait via the blanket implementation.
106pub trait Spawner {
107 /// Spawn a future on the async runtime.
108 fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>);
109}
110
111/// Implement Spawner for any callable type that matches the signature.
112///
113/// This includes function pointers, closures, and function items.
114impl<F> Spawner for F
115where
116 F: Fn(Pin<Box<dyn Future<Output = ()> + Send>>),
117{
118 fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
119 self(future)
120 }
121}
122
123/// The MVU runtime that orchestrates the event loop.
124///
125/// This is the core of the framework. It:
126/// 1. Initializes the Model and initial Effects via [`MvuLogic::init`]
127/// 2. Processes events through [`MvuLogic::update`]
128/// 3. Reduces the Model to Props via [`MvuLogic::view`]
129/// 4. Delivers Props to the [`Renderer`] for rendering
130///
131/// The runtime creates a single [`Emitter`] that can send events from any thread.
132/// Events are queued via a lock-free MPMC channel and processed on the thread where
133/// [`MvuRuntime::run`] was called.
134///
135/// For testing with manual control, use [`TestMvuRuntime`] with a [`crate::TestRenderer`].
136///
137/// See the [crate-level documentation](crate) for a complete example.
138///
139/// # Type Parameters
140///
141/// * `Event` - The event type for your application
142/// * `Model` - The model/state type for your application
143/// * `Props` - The props type produced by the view function
144/// * `Logic` - The logic implementation type (implements [`MvuLogic`])
145/// * `Render` - The renderer implementation type (implements [`Renderer`])
146/// * `Spawn` - The spawner implementation type (implements [`Spawner`])
147pub struct MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
148where
149 Event: EventTrait,
150 Model: Clone,
151 Logic: MvuLogic<Event, Model, Props>,
152 Render: Renderer<Props>,
153 Spawn: Spawner,
154{
155 logic: Logic,
156 renderer: Render,
157 event_receiver: Receiver<Event>,
158 model: Model,
159 emitter: Emitter<Event>,
160 spawner: Spawn,
161 _props: core::marker::PhantomData<Props>,
162}
163
164impl<Event, Model, Props, Logic, Render, Spawn>
165 MvuRuntime<Event, Model, Props, Logic, Render, Spawn>
166where
167 Event: EventTrait,
168 Model: Clone + 'static,
169 Props: 'static,
170 Logic: MvuLogic<Event, Model, Props>,
171 Render: Renderer<Props>,
172 Spawn: Spawner,
173{
174 /// Create a builder for configuring the runtime.
175 ///
176 /// Use this when you need to customize runtime parameters like event buffer capacity.
177 ///
178 /// # Arguments
179 ///
180 /// * `init_model` - The initial state
181 /// * `logic` - Application logic implementing MvuLogic
182 /// * `renderer` - Platform rendering implementation for rendering Props
183 /// * `spawner` - Spawner to execute async effects on your chosen runtime
184 ///
185 /// # Example
186 ///
187 /// ```rust,no_run
188 /// # use oxide_mvu::{Emitter, Effect, MvuLogic, MvuRuntime, Renderer};
189 /// # #[derive(Clone)] enum Event {}
190 /// # #[derive(Clone)] struct Model;
191 /// # struct Props;
192 /// # struct MyLogic;
193 /// # impl MvuLogic<Event, Model, Props> for MyLogic {
194 /// # fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
195 /// # fn update(&self, _: Event, model: &Model) -> (Model, Effect<Event>) { (model.clone(), Effect::none()) }
196 /// # fn view(&self, _: &Model, _: &Emitter<Event>) -> Props { Props }
197 /// # }
198 /// # struct MyRenderer;
199 /// # impl Renderer<Props> for MyRenderer { fn render(&mut self, _: Props) {} }
200 /// // For memory-constrained embedded systems
201 /// let runtime = MvuRuntime::builder(Model, MyLogic, MyRenderer, |_| {})
202 /// .capacity(8)
203 /// .build();
204 /// ```
205 pub fn builder(
206 init_model: Model,
207 logic: Logic,
208 renderer: Render,
209 spawner: Spawn,
210 ) -> MvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn> {
211 MvuRuntimeBuilder {
212 init_model,
213 logic,
214 renderer,
215 spawner,
216 capacity: DEFAULT_EVENT_CAPACITY,
217 _event: core::marker::PhantomData,
218 _props: core::marker::PhantomData,
219 }
220 }
221
222 /// Initialize the runtime and run the event processing loop.
223 ///
224 /// - Uses the MvuLogic::init function to create and enqueue initial side effects.
225 /// - Reduces the initial Model provided at construction to Props via MvuLogic::view.
226 /// - Renders the initial Props.
227 /// - Processes events from the channel in a loop.
228 ///
229 /// This is an async function that runs the event loop. You can spawn it on your
230 /// chosen runtime using the spawner, or await it directly.
231 ///
232 /// Events can be emitted from any thread via the Emitter, but are always processed
233 /// sequentially on the thread where this future is awaited/polled.
234 pub async fn run(mut self) {
235 let (init_model, init_effect) = self.logic.init(self.model.clone());
236
237 let initial_props = {
238 let emitter = &self.emitter;
239 self.logic.view(&init_model, emitter)
240 };
241
242 self.renderer.render(initial_props);
243
244 // Execute initial effect by spawning it
245 let emitter = self.emitter.clone();
246 let future = init_effect.execute(&emitter);
247 self.spawner.spawn(Box::pin(future));
248
249 // Event processing loop
250 while let Ok(event) = self.event_receiver.recv().await {
251 self.step(event);
252 }
253 }
254
255 fn step(&mut self, event: Event) {
256 // Update model with event
257 let (new_model, effect) = self.logic.update(event, &self.model);
258
259 // Reduce to props and render
260 let props = self.logic.view(&new_model, &self.emitter);
261 self.renderer.render(props);
262
263 // Update model
264 self.model = new_model;
265
266 // Execute the effect
267 let emitter = self.emitter.clone();
268 let future = effect.execute(&emitter);
269 self.spawner.spawn(Box::pin(future));
270 }
271}
272
273#[cfg(any(test, feature = "testing"))]
274/// Test spawner function that executes futures synchronously.
275///
276/// This blocks on the future immediately rather than spawning it on an async runtime.
277pub fn test_spawner_fn(fut: Pin<Box<dyn Future<Output = ()> + Send>>) {
278 // Execute the future synchronously for deterministic testing
279 futures::executor::block_on(fut);
280}
281
282#[cfg(any(test, feature = "testing"))]
283/// Creates a test spawner that executes futures synchronously.
284///
285/// This is useful for testing - it blocks on the future immediately rather
286/// than spawning it on an async runtime. Use this with [`TestMvuRuntime`]
287/// or [`MvuRuntime`] in test scenarios.
288///
289/// Returns a function pointer that can be passed directly to runtime constructors
290/// without heap allocation.
291pub fn create_test_spawner() -> fn(Pin<Box<dyn Future<Output = ()> + Send>>) {
292 test_spawner_fn
293}
294
295#[cfg(any(test, feature = "testing"))]
296/// Test runtime driver for manual event processing control.
297///
298/// Only available with the `testing` feature or during tests.
299///
300/// Returned by [`TestMvuRuntime::run`]. Provides methods to manually
301/// emit events and process the event queue for precise control in tests.
302///
303/// See [`TestMvuRuntime`] for usage.
304pub struct TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
305where
306 Event: EventTrait,
307 Model: Clone + 'static,
308 Props: 'static,
309 Logic: MvuLogic<Event, Model, Props>,
310 Render: Renderer<Props>,
311 Spawn: Spawner,
312{
313 _runtime: TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
314}
315
316#[cfg(any(test, feature = "testing"))]
317impl<Event, Model, Props, Logic, Render, Spawn>
318 TestMvuDriver<Event, Model, Props, Logic, Render, Spawn>
319where
320 Event: EventTrait,
321 Model: Clone + 'static,
322 Props: 'static,
323 Logic: MvuLogic<Event, Model, Props>,
324 Render: Renderer<Props>,
325 Spawn: Spawner,
326{
327 /// Process all queued events.
328 ///
329 /// This processes events until the queue is empty. Call this after emitting
330 /// events to drive the event loop in tests.
331 pub fn process_events(&mut self) {
332 self._runtime.process_queued_events();
333 }
334}
335
336#[cfg(any(test, feature = "testing"))]
337/// Test runtime for MVU with manual event processing control.
338///
339/// Only available with the `testing` feature or during tests.
340///
341/// Unlike [`MvuRuntime`], this runtime does not automatically
342/// process events when they are emitted. Instead, tests must manually call
343/// [`process_events`](TestMvuDriver::process_events) on the returned driver
344/// to process the event queue.
345///
346/// This provides precise control over event timing in tests.
347///
348/// ```rust
349/// use oxide_mvu::{Emitter, Effect, Renderer, MvuLogic, TestMvuRuntime};
350/// # #[derive(Clone)]
351/// # enum Event { Increment }
352/// # #[derive(Clone)]
353/// # struct Model { count: i32 }
354/// # struct Props { count: i32, on_click: Box<dyn Fn()> }
355/// # struct MyApp;
356/// # impl MvuLogic<Event, Model, Props> for MyApp {
357/// # fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
358/// # fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
359/// # (Model { count: model.count + 1 }, Effect::none())
360/// # }
361/// # fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
362/// # let e = emitter.clone();
363/// # Props { count: model.count, on_click: Box::new(move || { e.try_emit(Event::Increment); }) }
364/// # }
365/// # }
366/// # struct TestRenderer;
367/// # impl Renderer<Props> for TestRenderer { fn render(&mut self, _props: Props) {} }
368/// use oxide_mvu::create_test_spawner;
369///
370/// let runtime = TestMvuRuntime::builder(
371/// Model { count: 0 },
372/// MyApp,
373/// TestRenderer,
374/// create_test_spawner()
375/// ).build();
376/// let mut driver = runtime.run();
377/// driver.process_events(); // Manually process events
378/// ```
379pub struct TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
380where
381 Event: EventTrait,
382 Model: Clone + 'static,
383 Props: 'static,
384 Logic: MvuLogic<Event, Model, Props>,
385 Render: Renderer<Props>,
386 Spawn: Spawner,
387{
388 runtime: MvuRuntime<Event, Model, Props, Logic, Render, Spawn>,
389}
390
391#[cfg(any(test, feature = "testing"))]
392/// Builder for configuring and constructing a [`TestMvuRuntime`].
393///
394/// Created via [`TestMvuRuntime::builder`]. Allows customizing runtime parameters
395/// like event buffer capacity before building the test runtime.
396pub struct TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
397where
398 Event: EventTrait,
399 Model: Clone + 'static,
400 Props: 'static,
401 Logic: MvuLogic<Event, Model, Props>,
402 Render: Renderer<Props>,
403 Spawn: Spawner,
404{
405 init_model: Model,
406 logic: Logic,
407 renderer: Render,
408 spawner: Spawn,
409 capacity: usize,
410 _event: core::marker::PhantomData<Event>,
411 _props: core::marker::PhantomData<Props>,
412}
413
414#[cfg(any(test, feature = "testing"))]
415impl<Event, Model, Props, Logic, Render, Spawn>
416 TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn>
417where
418 Event: EventTrait,
419 Model: Clone + 'static,
420 Props: 'static,
421 Logic: MvuLogic<Event, Model, Props>,
422 Render: Renderer<Props>,
423 Spawn: Spawner,
424{
425 /// Set the event buffer capacity.
426 ///
427 /// This bounds the number of events that can be queued before
428 /// [`Emitter::try_emit`](crate::Emitter::try_emit) returns `false`.
429 ///
430 /// Defaults to [`DEFAULT_EVENT_CAPACITY`].
431 pub fn capacity(mut self, capacity: usize) -> Self {
432 self.capacity = capacity;
433 self
434 }
435
436 /// Build the test runtime with the configured settings.
437 pub fn build(self) -> TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn> {
438 let (event_sender, event_receiver) = bounded(self.capacity);
439
440 TestMvuRuntime {
441 runtime: MvuRuntime {
442 logic: self.logic,
443 renderer: self.renderer,
444 event_receiver,
445 model: self.init_model,
446 emitter: Emitter::new(event_sender),
447 spawner: self.spawner,
448 _props: core::marker::PhantomData,
449 },
450 }
451 }
452}
453
454#[cfg(any(test, feature = "testing"))]
455impl<Event, Model, Props, Logic, Render, Spawn>
456 TestMvuRuntime<Event, Model, Props, Logic, Render, Spawn>
457where
458 Event: EventTrait,
459 Model: Clone + 'static,
460 Props: 'static,
461 Logic: MvuLogic<Event, Model, Props>,
462 Render: Renderer<Props>,
463 Spawn: Spawner,
464{
465 /// Create a builder for configuring the test runtime.
466 ///
467 /// # Arguments
468 ///
469 /// * `init_model` - The initial state
470 /// * `logic` - Application logic implementing MvuLogic
471 /// * `renderer` - Platform rendering implementation for rendering Props
472 /// * `spawner` - Spawner to execute async effects on your chosen runtime
473 pub fn builder(
474 init_model: Model,
475 logic: Logic,
476 renderer: Render,
477 spawner: Spawn,
478 ) -> TestMvuRuntimeBuilder<Event, Model, Props, Logic, Render, Spawn> {
479 TestMvuRuntimeBuilder {
480 init_model,
481 logic,
482 renderer,
483 spawner,
484 capacity: DEFAULT_EVENT_CAPACITY,
485 _event: core::marker::PhantomData,
486 _props: core::marker::PhantomData,
487 }
488 }
489
490 /// Initializes the runtime and returns a driver for manual event processing.
491 ///
492 /// This processes initial effects and renders the initial state, then returns
493 /// a [`TestMvuDriver`] that provides manual control over event processing.
494 pub fn run(mut self) -> TestMvuDriver<Event, Model, Props, Logic, Render, Spawn> {
495 let (init_model, init_effect) = self.runtime.logic.init(self.runtime.model.clone());
496
497 let initial_props = { self.runtime.logic.view(&init_model, &self.runtime.emitter) };
498
499 self.runtime.renderer.render(initial_props);
500
501 // Execute initial effect by spawning it
502 let future = init_effect.execute(&self.runtime.emitter);
503 self.runtime.spawner.spawn(Box::pin(future));
504
505 TestMvuDriver { _runtime: self }
506 }
507
508 /// Process all queued events (for testing).
509 ///
510 /// This is exposed for TestMvuRuntime to manually drive event processing.
511 fn process_queued_events(&mut self) {
512 while let Ok(event) = self.runtime.event_receiver.try_recv() {
513 self.runtime.step(event);
514 }
515 }
516}