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}