oxide_mvu/
runtime.rs

1//! The MVU runtime that orchestrates the event loop.
2
3#[cfg(feature = "no_std")]
4use alloc::boxed::Box;
5#[cfg(feature = "no_std")]
6use alloc::vec::Vec;
7
8use portable_atomic_util::Arc;
9use spin::Mutex;
10
11use crate::{Effect, Emitter, MvuLogic, Renderer};
12
13/// Internal state for the MVU runtime.
14struct RuntimeState<Event: Send, Model: Clone + Send> {
15    model: Model,
16    event_queue: Vec<Event>,
17    effects_queue: Vec<Effect<Event>>,
18}
19
20/// The MVU runtime that orchestrates the event loop.
21///
22/// This is the core of the framework. It:
23/// 1. Initializes the Model and initial Effects via [`MvuLogic::init`]
24/// 2. Processes events through [`MvuLogic::update`]
25/// 3. Reduces the Model to Props via [`MvuLogic::view`]
26/// 4. Delivers Props to the [`Renderer`] for rendering
27///
28/// The runtime creates a single [`Emitter`] that automatically processes events
29/// when [`Emitter::emit`] is called, regardless of which thread it's called from.
30/// Events are processed synchronously in a thread-safe manner.
31///
32/// For testing with manual control, use [`TestMvuRuntime`] with a [`crate::TestRenderer`].
33///
34/// See the [crate-level documentation](crate) for a complete example.
35pub struct MvuRuntime<Event: Send, Model: Clone + Send, Props> {
36    logic: Box<dyn MvuLogic<Event, Model, Props> + Send>,
37    renderer: Box<dyn Renderer<Props> + Send>,
38    state: Arc<Mutex<RuntimeState<Event, Model>>>,
39    emitter: Emitter<Event>,
40}
41
42impl<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static>
43    MvuRuntime<Event, Model, Props>
44{
45    /// Create a new runtime.
46    ///
47    /// The runtime will not be started until MvuRuntime::run is called.
48    ///
49    /// # Arguments
50    ///
51    /// * `init_model` - The initial state
52    /// * `logic` - Application logic implementing MvuLogic
53    /// * `renderer` - Platform rendering implementation for rendering Props
54    pub fn new(
55        init_model: Model,
56        logic: Box<dyn MvuLogic<Event, Model, Props> + Send>,
57        renderer: Box<dyn Renderer<Props> + Send>,
58    ) -> Self {
59        // Create state and emitter that enqueues to the state's event queue
60        let state = Arc::new(Mutex::new(RuntimeState {
61            model: init_model,
62            event_queue: Vec::new(),
63            effects_queue: Vec::new(),
64        }));
65
66        let state_clone = state.clone();
67        let emitter = Emitter::new(move |event| {
68            state_clone.lock().event_queue.push(event);
69        });
70
71        MvuRuntime {
72            logic,
73            renderer,
74            state,
75            emitter,
76        }
77    }
78
79    /// Initialize the runtime loop.
80    ///
81    /// - Uses the MvuLogic::init function to create and enqueue initial side effects.
82    /// - Reduces the initial Model provided at construction to Props via MvuLogic::view.
83    /// - Renders the initial Props.
84    pub fn run(mut self) {
85        // Initialize the model and get initial effects
86        let init_model = {
87            let mut runtime_state = self.state.lock();
88            let (init_model, init_effect) = {
89                let model = runtime_state.model.clone();
90                self.logic.init(model)
91            };
92
93            // Update model and queue effects
94            runtime_state.model = init_model;
95            runtime_state.effects_queue.push(init_effect);
96
97            runtime_state.model.clone()
98        };
99
100        let initial_props = {
101            let emitter = self.emitter;
102            self.logic.view(&init_model, &emitter)
103        };
104
105        self.renderer.render(initial_props);
106    }
107
108    #[cfg(any(test, feature = "testing"))]
109    fn step(&mut self, event: Event) {
110        // Reduce event and render props
111        let (model, effect, props) = self.reduce_event(event);
112
113        self.renderer.render(props);
114
115        // Update model
116        {
117            let state_mutex = self.state.clone();
118            let mut runtime_state = state_mutex.lock();
119            runtime_state.model = model;
120        }
121
122        // Execute the effect (which may enqueue more events)
123        effect.execute(&self.emitter);
124
125        // Process any newly queued events
126        self.process_queued_events()
127    }
128
129    #[cfg(any(test, feature = "testing"))]
130    /// Dispatch a single event through update -> view -> render.
131    fn reduce_event(&self, event: Event) -> (Model, Effect<Event>, Props) {
132        // Update model just event
133        let (new_model, effect) = {
134            let runtime_state = self.state.lock();
135            self.logic.update(event, &runtime_state.model)
136        };
137
138        // Reduce the new model and emitter to props
139        let emitter = &self.emitter;
140        let props = self.logic.view(&new_model, emitter);
141
142        (new_model, effect, props)
143    }
144
145    #[cfg(any(test, feature = "testing"))]
146    /// Process all queued events (for testing).
147    ///
148    /// This is exposed for TestMvuRuntime to manually drive event processing.
149    fn process_queued_events(&mut self) {
150        loop {
151            let state_mutex = self.state.clone();
152            let next_event = {
153                let mut runtime_state = state_mutex.lock();
154                if runtime_state.event_queue.is_empty() {
155                    break;
156                }
157                runtime_state.event_queue.remove(0)
158            }; // Lock is dropped here
159            self.step(next_event);
160        }
161    }
162}
163
164#[cfg(any(test, feature = "testing"))]
165/// Test runtime driver for manual event processing control.
166///
167/// Only available with the `testing` feature or during tests.
168///
169/// Returned by [`TestMvuRuntime::run`]. Provides methods to manually
170/// emit events and process the event queue for precise control in tests.
171///
172/// See [`TestMvuRuntime`] for usage.
173pub struct TestMvuDriver<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static> {
174    _runtime: MvuRuntime<Event, Model, Props>,
175}
176
177#[cfg(any(test, feature = "testing"))]
178impl<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static>
179    TestMvuDriver<Event, Model, Props>
180{
181    /// Process all queued events.
182    ///
183    /// This processes events until the queue is empty. Call this after emitting
184    /// events to drive the event loop in tests.
185    pub fn process_events(&mut self) {
186        self._runtime.process_queued_events();
187    }
188}
189
190#[cfg(any(test, feature = "testing"))]
191/// Test runtime for MVU with manual event processing control.
192///
193/// Only available with the `testing` feature or during tests.
194///
195/// Unlike [`MvuRuntime`], this runtime does not automatically
196/// process events when they are emitted. Instead, tests must manually call
197/// [`process_events`](TestMvuDriver::process_events) on the returned driver
198/// to process the event queue.
199///
200/// This provides precise control over event timing in tests.
201///
202/// ```rust
203/// use oxide_mvu::{Emitter, Effect, Renderer, MvuLogic, TestMvuRuntime};
204/// # enum Event { Increment }
205/// # #[derive(Clone)]
206/// # struct Model { count: i32 }
207/// # struct Props { count: i32, on_click: Box<dyn Fn() + Send> }
208/// # struct MyApp;
209/// # impl MvuLogic<Event, Model, Props> for MyApp {
210/// #     fn init(&self, model: Model) -> (Model, Effect<Event>) { (model, Effect::none()) }
211/// #     fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
212/// #         (Model { count: model.count + 1 }, Effect::none())
213/// #     }
214/// #     fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
215/// #         let e = emitter.clone();
216/// #         Props { count: model.count, on_click: Box::new(move || e.emit(Event::Increment)) }
217/// #     }
218/// # }
219/// # struct TestRenderer;
220/// # impl Renderer<Props> for TestRenderer { fn render(&mut self, _props: Props) {} }
221///
222/// let runtime = TestMvuRuntime::new(
223///     Model { count: 0 },
224///     Box::new(MyApp),
225///     Box::new(TestRenderer)
226/// );
227/// let mut driver = runtime.run();
228/// driver.process_events(); // Manually process events
229/// ```
230pub struct TestMvuRuntime<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static> {
231    runtime: MvuRuntime<Event, Model, Props>,
232}
233
234#[cfg(any(test, feature = "testing"))]
235impl<Event: Send + 'static, Model: Clone + Send + 'static, Props: 'static>
236    TestMvuRuntime<Event, Model, Props>
237{
238    /// Create a new test runtime.
239    ///
240    /// Creates an emitter that enqueues events without automatically processing them.
241    pub fn new(
242        init_model: Model,
243        logic: Box<dyn MvuLogic<Event, Model, Props> + Send>,
244        renderer: Box<dyn Renderer<Props> + Send>,
245    ) -> Self {
246        // Create state and emitter that enqueues to the state's event queue
247        let state = Arc::new(Mutex::new(RuntimeState {
248            model: init_model,
249            event_queue: Vec::new(),
250            effects_queue: Vec::new(),
251        }));
252
253        let state_clone = state.clone();
254        let emitter = Emitter::new(move |event| {
255            state_clone.lock().event_queue.push(event);
256        });
257
258        TestMvuRuntime {
259            runtime: MvuRuntime {
260                logic,
261                renderer,
262                state,
263                emitter,
264            },
265        }
266    }
267
268    /// Initializes the runtime and returns a driver for manual event processing.
269    ///
270    /// This processes initial effects and renders the initial state, then returns
271    /// a [`TestMvuDriver`] that provides manual control over event processing.
272    pub fn run(mut self) -> TestMvuDriver<Event, Model, Props> {
273        // Initialize the model and get initial effects
274        let init_model = {
275            let mut runtime_state = self.runtime.state.lock();
276            let (init_model, init_effect) = {
277                let model = runtime_state.model.clone();
278                self.runtime.logic.init(model)
279            };
280
281            // Update model and queue effects
282            runtime_state.model = init_model;
283            runtime_state.effects_queue.push(init_effect);
284
285            runtime_state.model.clone()
286        };
287
288        let initial_props = {
289            let emitter = &self.runtime.emitter;
290            self.runtime.logic.view(&init_model, emitter)
291        };
292
293        self.runtime.renderer.render(initial_props);
294
295        // Process initial effects by executing them with the emitter
296        {
297            let mut runtime_state = self.runtime.state.lock();
298            let effects = runtime_state.effects_queue.drain(..).collect::<Vec<_>>();
299            drop(runtime_state);
300
301            for effect in effects {
302                effect.execute(&self.runtime.emitter);
303            }
304        }
305
306        TestMvuDriver {
307            _runtime: self.runtime,
308        }
309    }
310}