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