Skip to main content

agg_gui/
draw_ctx.rs

1//! `DrawCtx` — the unified drawing interface shared by the software (`GfxCtx`)
2//! and hardware (`GlGfxCtx`) rendering paths.
3//!
4//! Every `Widget::paint` implementation receives a `&mut dyn DrawCtx`.  The
5//! concrete type is either:
6//!
7//! - **`GfxCtx`** — software AGG rasteriser (used when a widget opts into a
8//!   back-buffer or when GL is unavailable).
9//! - **`GlGfxCtx`** — hardware GL path: shapes are tessellated via `tess2`
10//!   and submitted as GPU draw calls.
11//!
12//! The two implementations expose *identical* method signatures so that widget
13//! `paint` bodies are unchanged regardless of the render target.
14
15use std::sync::Arc;
16
17use crate::color::Color;
18use crate::geometry::Rect;
19use crate::text::{Font, TextMetrics};
20use crate::theme::Visuals;
21use agg_rust::comp_op::CompOp;
22use agg_rust::math_stroke::{LineCap, LineJoin};
23use agg_rust::trans_affine::TransAffine;
24
25// Paint definitions live in [`crate::paints`]; re-exported here so existing
26// `agg_gui::draw_ctx::{FillRule, RadialGradientPaint, …}` import paths keep
27// resolving unchanged.
28pub use crate::paints::{
29    FillRule, GradientSpread, GradientStop, LinearGradientPaint, PatternPaint, RadialGradientPaint,
30};
31
32// ---------------------------------------------------------------------------
33// GL paint hook
34// ---------------------------------------------------------------------------
35
36/// Trait for widgets that want to render 3-D (or other GPU) content inline
37/// during the widget paint pass.
38///
39/// `DrawCtx::gl_paint` calls this with an opaque `gl` handle — implementations
40/// downcast it to `glow::Context` (or whatever GL type the platform provides).
41/// The software `GfxCtx` never calls `paint`; see [`DrawCtx::gl_paint`].
42pub trait GlPaint {
43    /// Execute GPU draw calls for the widget's 3-D content.
44    ///
45    /// `gl` — opaque platform GL context; downcast via `std::any::Any`.
46    /// `screen_rect` — Y-up screen-space rect for this widget (for viewport/scissor).
47    /// `full_w`, `full_h` — full viewport dimensions (for restoring after).
48    /// `parent_clip` — current framework scissor rect `[x, y, w, h]` in GL/Y-up
49    ///   pixels, or `None` if no clip is active.  Implementations **must intersect**
50    ///   any scissor they set with this rect so that parent widget clips (e.g. a
51    ///   collapsed window) correctly hide GPU-rendered content.
52    fn gl_paint(
53        &mut self,
54        gl: &dyn std::any::Any,
55        screen_rect: Rect,
56        full_w: i32,
57        full_h: i32,
58        parent_clip: Option<[i32; 4]>,
59    );
60}
61
62/// Unified 2-D drawing context.
63///
64/// All coordinate parameters use the **Y-up, first-quadrant** convention:
65/// origin at the bottom-left, positive-Y upward.  This matches `GfxCtx` and
66/// the widget tree layout invariant.
67pub trait DrawCtx {
68    /// Optional escape hatch for widgets that need direct access to a
69    /// backend-specific concrete context (e.g. to push a custom GPU draw
70    /// command into the deferred command stream).
71    ///
72    /// The default returns `None`; backends that opt in override to return
73    /// `Some(self)`.  Callers must handle the `None` case gracefully — if a
74    /// widget falls back through `gl_paint` it works on every backend.
75    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
76        None
77    }
78
79    // ── State ─────────────────────────────────────────────────────────────────
80
81    fn set_fill_color(&mut self, color: Color);
82    fn set_stroke_color(&mut self, color: Color);
83    fn set_fill_linear_gradient(&mut self, _gradient: LinearGradientPaint) {}
84    fn set_fill_radial_gradient(&mut self, _gradient: RadialGradientPaint) {}
85    fn set_fill_pattern(&mut self, _pattern: PatternPaint) {}
86    fn set_stroke_linear_gradient(&mut self, _gradient: LinearGradientPaint) {}
87    fn set_stroke_radial_gradient(&mut self, _gradient: RadialGradientPaint) {}
88    fn set_stroke_pattern(&mut self, _pattern: PatternPaint) {}
89    fn supports_fill_linear_gradient(&self) -> bool {
90        false
91    }
92    fn supports_fill_radial_gradient(&self) -> bool {
93        false
94    }
95    fn supports_fill_pattern(&self) -> bool {
96        false
97    }
98    fn supports_stroke_linear_gradient(&self) -> bool {
99        false
100    }
101    fn supports_stroke_radial_gradient(&self) -> bool {
102        false
103    }
104    fn supports_stroke_pattern(&self) -> bool {
105        false
106    }
107    fn set_line_width(&mut self, w: f64);
108    fn set_line_join(&mut self, join: LineJoin);
109    fn set_line_cap(&mut self, cap: LineCap);
110    fn set_miter_limit(&mut self, limit: f64);
111    fn set_line_dash(&mut self, dashes: &[f64], offset: f64);
112    fn set_blend_mode(&mut self, mode: CompOp);
113    fn set_global_alpha(&mut self, alpha: f64);
114    fn set_fill_rule(&mut self, rule: FillRule);
115
116    // ── Font ──────────────────────────────────────────────────────────────────
117
118    fn set_font(&mut self, font: Arc<Font>);
119    fn set_font_size(&mut self, size: f64);
120
121    // ── Clipping ──────────────────────────────────────────────────────────────
122
123    fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64);
124    fn reset_clip(&mut self);
125
126    // ── Clear ─────────────────────────────────────────────────────────────────
127
128    /// Fill the entire render target with `color`, ignoring the current clip.
129    fn clear(&mut self, color: Color);
130
131    // ── Path building ─────────────────────────────────────────────────────────
132
133    fn begin_path(&mut self);
134    fn move_to(&mut self, x: f64, y: f64);
135    fn line_to(&mut self, x: f64, y: f64);
136    fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64);
137    fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64);
138    fn arc_to(&mut self, cx: f64, cy: f64, r: f64, start_angle: f64, end_angle: f64, ccw: bool);
139
140    /// Add a full circle contour to the current path.
141    fn circle(&mut self, cx: f64, cy: f64, r: f64);
142
143    /// Add an axis-aligned rectangle contour to the current path.
144    fn rect(&mut self, x: f64, y: f64, w: f64, h: f64);
145
146    /// Add a rounded-rectangle contour to the current path.
147    fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64);
148
149    fn close_path(&mut self);
150
151    // ── Path drawing ──────────────────────────────────────────────────────────
152
153    fn fill(&mut self);
154    fn stroke(&mut self);
155    fn fill_and_stroke(&mut self);
156
157    /// Submit **pre-tessellated** AA triangles with per-vertex coverage
158    /// (`x`, `y`, `alpha`) and triangle indices.
159    ///
160    /// This is the fast path for callers that tessellate their geometry
161    /// ONCE at load time (e.g. the Lion demo, SVG icons): they do the
162    /// `tessellate_path_aa` pass themselves, cache the vertex+index
163    /// buffers, then submit them every frame with only a cheap CPU
164    /// transform applied to the x/y components.  Compared to issuing
165    /// `move_to` / `line_to` / `fill` every frame, this keeps the polygon
166    /// set deterministic (no tess2 re-running on subtly-different
167    /// coordinates), avoids thousands of re-tessellations per frame, and
168    /// produces identical output regardless of the widget's transform.
169    ///
170    /// Vertices are `(x_logical_pixels, y_logical_pixels, alpha_0_to_1)`.
171    /// `alpha` is multiplied into the supplied `color.a` in the AA shader
172    /// so halo-strip edge AA survives this fast path.
173    ///
174    /// The software `GfxCtx` ignores the alpha attribute and rasterises
175    /// each triangle as a solid fill — correct but without edge AA, which
176    /// matches the software path's existing stroke/fill behaviour.
177    fn draw_triangles_aa(
178        &mut self,
179        vertices: &[[f32; 3]],
180        indices: &[u32],
181        color: crate::color::Color,
182    );
183
184    // ── Text ──────────────────────────────────────────────────────────────────
185
186    /// Draw `text` with the bottom of the baseline at `(x, y)`.
187    fn fill_text(&mut self, text: &str, x: f64, y: f64);
188
189    /// **Do not call this from application code, ever.**
190    ///
191    /// This is the built-in AGG Glyph-Stroke-Vector fallback font — a
192    /// stroked vector typeface that pre-dates AGG's real text stack. It
193    /// **bypasses every text-rendering facility this framework offers**:
194    ///
195    /// - no font shaping (no kerning, no proper metrics, no UTF-8 fallback),
196    /// - no backbuffer caching (rasterised every frame from scratch),
197    /// - no LCD subpixel rendering (always grayscale outlines),
198    /// - no theme integration (ignores `Visuals::text_color`),
199    /// - no integration with [`crate::font_settings`] (system font,
200    ///   font-size scale, hinting, gamma, etc. all ignored).
201    ///
202    /// It exists only as an internal bootstrap path so the framework can
203    /// draw the very first frame before a real [`crate::text::Font`] has
204    /// loaded, and so a handful of `agg-rust` reference demos that
205    /// specifically test the GSV path stay reproducible. Outside those
206    /// two contexts there is **no situation where calling this is
207    /// correct** — including for "quick debug text," diagnostics
208    /// overlays, perf labels, FPS counters, watermarks, anything.
209    ///
210    /// Use a [`crate::widgets::Label`] widget instead. `Label` is
211    /// backbuffer-cached by default and uses LCD subpixel rendering
212    /// when the global toggle is on (which itself defaults to "on at
213    /// standard DPI, off at HiDPI" via [`crate::font_settings::lcd_enabled`]).
214    /// If you genuinely need imperative text inside a custom widget's
215    /// `paint`, call [`set_font`](Self::set_font) + [`fill_text`](Self::fill_text)
216    /// — that goes through the real text stack.
217    ///
218    /// If you find yourself reaching for `fill_text_gsv`, you are
219    /// almost certainly looking at a bug in the calling code; do not
220    /// add it as a workaround. File an issue against agg-gui.
221    fn fill_text_gsv(&mut self, text: &str, x: f64, y: f64, size: f64);
222
223    /// Measure `text` with the current font and font-size settings.
224    fn measure_text(&self, text: &str) -> Option<TextMetrics>;
225
226    // ── Transform ─────────────────────────────────────────────────────────────
227
228    /// Current accumulated transform (CTM).
229    fn transform(&self) -> TransAffine;
230
231    /// Current transform expressed in the root render target's coordinate
232    /// space, even when drawing inside an offscreen layer whose local CTM was
233    /// reset to identity. Global overlays use this to submit app-level bounds.
234    fn root_transform(&self) -> TransAffine {
235        self.transform()
236    }
237
238    fn save(&mut self);
239    fn restore(&mut self);
240    fn translate(&mut self, tx: f64, ty: f64);
241    fn rotate(&mut self, radians: f64);
242    fn scale(&mut self, sx: f64, sy: f64);
243    fn set_transform(&mut self, m: TransAffine);
244    fn reset_transform(&mut self);
245
246    /// **Opt-in** pixel snapping.  Strips the fractional part of the current
247    /// CTM translation so subsequent integer-coordinate `rect` / `fill` /
248    /// `stroke` / `draw_image_rgba*` calls land exactly on the physical pixel
249    /// grid — no AA fringe on edges, no LINEAR-filter blur on 1:1 texture
250    /// blits.
251    ///
252    /// Call this ONLY when the widget genuinely wants pixel-aligned drawing
253    /// (text backbuffers, pixel-alignment diagnostics, crisp UI strokes).
254    /// Sub-pixel positioning remains the default — e.g. a smooth-scrolling
255    /// panel or an animated marker may legitimately want a fractional offset.
256    /// Typical usage:
257    /// ```ignore
258    /// ctx.save();
259    /// ctx.snap_to_pixel();
260    /// ctx.rect(0.0, 0.0, 10.0, 10.0);
261    /// ctx.fill();
262    /// ctx.restore();
263    /// ```
264    ///
265    /// Only the translation component is affected; rotations and non-uniform
266    /// scales pass through untouched (pixel alignment under those transforms
267    /// isn't well defined, and forcing a snap would visibly jitter rotated
268    /// content).
269    fn snap_to_pixel(&mut self) {
270        let t = self.transform();
271        let fx = t.tx - t.tx.floor();
272        let fy = t.ty - t.ty.floor();
273        if fx != 0.0 || fy != 0.0 {
274            self.translate(-fx, -fy);
275        }
276    }
277
278    // ── Compositing layers ────────────────────────────────────────────────────
279
280    /// Begin a new transparent compositing layer of the given pixel dimensions.
281    ///
282    /// All subsequent drawing (by this widget and its descendants) is redirected
283    /// into the new layer until [`pop_layer`] is called.  Layers nest: each
284    /// `push_layer` must be matched by exactly one `pop_layer`.
285    ///
286    /// The current accumulated transform records the layer's screen-space origin;
287    /// drawing inside the layer uses a fresh local-space transform (origin 0,0).
288    ///
289    /// Implementations that do not support layers (e.g. the GL path) may leave
290    /// this as a no-op — the widget renders pass-through into the parent target.
291    fn push_layer(&mut self, _width: f64, _height: f64) {}
292
293    /// Whether this backend implements real offscreen compositing layers.
294    ///
295    /// The default is `false` so widgets can opt into layer-based rendering
296    /// without forcing every backend to pay for, or emulate, that feature.
297    fn supports_compositing_layers(&self) -> bool {
298        false
299    }
300
301    /// Whether this backend can retain named offscreen layers across frames.
302    ///
303    /// Generic compositing support is enough for isolated opacity groups, but
304    /// retained widget backbuffers need a backend-owned surface keyed by ID.
305    fn supports_retained_layers(&self) -> bool {
306        false
307    }
308
309    /// Begin a new transparent compositing layer that will be multiplied by
310    /// `alpha` when composited back into the parent target.
311    ///
312    /// Backends that do not support layer alpha can fall back to `push_layer`;
313    /// callers gate this through [`supports_compositing_layers`].
314    fn push_layer_with_alpha(&mut self, width: f64, height: f64, _alpha: f64) {
315        self.push_layer(width, height);
316    }
317
318    /// Constrain subsequent drawing in the current layer to a rounded-rect
319    /// mask. Used by window layers after shadows are drawn so chrome/content
320    /// cannot write into rounded transparent corners.
321    ///
322    /// This is a containment clip, not the visual antialiasing edge. Backends
323    /// should leave enough room for partially-transparent edge pixels so the
324    /// caller's normal alpha coverage can feather corners and edges.
325    fn set_layer_rounded_clip(&mut self, _x: f64, _y: f64, _w: f64, _h: f64, _r: f64) {}
326
327    /// Composite a previously retained backend layer. Returns `true` when
328    /// the backend had a retained surface for `key` and drew it.
329    fn composite_retained_layer(
330        &mut self,
331        _key: u64,
332        _width: f64,
333        _height: f64,
334        _alpha: f64,
335    ) -> bool {
336        false
337    }
338
339    /// Begin rendering into a retained backend layer identified by `key`.
340    /// Backends that do not retain layers may fall back to a transient layer.
341    fn push_retained_layer_with_alpha(&mut self, _key: u64, width: f64, height: f64, alpha: f64) {
342        self.push_layer_with_alpha(width, height, alpha);
343    }
344
345    /// Composite the current layer back into the previous render target using
346    /// SrcOver alpha blending, then discard the layer.
347    ///
348    /// Must be called after a matching `push_layer`.  Unmatched calls are ignored.
349    fn pop_layer(&mut self) {}
350
351    // ── GL / GPU content ──────────────────────────────────────────────────────
352
353    /// Render GPU content (3-D scene, video frame, etc.) inline at the correct
354    /// painter-order position.
355    ///
356    /// `screen_rect` is the widget's screen-space rect in Y-up coordinates
357    /// (i.e. `ctx.transform()` origin + `widget.bounds().size`).
358    ///
359    /// The GL implementation executes `painter.gl_paint()` immediately so that
360    /// any 2-D widgets painted after this call naturally overdraw the GPU
361    /// content — correct back-to-front ordering with no post-frame fixup.
362    ///
363    /// The **software (`GfxCtx`) path is a no-op**: widgets should draw a 2-D
364    /// placeholder before calling this method so the software render has
365    /// something visible.
366    fn gl_paint(&mut self, _screen_rect: Rect, _painter: &mut dyn GlPaint) {}
367
368    // ── LCD mask compositing ──────────────────────────────────────────────────
369
370    /// Composite a pre-rasterized LCD subpixel mask onto the current
371    /// render target, mixing `src_color` into the destination through
372    /// per-channel coverage.
373    ///
374    /// `mask` is three bytes per pixel (`cov_r`, `cov_g`, `cov_b`) as
375    /// produced by [`crate::text_lcd::rasterize_lcd_mask`].  The caller
376    /// specifies `(dst_x, dst_y)` in local coordinates (Y-up in our
377    /// convention) and `mask_w × mask_h` to tell the backend the mask's
378    /// dimensions.
379    ///
380    /// Per-channel source-over blend:
381    /// ```text
382    /// dst.r = src.r * mask.r + dst.r * (1 - mask.r)
383    /// dst.g = src.g * mask.g + dst.g * (1 - mask.g)
384    /// dst.b = src.b * mask.b + dst.b * (1 - mask.b)
385    /// ```
386    ///
387    /// **This is the universal "composite LCD text onto arbitrary bg"
388    /// primitive** — it replaces the prior walk / sample / pre-fill
389    /// approach.  Software ctx implements it as an inner-loop blend; the
390    /// GL ctx implements it via a dual-source-blend fragment shader.
391    /// Backends that haven't wired it yet use the default no-op, which
392    /// makes callers fall back to grayscale AA.
393    fn draw_lcd_mask(
394        &mut self,
395        _mask: &[u8],
396        _mask_w: u32,
397        _mask_h: u32,
398        _src_color: Color,
399        _dst_x: f64,
400        _dst_y: f64,
401    ) {
402    }
403
404    /// Arc-keyed variant so GL backends can cache the uploaded texture
405    /// on the `Arc`'s pointer identity — one `glTexImage2D` per unique
406    /// raster, lifetime tied to the mask's strong-ref count.  Software
407    /// backends fall through to the slice path.
408    fn draw_lcd_mask_arc(
409        &mut self,
410        mask: &std::sync::Arc<Vec<u8>>,
411        mask_w: u32,
412        mask_h: u32,
413        src_color: Color,
414        dst_x: f64,
415        dst_y: f64,
416    ) {
417        self.draw_lcd_mask(mask.as_slice(), mask_w, mask_h, src_color, dst_x, dst_y);
418    }
419
420    /// Returns `true` if this backend supports [`draw_lcd_mask`] — i.e.
421    /// it can composite per-channel LCD coverage onto the active target.
422    /// Label queries this to decide between the LCD and grayscale AA
423    /// paths; a backend that returns `false` will never see LCD text.
424    fn has_lcd_mask_composite(&self) -> bool {
425        false
426    }
427
428    // ── Image blitting ────────────────────────────────────────────────────────
429
430    /// Returns `true` if this context implements `draw_image_rgba` with actual
431    /// pixel blitting.  `Label` (and any other widget that uses a software
432    /// backbuffer) gates its cache path on this method so it can fall back to
433    /// direct `fill_text()` on render targets that don't support blitting
434    /// (e.g. the GL path).
435    ///
436    /// Default: `false`.  Override to `true` in `GfxCtx`.
437    fn has_image_blit(&self) -> bool {
438        false
439    }
440
441    /// Draw raw RGBA pixel data into `dst_rect` (Y-up local coordinates).
442    ///
443    /// `data` must be `img_w * img_h * 4` bytes of tightly-packed RGBA8 data
444    /// in row-major order, **top-row first** (Y-down image storage convention).
445    /// The image is scaled to fit `(dst_x, dst_y, dst_w, dst_h)`.
446    ///
447    /// Default implementation: no-op (GL path or software paths that do not
448    /// implement blitting can leave this as a placeholder).
449    fn draw_image_rgba(
450        &mut self,
451        data: &[u8],
452        img_w: u32,
453        img_h: u32,
454        dst_x: f64,
455        dst_y: f64,
456        dst_w: f64,
457        dst_h: f64,
458    ) {
459        let _ = (data, img_w, img_h, dst_x, dst_y, dst_w, dst_h);
460    }
461
462    /// Same as [`draw_image_rgba`] but accepts an `Arc<Vec<u8>>` so the GL
463    /// backend can key its texture cache on the `Arc`'s pointer identity and
464    /// hold a `Weak` ref for automatic cleanup when the underlying buffer is
465    /// dropped — the pattern MatterCAD implements with C# `ConditionalWeakTable`.
466    ///
467    /// Used by `Label` (and future glyph-atlas consumers) in tandem with the
468    /// crate-level [`image_cache`](crate::image_cache) so that rebuilt widget
469    /// trees with unchanged content never re-rasterize OR re-upload.
470    ///
471    /// Default implementation: forward to [`draw_image_rgba`] via slice
472    /// borrow.  Software backends don't benefit from GPU texture caching so
473    /// the default is usually fine; the GL backend overrides.
474    fn draw_image_rgba_arc(
475        &mut self,
476        data: &std::sync::Arc<Vec<u8>>,
477        img_w: u32,
478        img_h: u32,
479        dst_x: f64,
480        dst_y: f64,
481        dst_w: f64,
482        dst_h: f64,
483    ) {
484        self.draw_image_rgba(data.as_slice(), img_w, img_h, dst_x, dst_y, dst_w, dst_h);
485    }
486
487    /// Blit `data` as a textured quad whose four destination corners
488    /// are supplied explicitly. Caller is responsible for choosing the
489    /// corners (typically the projection of a 3-D rotated card onto
490    /// the 2-D viewport). `corners` is ordered **bottom-left,
491    /// bottom-right, top-right, top-left** in agg-gui's Y-up local
492    /// coordinate space, and is fed through the current CTM the same
493    /// way axis-aligned blits are.
494    ///
495    /// Backends that can't render a perspective-distorted quad
496    /// (software fallback) fall back on the axis-aligned bounding
497    /// rect of the four corners.
498    fn draw_image_rgba_corners(
499        &mut self,
500        data: &std::sync::Arc<Vec<u8>>,
501        img_w: u32,
502        img_h: u32,
503        corners: [(f64, f64); 4],
504    ) {
505        // Default: bounding-rect fallback. The wgpu backend overrides
506        // with a real 4-corner textured-quad draw.
507        let (min_x, min_y, max_x, max_y) = corners
508            .iter()
509            .fold((f64::MAX, f64::MAX, f64::MIN, f64::MIN), |a, c| {
510                (a.0.min(c.0), a.1.min(c.1), a.2.max(c.0), a.3.max(c.1))
511            });
512        self.draw_image_rgba_arc(
513            data,
514            img_w,
515            img_h,
516            min_x,
517            min_y,
518            (max_x - min_x).max(0.0),
519            (max_y - min_y).max(0.0),
520        );
521    }
522
523    // ── LCD backbuffer blit ───────────────────────────────────────────────────
524
525    /// Composite a two-plane `LcdCoverage`-mode backbuffer onto the active
526    /// render target at `(dst_x, dst_y)` with size `(dst_w, dst_h)` (in
527    /// local coords).  Inputs are two `Arc<Vec<u8>>`, each 3 bytes per
528    /// pixel, **top-row-first**:
529    ///
530    /// - `color`: premultiplied per-channel RGB.
531    /// - `alpha`: per-channel alpha (coverage).
532    ///
533    /// The compositor applies per-channel premultiplied src-over:
534    ///
535    /// ```text
536    /// dst.ch := src.color_ch + dst.ch * (1 - src.alpha_ch)
537    /// ```
538    ///
539    /// which preserves LCD subpixel chroma through the cache round-trip.
540    /// Used by [`crate::widget::paint_subtree_backbuffered`] when a widget's
541    /// [`crate::widget::BackbufferMode::LcdCoverage`] cache is ready to
542    /// composite onto its parent.
543    ///
544    /// **Default:** collapses the two planes into a single straight-alpha
545    /// RGBA8 image (max of channel alphas, divided back to straight colour)
546    /// and forwards to [`draw_image_rgba`].  Correct for any content where
547    /// the three channel alphas agree; lossy of LCD chroma where they
548    /// diverge.  Backends that want full subpixel quality through the
549    /// cache override this with a two-texture shader path.
550    fn draw_lcd_backbuffer_arc(
551        &mut self,
552        color: &std::sync::Arc<Vec<u8>>,
553        alpha: &std::sync::Arc<Vec<u8>>,
554        w: u32,
555        h: u32,
556        dst_x: f64,
557        dst_y: f64,
558        dst_w: f64,
559        dst_h: f64,
560    ) {
561        // Collapse to straight-alpha RGBA8 on the fly.  Matches the same
562        // math `LcdBuffer::to_rgba8_top_down_collapsed` uses internally,
563        // except applied to a top-down pair rather than a Y-up pair.
564        let w_u = w as usize;
565        let h_u = h as usize;
566        if color.len() < w_u * h_u * 3 || alpha.len() < w_u * h_u * 3 {
567            return;
568        }
569        let mut rgba = vec![0u8; w_u * h_u * 4];
570        for i in 0..(w_u * h_u) {
571            let ci = i * 3;
572            let ra = alpha[ci];
573            let ga = alpha[ci + 1];
574            let ba = alpha[ci + 2];
575            let a = ra.max(ga).max(ba);
576            if a == 0 {
577                continue;
578            }
579            let af = a as f32 / 255.0;
580            let rc = color[ci] as f32 / 255.0;
581            let gc = color[ci + 1] as f32 / 255.0;
582            let bc = color[ci + 2] as f32 / 255.0;
583            let di = i * 4;
584            rgba[di] = ((rc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
585            rgba[di + 1] = ((gc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
586            rgba[di + 2] = ((bc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
587            rgba[di + 3] = a;
588        }
589        self.draw_image_rgba(&rgba, w, h, dst_x, dst_y, dst_w, dst_h);
590    }
591
592    // ── Screenshot capture (GPU-direct path) ──────────────────────────────────
593    //
594    // Hardware-accelerated screenshot pipeline.  The capture lives on the GPU
595    // as a backend-internal texture, so the live preview pane samples it
596    // directly with proper downsample filtering — no CPU readback per frame,
597    // no re-upload, no mipmap generation in the hot path.  Pixels are pulled
598    // back to system memory only when the user actually clicks Save / Copy.
599    //
600    // Default impls are no-ops returning `false` / empty so the software
601    // backend stays unchanged: the screenshot widget falls back to the
602    // existing `draw_image_rgba_arc` + Vec<u8> path automatically.
603
604    /// Snapshot the current frame's surface into the backend's internal
605    /// screenshot texture (allocating / resizing as needed).  Must be
606    /// called inside the active frame, after `end_frame` has flushed the
607    /// 2-D render but before the platform shell calls present.
608    ///
609    /// Returns `true` if the backend supports the capture path.
610    fn capture_screenshot(&mut self) -> bool {
611        false
612    }
613
614    /// True if a previously-captured screenshot is held by the backend
615    /// and available for [`Self::draw_captured_screenshot`].
616    fn has_captured_screenshot(&self) -> bool {
617        false
618    }
619
620    /// Dimensions of the held capture, or `None` when no capture exists.
621    fn captured_screenshot_size(&self) -> Option<(u32, u32)> {
622        None
623    }
624
625    /// Draw the held capture into `(dst_x, dst_y, dst_w, dst_h)` using the
626    /// backend's preferred filtered sampling.  Returns `true` if the
627    /// capture exists and was drawn.
628    fn draw_captured_screenshot(
629        &mut self,
630        _dst_x: f64,
631        _dst_y: f64,
632        _dst_w: f64,
633        _dst_h: f64,
634    ) -> bool {
635        false
636    }
637
638    /// Read the held capture's pixels back to CPU memory as Y-down RGBA8 —
639    /// for Save / Copy.  This is intentionally a single-shot synchronous
640    /// readback; widgets should NOT call this every frame.  Returns
641    /// `(empty, 0, 0)` on backends without a capture or without GPU
642    /// readback support.
643    fn read_captured_screenshot(&mut self) -> (Vec<u8>, u32, u32) {
644        (Vec::new(), 0, 0)
645    }
646
647    // ── Theme / Visuals ───────────────────────────────────────────────────────
648
649    /// Return the currently-active [`Visuals`] palette.
650    ///
651    /// Delegates to [`crate::theme::current_visuals`], which reads the
652    /// thread-local set by [`crate::theme::set_visuals`].  Widget `paint()`
653    /// implementations call this to get colours instead of hardcoding them.
654    fn visuals(&self) -> Visuals {
655        crate::theme::current_visuals()
656    }
657}