Skip to main content

agg_gui/widget/
backbuffer.rs

1//! Widget backbuffer types: caching specs, retained state, and compositing layers.
2//!
3//! Any widget can opt into a cached CPU backbuffer by returning `Some(&mut ...)`
4//! from [`Widget::backbuffer_cache_mut`].  The framework's `paint_subtree`
5//! handles caching transparently: when the widget is dirty (or has no bitmap
6//! yet) it allocates a fresh `Framebuffer`, runs `widget.paint` + all children
7//! into it via a software `GfxCtx`, and caches the resulting RGBA8 pixels as a
8//! shared `Arc<Vec<u8>>`.  Every subsequent frame that finds the widget clean
9//! just blits the cached pixels through `ctx.draw_image_rgba_arc` — zero AGG
10//! cost in steady state.  On the GL backend the `Arc`'s pointer identity keys
11//! the GPU texture cache (see `arc_texture_cache`), so the hardware texture
12//! is also reused across frames and dropped when the bitmap drops.
13//!
14//! LCD subpixel rendering works naturally inside a backbuffer: the widget
15//! paints its own background first (so text has a solid dst) and then any
16//! `fill_text` call composites the per-channel coverage mask onto that
17//! destination.  No walk / sample / bg-declaration needed.
18
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::Arc;
21
22use crate::layout_props::Insets;
23
24/// How a widget's backbuffer stores pixels.
25///
26/// The choice controls what the framework allocates as the render
27/// target during `paint_subtree_backbuffered` and how the cached
28/// bitmap is composited back onto the parent.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum BackbufferMode {
31    /// 8-bit straight-alpha RGBA.  Standard Porter-Duff `SRC_ALPHA,
32    /// ONE_MINUS_SRC_ALPHA` composite on blit.  Works for any widget,
33    /// including ones with transparent areas.  Text inside is grayscale
34    /// AA (no LCD subpixel).
35    Rgba,
36    /// 3 bytes-per-pixel **composited opaque RGB** — no alpha channel.
37    /// Every fill (rects, strokes, text, etc.) inside the buffer goes
38    /// through the 3× horizontal supersample + 5-tap filter + per-channel
39    /// src-over pipeline described in `lcd-subpixel-compositing.md`.
40    /// The buffer is blitted as an opaque RGB texture.
41    ///
42    /// **Contract:** the widget is responsible for painting content
43    /// that covers its full bounds with opaque fills (starting with a
44    /// bg rect).  Uncovered pixels land as black on the parent because
45    /// there is no alpha channel to carry "no paint here."
46    LcdCoverage,
47}
48
49/// Unified backbuffer target kind requested by a widget.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum BackbufferKind {
52    /// Paint directly into the parent render target.
53    None,
54    /// Retained software RGBA framebuffer.
55    SoftwareRgba,
56    /// Retained software LCD coverage framebuffer.
57    SoftwareLcd,
58    /// Retained GL framebuffer object.
59    GlFbo,
60}
61
62/// Widget-owned backbuffer request. Windows use this for retained GL FBOs,
63/// while existing label/text-field CPU caches map naturally to the software
64/// variants.
65#[derive(Clone, Copy, Debug)]
66pub struct BackbufferSpec {
67    pub kind: BackbufferKind,
68    pub cached: bool,
69    pub alpha: f64,
70    pub outsets: Insets,
71    pub rounded_clip: Option<f64>,
72}
73
74impl BackbufferSpec {
75    pub const fn none() -> Self {
76        Self {
77            kind: BackbufferKind::None,
78            cached: false,
79            alpha: 1.0,
80            outsets: Insets::ZERO,
81            rounded_clip: None,
82        }
83    }
84}
85
86impl Default for BackbufferSpec {
87    fn default() -> Self {
88        Self::none()
89    }
90}
91
92/// A CPU bitmap owned by a widget that opts into backbuffer caching.
93///
94/// The framework re-rasterises when the cache's explicit dirty flag is set or
95/// when global styling epochs change.
96pub struct BackbufferCache {
97    /// In **Rgba** mode: top-row-first RGBA8 pixels, straight alpha.
98    /// Blitted via [`DrawCtx::draw_image_rgba_arc`].
99    ///
100    /// In **LcdCoverage** mode: top-row-first **colour plane** — 3
101    /// bytes/pixel (R_premult, G_premult, B_premult) matching the
102    /// convention of [`crate::lcd_coverage::LcdBuffer::color_plane`]
103    /// flipped to top-down.  The companion alpha plane lives in
104    /// [`Self::lcd_alpha`].
105    pub pixels: Option<Arc<Vec<u8>>>,
106    /// `LcdCoverage`-mode companion to `pixels`: top-row-first per-channel
107    /// **alpha plane** (3 bytes/pixel, `(R_alpha, G_alpha, B_alpha)`).
108    /// `None` means this is a plain Rgba cache.  When `Some`, the blit
109    /// step uses [`DrawCtx::draw_lcd_backbuffer_arc`] to preserve the
110    /// per-channel subpixel information through to the destination —
111    /// required for LCD chroma to survive the cache round-trip.
112    pub lcd_alpha: Option<Arc<Vec<u8>>>,
113    pub width: u32,
114    pub height: u32,
115    /// When true, the next paint will re-rasterise rather than reusing
116    /// `pixels`.  Widgets set this from their mutation paths
117    /// (`set_text`, `set_color`, focus/hover changes, etc.) and the
118    /// framework clears it after a successful re-raster.
119    pub dirty: bool,
120    /// Visuals epoch (see [`crate::theme::current_visuals_epoch`]) recorded
121    /// the last time this cache was populated.  `paint_subtree_backbuffered`
122    /// compares it against the live epoch and forces a re-raster on mismatch,
123    /// so widgets whose text/fill colours come from `ctx.visuals()` refresh
124    /// automatically on a dark/light theme flip without needing every widget
125    /// to subscribe to theme-change events.
126    pub theme_epoch: u64,
127    /// Typography epoch (see
128    /// [`crate::font_settings::current_typography_epoch`]) — same
129    /// pattern as `theme_epoch` but for font / size scale / LCD /
130    /// hinting / gamma / width / interval / faux-* globals.  Lets a
131    /// slider drag in the LCD Subpixel demo invalidate every cached
132    /// `Label` bitmap without bespoke hooks per widget.
133    pub typography_epoch: u64,
134    /// Async-state epoch (see
135    /// [`crate::animation::async_state_epoch`]) — bumped when an
136    /// off-thread / async source (e.g. an image fetch + decode)
137    /// finishes outside the normal event-dispatch path that would
138    /// otherwise mark widgets dirty.  Mismatch forces a re-raster
139    /// so freshly-loaded data lands in newly-laid-out bounds.
140    pub async_state_epoch: u64,
141}
142
143impl BackbufferCache {
144    pub fn new() -> Self {
145        Self {
146            pixels: None,
147            lcd_alpha: None,
148            width: 0,
149            height: 0,
150            dirty: true,
151            theme_epoch: 0,
152            typography_epoch: 0,
153            async_state_epoch: 0,
154        }
155    }
156
157    /// Mark the cache dirty so the next paint re-rasterises.
158    pub fn invalidate(&mut self) {
159        self.dirty = true;
160    }
161}
162
163impl Default for BackbufferCache {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169static NEXT_BACKBUFFER_ID: AtomicU64 = AtomicU64::new(1);
170
171/// Retained widget backbuffer state shared by software and GL implementations.
172pub struct BackbufferState {
173    id: u64,
174    pub cache: BackbufferCache,
175    pub dirty: bool,
176    pub width: u32,
177    pub height: u32,
178    pub spec_kind: BackbufferKind,
179    /// Visuals epoch recorded the last time this retained surface was repainted.
180    /// Retained backend layers compare it against the live theme epoch so a
181    /// dark/light flip rebuilds the window/layer in the shared paint path.
182    pub theme_epoch: u64,
183    /// Typography epoch recorded the last time this retained surface was
184    /// repainted. Without this, a clean parent FBO can keep compositing old
185    /// text after global font/LCD settings change.
186    pub typography_epoch: u64,
187    /// Async-state epoch (see [`crate::animation::async_state_epoch`])
188    /// recorded the last paint.  Mismatch forces a re-raster so a
189    /// freshly-arrived async result (image fetch, font load) doesn't
190    /// composite the previous frame's stale FBO contents.
191    pub async_state_epoch: u64,
192    pub repaint_count: u64,
193    pub composite_count: u64,
194}
195
196impl BackbufferState {
197    pub fn new() -> Self {
198        Self {
199            id: NEXT_BACKBUFFER_ID.fetch_add(1, Ordering::Relaxed),
200            cache: BackbufferCache::new(),
201            dirty: true,
202            width: 0,
203            height: 0,
204            spec_kind: BackbufferKind::None,
205            theme_epoch: 0,
206            typography_epoch: 0,
207            async_state_epoch: 0,
208            repaint_count: 0,
209            composite_count: 0,
210        }
211    }
212
213    pub fn id(&self) -> u64 {
214        self.id
215    }
216
217    pub fn invalidate(&mut self) {
218        self.dirty = true;
219        self.cache.invalidate();
220    }
221}
222
223impl Default for BackbufferState {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229/// Offscreen compositing layer requested by a widget for itself and its
230/// descendants.
231#[derive(Clone, Copy, Debug)]
232pub struct CompositingLayer {
233    /// Extra transparent pixels to the left of the widget bounds.
234    pub outset_left: f64,
235    /// Extra transparent pixels below the widget bounds.
236    pub outset_bottom: f64,
237    /// Extra transparent pixels to the right of the widget bounds.
238    pub outset_right: f64,
239    /// Extra transparent pixels above the widget bounds.
240    pub outset_top: f64,
241    /// Whole-layer opacity applied while compositing back to the parent.
242    pub alpha: f64,
243}
244
245impl CompositingLayer {
246    pub const fn new(
247        outset_left: f64,
248        outset_bottom: f64,
249        outset_right: f64,
250        outset_top: f64,
251        alpha: f64,
252    ) -> Self {
253        Self {
254            outset_left,
255            outset_bottom,
256            outset_right,
257            outset_top,
258            alpha,
259        }
260    }
261}