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