oxide_mvu/renderer.rs
1//! Renderer abstraction for rendering Props.
2
3#[cfg(any(test, feature = "testing"))]
4#[cfg(feature = "no_std")]
5use alloc::vec::Vec;
6
7#[cfg(any(test, feature = "testing"))]
8use portable_atomic_util::Arc;
9#[cfg(any(test, feature = "testing"))]
10use spin::Mutex;
11
12/// Renderer abstraction for rendering Props.
13///
14/// Implement this trait to integrate oxide-mvu just your rendering system
15/// (UI framework, terminal, embedded display, etc.).
16///
17/// The [`render`](Self::render) method is called whenever the model changes, receiving
18/// fresh Props derived from the current state via [`MvuLogic::view`](crate::MvuLogic::view).
19///
20/// # Example
21///
22/// ```rust
23/// use oxide_mvu::Renderer;
24///
25/// struct Props {
26/// message: &'static str,
27/// }
28///
29/// struct ConsoleRenderer;
30///
31/// impl Renderer<Props> for ConsoleRenderer {
32/// fn render(&mut self, props: Props) {
33/// println!("{}", props.message);
34/// }
35/// }
36/// ```
37pub trait Renderer<Props> {
38 /// Render the given props.
39 ///
40 /// This is where you integrate just your rendering system. Props may
41 /// contain callbacks (via [`Emitter`](crate::Emitter)) that can trigger new events.
42 ///
43 /// # Arguments
44 ///
45 /// * `props` - The props to render, derived from the current model state
46 fn render(&mut self, props: Props);
47}
48
49#[cfg(any(test, feature = "testing"))]
50/// Test renderer that captures all rendered Props for assertions.
51///
52/// Only available with the `testing` feature.
53///
54/// Use this with [`TestMvuRuntime`](crate::TestMvuRuntime) to capture and inspect
55/// Props in integration tests.
56///
57/// # Example
58///
59/// ```rust
60/// use oxide_mvu::{create_test_spawner, TestRenderer, TestMvuRuntime, MvuLogic, Effect, Emitter};
61///
62/// # struct Props { count: i32 }
63/// #
64/// # #[derive(Clone)]
65/// # struct Model { count: i32 }
66/// #
67/// # #[derive(Clone)]
68/// # enum Event { Inc }
69/// #
70/// # struct Logic;
71/// #
72/// # impl MvuLogic<Event, Model, Props> for Logic {
73/// # fn init(&self, m: Model) -> (Model, Effect<Event>) { (m, Effect::none()) }
74/// # fn update(&self, _e: Event, m: &Model) -> (Model, Effect<Event>) {
75/// # (Model { count: m.count + 1 }, Effect::none())
76/// # }
77/// # fn view(&self, m: &Model, _: &Emitter<Event>) -> Props {
78/// # Props { count: m.count }
79/// # }
80/// # }
81/// // Create a TestRenderer for props assertions
82/// let renderer = TestRenderer::new();
83///
84/// // Construct a TestMvuRuntime using the renderer
85/// let runtime = TestMvuRuntime::new(
86/// Model { count: 0 },
87/// Logic,
88/// renderer.clone(),
89/// create_test_spawner()
90/// );
91///
92/// let driver = runtime.run();
93///
94/// // Use renderer to inspect renders
95/// renderer.with_renders(|renders| {
96/// assert_eq!(renders[0].count, 0);
97/// });
98/// ```
99pub struct TestRenderer<Props> {
100 renders: Arc<Mutex<Vec<Props>>>,
101}
102
103#[cfg(any(test, feature = "testing"))]
104impl<Props> Clone for TestRenderer<Props> {
105 fn clone(&self) -> Self {
106 Self {
107 renders: self.renders.clone(),
108 }
109 }
110}
111
112#[cfg(any(test, feature = "testing"))]
113impl<Props> Renderer<Props> for TestRenderer<Props> {
114 fn render(&mut self, props: Props) {
115 self.renders.lock().push(props);
116 }
117}
118
119#[cfg(any(test, feature = "testing"))]
120impl<Props: 'static> Default for TestRenderer<Props> {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126#[cfg(any(test, feature = "testing"))]
127impl<Props: 'static> TestRenderer<Props> {
128 pub fn new() -> Self {
129 Self {
130 renders: Arc::new(Mutex::new(Vec::new())),
131 }
132 }
133
134 /// Get the number of renders that have occurred.
135 pub fn count(&self) -> usize {
136 self.renders.lock().len()
137 }
138
139 /// Access the captured renders with a closure.
140 ///
141 /// The closure receives a reference to the Vec of all captured Props.
142 /// This allows you to make assertions on Props emissions or execute
143 /// callbacks for further testing.
144 ///
145 /// # Example
146 ///
147 /// ```rust
148 /// # use oxide_mvu::TestRenderer;
149 /// # struct Props { count: i32, on_click: Box<dyn Fn()> }
150 /// # let renderer = TestRenderer::<Props>::new();
151 ///
152 /// // Compute render count
153 /// let count = renderer.with_renders(|renders| renders.len());
154 ///
155 /// // Make Props assertions
156 /// renderer.with_renders(|renders| {
157 /// // assert_eq!(renders[0].count, 42);
158 /// });
159 ///
160 /// // Execute a specific Props callback
161 /// renderer.with_renders(|renders| {
162 /// // (renders[0].on_click)();
163 /// });
164 /// ```
165 pub fn with_renders<F, R>(&self, f: F) -> R
166 where
167 F: FnOnce(&Vec<Props>) -> R,
168 {
169 let renders = self.renders.lock();
170 f(&renders)
171 }
172}