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}