Skip to main content

rosace_widgets/tree/
app.rs

1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use rosace_render::{Color, FontCache, PictureRecorder, SkiaCanvas};
4use rosace_theme::{ThemeData, built_in};
5use super::{Widget, LayoutCtx, PaintCtx};
6
7/// Off-screen widget renderer — renders a widget tree to a [`SkiaCanvas`] or PNG bytes.
8///
9/// Use this for **golden / snapshot tests**: render a widget to PNG, save it, and
10/// compare future runs byte-for-byte to catch visual regressions.
11///
12/// For a real windowed app, use `rosace::App` from the umbrella crate instead.
13///
14/// ```rust,ignore
15/// // Render to PNG bytes (dark theme by default):
16/// let png = WidgetApp::new(800, 600).render_png(&root);
17///
18/// // Light theme:
19/// let png = WidgetApp::new(800, 600).light().render_png(&root);
20///
21/// // Custom theme:
22/// let png = WidgetApp::new(800, 600).theme(my_theme).render_png(&root);
23/// ```
24pub struct WidgetApp {
25    pub width: u32,
26    pub height: u32,
27    /// `None` = derive from `theme.colors.background`; `Some` = explicit override.
28    background: Option<Color>,
29    pub font: FontCache,
30    pub theme: ThemeData,
31}
32
33impl WidgetApp {
34    /// Create a new app with the default dark theme.
35    pub fn new(width: u32, height: u32) -> Self {
36        let font = FontCache::system_ui()
37            .or_else(FontCache::system_mono)
38            .expect("no system font found");
39        Self {
40            width,
41            height,
42            background: None,
43            font,
44            theme: built_in::dark_theme(),
45        }
46    }
47
48    /// Override the background color explicitly (ignores theme background).
49    pub fn background(mut self, c: Color) -> Self { self.background = Some(c); self }
50
51    /// Replace the font.
52    pub fn with_font(mut self, f: FontCache) -> Self { self.font = f; self }
53
54    /// Set a fully custom theme.
55    pub fn theme(mut self, t: ThemeData) -> Self { self.theme = t; self }
56
57    /// Use the built-in dark theme (this is already the default).
58    pub fn dark(mut self) -> Self { self.theme = built_in::dark_theme(); self }
59
60    /// Use the built-in light theme.
61    pub fn light(mut self) -> Self { self.theme = built_in::light_theme(); self }
62
63    fn resolve_bg(&self) -> Color {
64        self.background.unwrap_or_else(|| {
65            let c = self.theme.colors.background;
66            Color::rgba(
67                (c.r * 255.0) as u8,
68                (c.g * 255.0) as u8,
69                (c.b * 255.0) as u8,
70                (c.a * 255.0) as u8,
71            )
72        })
73    }
74
75    /// Render the widget tree to a [`SkiaCanvas`].
76    pub fn render(&self, root: &dyn Widget) -> SkiaCanvas {
77        rosace_theme::set_theme(self.theme.clone());
78
79        let mut canvas = SkiaCanvas::new(self.width, self.height);
80        canvas.clear(self.resolve_bg());
81
82        let constraints = Constraints::tight(self.width as f32, self.height as f32);
83        let lctx = LayoutCtx::new(constraints, &self.font, &self.theme);
84        let size = root.layout(&lctx);
85
86        let mut recorder = PictureRecorder::new();
87        let mut ctx = PaintCtx::root(
88            &mut recorder,
89            Rect {
90                origin: Point { x: 0.0, y: 0.0 },
91                size: Size {
92                    width:  size.width.min(self.width as f32),
93                    height: size.height.min(self.height as f32),
94                },
95            },
96            &self.font,
97            self.theme.clone(),
98            std::rc::Rc::new(std::cell::RefCell::new(super::RenderTree::new())),
99        );
100
101        root.paint(&mut ctx);
102        let picture = recorder.finish();
103        canvas.play_picture(&picture, &self.font);
104        canvas
105    }
106
107    /// Paint the widget tree onto an existing canvas (for windowed apps).
108    pub fn render_into(&self, canvas: &mut SkiaCanvas, root: &dyn Widget) {
109        rosace_theme::set_theme(self.theme.clone());
110        canvas.clear(self.resolve_bg());
111
112        let w = canvas.width() as f32;
113        let h = canvas.height() as f32;
114        let constraints = Constraints::tight(w, h);
115        let lctx = LayoutCtx::new(constraints, &self.font, &self.theme);
116        let size = root.layout(&lctx);
117
118        let mut recorder = PictureRecorder::new();
119        let mut ctx = PaintCtx::root(
120            &mut recorder,
121            Rect {
122                origin: Point { x: 0.0, y: 0.0 },
123                size: Size {
124                    width:  size.width.min(w),
125                    height: size.height.min(h),
126                },
127            },
128            &self.font,
129            self.theme.clone(),
130            std::rc::Rc::new(std::cell::RefCell::new(super::RenderTree::new())),
131        );
132        root.paint(&mut ctx);
133        let picture = recorder.finish();
134        canvas.play_picture(&picture, &self.font);
135    }
136
137    /// Render and encode to PNG bytes (convenience).
138    pub fn render_png(&self, root: &dyn Widget) -> Vec<u8> {
139        self.render(root)
140            .encode_png()
141            .expect("png encode failed")
142    }
143}