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 /// Declare that the current layer is now covered by opaque content, so
328 /// text drawn into it lands on destination alpha 1.
329 ///
330 /// This re-enables LCD subpixel text inside the layer. Subpixel
331 /// coverage is only meaningful against an opaque destination: a fresh
332 /// layer texture is cleared to alpha 0, and the per-channel composite
333 /// writes colour without alpha, so glyphs over a transparent layer
334 /// would stay at alpha 0 and blend additively toward white on pop.
335 /// Once an opaque body has been painted, that hazard is gone and
336 /// subpixel rendering is as correct as it is on the backbuffer.
337 ///
338 /// Call it *after* filling the layer, not at push time. Widgets that
339 /// composite genuinely translucent content (an opacity fade) must not
340 /// call it; they keep the grayscale, alpha-writing text path.
341 ///
342 /// No-op by default and at the top level, where the target is already
343 /// opaque.
344 fn set_layer_opaque_backdrop(&mut self, _opaque: bool) {}
345
346 /// Composite a previously retained backend layer. Returns `true` when
347 /// the backend had a retained surface for `key` and drew it.
348 fn composite_retained_layer(
349 &mut self,
350 _key: u64,
351 _width: f64,
352 _height: f64,
353 _alpha: f64,
354 ) -> bool {
355 false
356 }
357
358 /// Begin rendering into a retained backend layer identified by `key`.
359 /// Backends that do not retain layers may fall back to a transient layer.
360 fn push_retained_layer_with_alpha(&mut self, _key: u64, width: f64, height: f64, alpha: f64) {
361 self.push_layer_with_alpha(width, height, alpha);
362 }
363
364 /// Composite the current layer back into the previous render target using
365 /// SrcOver alpha blending, then discard the layer.
366 ///
367 /// Must be called after a matching `push_layer`. Unmatched calls are ignored.
368 fn pop_layer(&mut self) {}
369
370 // ── GL / GPU content ──────────────────────────────────────────────────────
371
372 /// Render GPU content (3-D scene, video frame, etc.) inline at the correct
373 /// painter-order position.
374 ///
375 /// `screen_rect` is the widget's screen-space rect in Y-up coordinates
376 /// (i.e. `ctx.transform()` origin + `widget.bounds().size`).
377 ///
378 /// The GL implementation executes `painter.gl_paint()` immediately so that
379 /// any 2-D widgets painted after this call naturally overdraw the GPU
380 /// content — correct back-to-front ordering with no post-frame fixup.
381 ///
382 /// The **software (`GfxCtx`) path is a no-op**: widgets should draw a 2-D
383 /// placeholder before calling this method so the software render has
384 /// something visible.
385 fn gl_paint(&mut self, _screen_rect: Rect, _painter: &mut dyn GlPaint) {}
386
387 // ── LCD mask compositing ──────────────────────────────────────────────────
388
389 /// Composite a pre-rasterized LCD subpixel mask onto the current
390 /// render target, mixing `src_color` into the destination through
391 /// per-channel coverage.
392 ///
393 /// `mask` is three bytes per pixel (`cov_r`, `cov_g`, `cov_b`) as
394 /// produced by [`crate::text_lcd::rasterize_lcd_mask`]. The caller
395 /// specifies `(dst_x, dst_y)` in local coordinates (Y-up in our
396 /// convention) and `mask_w × mask_h` to tell the backend the mask's
397 /// dimensions.
398 ///
399 /// Per-channel source-over blend:
400 /// ```text
401 /// dst.r = src.r * mask.r + dst.r * (1 - mask.r)
402 /// dst.g = src.g * mask.g + dst.g * (1 - mask.g)
403 /// dst.b = src.b * mask.b + dst.b * (1 - mask.b)
404 /// ```
405 ///
406 /// **This is the universal "composite LCD text onto arbitrary bg"
407 /// primitive** — it replaces the prior walk / sample / pre-fill
408 /// approach. Software ctx implements it as an inner-loop blend; the
409 /// GL ctx implements it via a dual-source-blend fragment shader.
410 /// Backends that haven't wired it yet use the default no-op, which
411 /// makes callers fall back to grayscale AA.
412 fn draw_lcd_mask(
413 &mut self,
414 _mask: &[u8],
415 _mask_w: u32,
416 _mask_h: u32,
417 _src_color: Color,
418 _dst_x: f64,
419 _dst_y: f64,
420 ) {
421 }
422
423 /// Arc-keyed variant so GL backends can cache the uploaded texture
424 /// on the `Arc`'s pointer identity — one `glTexImage2D` per unique
425 /// raster, lifetime tied to the mask's strong-ref count. Software
426 /// backends fall through to the slice path.
427 fn draw_lcd_mask_arc(
428 &mut self,
429 mask: &std::sync::Arc<Vec<u8>>,
430 mask_w: u32,
431 mask_h: u32,
432 src_color: Color,
433 dst_x: f64,
434 dst_y: f64,
435 ) {
436 self.draw_lcd_mask(mask.as_slice(), mask_w, mask_h, src_color, dst_x, dst_y);
437 }
438
439 /// Returns `true` if this backend supports [`draw_lcd_mask`] — i.e.
440 /// it can composite per-channel LCD coverage onto the active target.
441 /// Label queries this to decide between the LCD and grayscale AA
442 /// paths; a backend that returns `false` will never see LCD text.
443 fn has_lcd_mask_composite(&self) -> bool {
444 false
445 }
446
447 // ── Image blitting ────────────────────────────────────────────────────────
448
449 /// Returns `true` if this context implements `draw_image_rgba` with actual
450 /// pixel blitting. `Label` (and any other widget that uses a software
451 /// backbuffer) gates its cache path on this method so it can fall back to
452 /// direct `fill_text()` on render targets that don't support blitting
453 /// (e.g. the GL path).
454 ///
455 /// Default: `false`. Override to `true` in `GfxCtx`.
456 fn has_image_blit(&self) -> bool {
457 false
458 }
459
460 /// Draw raw RGBA pixel data into `dst_rect` (Y-up local coordinates).
461 ///
462 /// `data` must be `img_w * img_h * 4` bytes of tightly-packed RGBA8 data
463 /// in row-major order, **top-row first** (Y-down image storage convention).
464 /// The image is scaled to fit `(dst_x, dst_y, dst_w, dst_h)`.
465 ///
466 /// Default implementation: no-op (GL path or software paths that do not
467 /// implement blitting can leave this as a placeholder).
468 fn draw_image_rgba(
469 &mut self,
470 data: &[u8],
471 img_w: u32,
472 img_h: u32,
473 dst_x: f64,
474 dst_y: f64,
475 dst_w: f64,
476 dst_h: f64,
477 ) {
478 let _ = (data, img_w, img_h, dst_x, dst_y, dst_w, dst_h);
479 }
480
481 /// Same as [`draw_image_rgba`] but accepts an `Arc<Vec<u8>>` so the GL
482 /// backend can key its texture cache on the `Arc`'s pointer identity and
483 /// hold a `Weak` ref for automatic cleanup when the underlying buffer is
484 /// dropped — the pattern MatterCAD implements with C# `ConditionalWeakTable`.
485 ///
486 /// Used by `Label` (and future glyph-atlas consumers) in tandem with the
487 /// crate-level [`image_cache`](crate::image_cache) so that rebuilt widget
488 /// trees with unchanged content never re-rasterize OR re-upload.
489 ///
490 /// Default implementation: forward to [`draw_image_rgba`] via slice
491 /// borrow. Software backends don't benefit from GPU texture caching so
492 /// the default is usually fine; the GL backend overrides.
493 fn draw_image_rgba_arc(
494 &mut self,
495 data: &std::sync::Arc<Vec<u8>>,
496 img_w: u32,
497 img_h: u32,
498 dst_x: f64,
499 dst_y: f64,
500 dst_w: f64,
501 dst_h: f64,
502 ) {
503 self.draw_image_rgba(data.as_slice(), img_w, img_h, dst_x, dst_y, dst_w, dst_h);
504 }
505
506 /// Blit `data` as a textured quad whose four destination corners
507 /// are supplied explicitly. Caller is responsible for choosing the
508 /// corners (typically the projection of a 3-D rotated card onto
509 /// the 2-D viewport). `corners` is ordered **bottom-left,
510 /// bottom-right, top-right, top-left** in agg-gui's Y-up local
511 /// coordinate space, and is fed through the current CTM the same
512 /// way axis-aligned blits are.
513 ///
514 /// Backends that can't render a perspective-distorted quad
515 /// (software fallback) fall back on the axis-aligned bounding
516 /// rect of the four corners.
517 fn draw_image_rgba_corners(
518 &mut self,
519 data: &std::sync::Arc<Vec<u8>>,
520 img_w: u32,
521 img_h: u32,
522 corners: [(f64, f64); 4],
523 ) {
524 // Default: bounding-rect fallback. The wgpu backend overrides
525 // with a real 4-corner textured-quad draw.
526 let (min_x, min_y, max_x, max_y) = corners
527 .iter()
528 .fold((f64::MAX, f64::MAX, f64::MIN, f64::MIN), |a, c| {
529 (a.0.min(c.0), a.1.min(c.1), a.2.max(c.0), a.3.max(c.1))
530 });
531 self.draw_image_rgba_arc(
532 data,
533 img_w,
534 img_h,
535 min_x,
536 min_y,
537 (max_x - min_x).max(0.0),
538 (max_y - min_y).max(0.0),
539 );
540 }
541
542 // ── LCD backbuffer blit ───────────────────────────────────────────────────
543
544 /// Composite a two-plane `LcdCoverage`-mode backbuffer onto the active
545 /// render target at `(dst_x, dst_y)` with size `(dst_w, dst_h)` (in
546 /// local coords). Inputs are two `Arc<Vec<u8>>`, each 3 bytes per
547 /// pixel, **top-row-first**:
548 ///
549 /// - `color`: premultiplied per-channel RGB.
550 /// - `alpha`: per-channel alpha (coverage).
551 ///
552 /// The compositor applies per-channel premultiplied src-over:
553 ///
554 /// ```text
555 /// dst.ch := src.color_ch + dst.ch * (1 - src.alpha_ch)
556 /// ```
557 ///
558 /// which preserves LCD subpixel chroma through the cache round-trip.
559 /// Used by [`crate::widget::paint_subtree_backbuffered`] when a widget's
560 /// [`crate::widget::BackbufferMode::LcdCoverage`] cache is ready to
561 /// composite onto its parent.
562 ///
563 /// `content_version` is a monotone, process-globally-unique content
564 /// revision (see `crate::widget::next_content_version`) stamped whenever
565 /// the published planes change. Backends that key a GPU texture cache on the
566 /// `Arc` buffer *identity* pair it with the pointer so an in-place strip edit
567 /// (which keeps the buffer address stable through `Arc::make_mut`) still
568 /// forces a re-upload. CPU backends ignore it.
569 ///
570 /// **Default:** collapses the two planes into a single straight-alpha
571 /// RGBA8 image via [`crate::lcd_coverage::collapse_lcd_pixel`]
572 /// (Rec.709 luminance-weighted alpha, lifted so the unpremultiply cannot
573 /// clamp) and forwards to [`draw_image_rgba`]. Lossy of LCD chroma where
574 /// the three channel alphas diverge, but luminance-preserving. Backends
575 /// that want full subpixel quality through the cache override this with a
576 /// two-texture shader path.
577 ///
578 /// This default is live CPU code, not a fallback stub: `LcdGfxCtx` does not
579 /// override it, so a nested `BackbufferMode::LcdCoverage` widget blitting
580 /// into a parent LCD backbuffer lands here.
581 fn draw_lcd_backbuffer_arc(
582 &mut self,
583 color: &std::sync::Arc<Vec<u8>>,
584 alpha: &std::sync::Arc<Vec<u8>>,
585 content_version: u64,
586 w: u32,
587 h: u32,
588 dst_x: f64,
589 dst_y: f64,
590 dst_w: f64,
591 dst_h: f64,
592 ) {
593 let _ = content_version; // CPU collapse path is stateless; version unused.
594 // Collapse to straight-alpha RGBA8 on the fly through the SAME shared
595 // per-pixel rule `LcdBuffer::to_rgba8_top_down_collapsed` uses — only
596 // the row order differs (this pair is already top-down). Keeping the
597 // math in one place is deliberate: this site kept an independent `max`
598 // collapse after the other was fixed, which is how the "LCD text is
599 // bolder" bug stayed alive on the nested-backbuffer path.
600 let w_u = w as usize;
601 let h_u = h as usize;
602 if color.len() < w_u * h_u * 3 || alpha.len() < w_u * h_u * 3 {
603 return;
604 }
605 let mut rgba = vec![0u8; w_u * h_u * 4];
606 for i in 0..(w_u * h_u) {
607 let ci = i * 3;
608 let di = i * 4;
609 let px = crate::lcd_coverage::collapse_lcd_pixel(
610 [color[ci], color[ci + 1], color[ci + 2]],
611 [alpha[ci], alpha[ci + 1], alpha[ci + 2]],
612 );
613 rgba[di..di + 4].copy_from_slice(&px);
614 }
615 self.draw_image_rgba(&rgba, w, h, dst_x, dst_y, dst_w, dst_h);
616 }
617
618 // ── Screenshot capture (GPU-direct path) ──────────────────────────────────
619 //
620 // Hardware-accelerated screenshot pipeline. The capture lives on the GPU
621 // as a backend-internal texture, so the live preview pane samples it
622 // directly with proper downsample filtering — no CPU readback per frame,
623 // no re-upload, no mipmap generation in the hot path. Pixels are pulled
624 // back to system memory only when the user actually clicks Save / Copy.
625 //
626 // Default impls are no-ops returning `false` / empty so the software
627 // backend stays unchanged: the screenshot widget falls back to the
628 // existing `draw_image_rgba_arc` + Vec<u8> path automatically.
629
630 /// Snapshot the current frame's surface into the backend's internal
631 /// screenshot texture (allocating / resizing as needed). Must be
632 /// called inside the active frame, after `end_frame` has flushed the
633 /// 2-D render but before the platform shell calls present.
634 ///
635 /// Returns `true` if the backend supports the capture path.
636 fn capture_screenshot(&mut self) -> bool {
637 false
638 }
639
640 /// True if a previously-captured screenshot is held by the backend
641 /// and available for [`Self::draw_captured_screenshot`].
642 fn has_captured_screenshot(&self) -> bool {
643 false
644 }
645
646 /// Dimensions of the held capture, or `None` when no capture exists.
647 fn captured_screenshot_size(&self) -> Option<(u32, u32)> {
648 None
649 }
650
651 /// Draw the held capture into `(dst_x, dst_y, dst_w, dst_h)` using the
652 /// backend's preferred filtered sampling. Returns `true` if the
653 /// capture exists and was drawn.
654 fn draw_captured_screenshot(
655 &mut self,
656 _dst_x: f64,
657 _dst_y: f64,
658 _dst_w: f64,
659 _dst_h: f64,
660 ) -> bool {
661 false
662 }
663
664 /// Read the held capture's pixels back to CPU memory as Y-down RGBA8 —
665 /// for Save / Copy. This is intentionally a single-shot synchronous
666 /// readback; widgets should NOT call this every frame. Returns
667 /// `(empty, 0, 0)` on backends without a capture or without GPU
668 /// readback support.
669 fn read_captured_screenshot(&mut self) -> (Vec<u8>, u32, u32) {
670 (Vec::new(), 0, 0)
671 }
672
673 // ── Theme / Visuals ───────────────────────────────────────────────────────
674
675 /// Return the currently-active [`Visuals`] palette.
676 ///
677 /// Delegates to [`crate::theme::current_visuals`], which reads the
678 /// thread-local set by [`crate::theme::set_visuals`]. Widget `paint()`
679 /// implementations call this to get colours instead of hardcoding them.
680 fn visuals(&self) -> Visuals {
681 crate::theme::current_visuals()
682 }
683}