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}