Skip to main content

waterui_core/ui/
view_renderer.rs

1//! View renderer for capturing views to pixel data.
2//!
3//! This module provides the `ViewRenderer` capability that native backends
4//! install via FFI. It allows capturing `WaterUI` views as RGBA pixel data.
5
6use alloc::boxed::Box;
7use alloc::vec::Vec;
8use core::fmt;
9use core::future::Future;
10use core::pin::Pin;
11
12use crate::AnyView;
13
14/// Size for view rendering.
15#[derive(Debug, Clone, Copy)]
16pub struct RenderSize {
17    /// Width in points.
18    pub width: f32,
19    /// Height in points.
20    pub height: f32,
21}
22
23impl RenderSize {
24    /// Create a new render size.
25    #[must_use]
26    pub const fn new(width: f32, height: f32) -> Self {
27        Self { width, height }
28    }
29}
30
31/// Result of rendering a view to RGBA pixels.
32#[derive(Debug)]
33pub struct RenderResult {
34    /// RGBA pixel data (4 bytes per pixel, row-major order).
35    pub rgba_data: Vec<u8>,
36    /// Actual rendered width in pixels.
37    pub width: u32,
38    /// Actual rendered height in pixels.
39    pub height: u32,
40}
41
42/// Trait for custom view renderers.
43///
44/// Native backends implement this to provide view-to-RGBA capture. The method
45/// returns `impl Future`, so implementations write a plain `async fn` — the
46/// object-safe boxing needed to store the renderer in the [`Environment`](crate::Environment) is an
47/// internal detail of [`ViewRenderer`], never part of this contract.
48pub trait CustomViewRenderer: 'static {
49    /// Render a view to RGBA bytes.
50    ///
51    /// The implementation should:
52    /// 1. Create an offscreen rendering context at the given size
53    /// 2. Render the view hierarchy (native widgets + GPU surfaces)
54    /// 3. Capture the final composited result to RGBA pixels
55    /// 4. Return the pixel data
56    fn render_to_rgba(&self, view: AnyView, size: RenderSize)
57    -> impl Future<Output = RenderResult>;
58}
59
60/// Object-safe shim over [`CustomViewRenderer`] so [`ViewRenderer`] can box the
61/// renderer: the public trait keeps its friendly `impl Future` signature and
62/// the blanket impl pins the future here, behind the erasure boundary.
63trait CustomViewRendererImpl: 'static {
64    fn render_to_rgba<'a>(
65        &'a self,
66        view: AnyView,
67        size: RenderSize,
68    ) -> Pin<Box<dyn 'a + Future<Output = RenderResult>>>;
69}
70
71impl<T: CustomViewRenderer> CustomViewRendererImpl for T {
72    fn render_to_rgba<'a>(
73        &'a self,
74        view: AnyView,
75        size: RenderSize,
76    ) -> Pin<Box<dyn 'a + Future<Output = RenderResult>>> {
77        Box::pin(CustomViewRenderer::render_to_rgba(self, view, size))
78    }
79}
80
81/// Type-erased view renderer stored in Environment.
82///
83/// This wrapper allows storing the renderer in the Environment without
84/// exposing the concrete implementation type.
85pub struct ViewRenderer(Box<dyn CustomViewRendererImpl>);
86
87impl ViewRenderer {
88    /// Create a new view renderer from a custom implementation.
89    pub fn new<T: CustomViewRenderer>(renderer: T) -> Self {
90        Self(Box::new(renderer))
91    }
92
93    /// Render a view to RGBA pixel data.
94    #[allow(clippy::future_not_send)]
95    pub async fn render(&self, view: AnyView, size: RenderSize) -> RenderResult {
96        self.0.render_to_rgba(view, size).await
97    }
98}
99
100impl fmt::Debug for ViewRenderer {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("ViewRenderer").finish_non_exhaustive()
103    }
104}