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/// Fill rule used when rasterizing closed paths.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum FillRule {
28 /// Non-zero winding rule.
29 NonZero,
30 /// Even-odd parity rule.
31 EvenOdd,
32}
33
34impl Default for FillRule {
35 fn default() -> Self {
36 Self::NonZero
37 }
38}
39
40/// How a gradient behaves outside the normalized `0..=1` range.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum GradientSpread {
43 /// Clamp to the nearest edge stop.
44 Pad,
45 /// Mirror each repeated interval.
46 Reflect,
47 /// Repeat the gradient ramp.
48 Repeat,
49}
50
51impl Default for GradientSpread {
52 fn default() -> Self {
53 Self::Pad
54 }
55}
56
57/// One color stop in a bridge-level gradient paint.
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct GradientStop {
60 pub offset: f64,
61 pub color: Color,
62}
63
64/// Linear gradient fill paint expressed in local drawing coordinates.
65#[derive(Clone, Debug, PartialEq)]
66pub struct LinearGradientPaint {
67 pub x1: f64,
68 pub y1: f64,
69 pub x2: f64,
70 pub y2: f64,
71 pub transform: TransAffine,
72 pub spread: GradientSpread,
73 pub stops: Vec<GradientStop>,
74}
75
76impl LinearGradientPaint {
77 pub fn sample(&self, mut x: f64, mut y: f64) -> Color {
78 if self.stops.is_empty() {
79 return Color::transparent();
80 }
81
82 self.transform.inverse_transform(&mut x, &mut y);
83
84 let dx = self.x2 - self.x1;
85 let dy = self.y2 - self.y1;
86 let len2 = dx * dx + dy * dy;
87 let t = if len2 > f64::EPSILON {
88 ((x - self.x1) * dx + (y - self.y1) * dy) / len2
89 } else {
90 0.0
91 };
92 let t = apply_spread(t, self.spread);
93
94 sample_stops(&self.stops, t)
95 }
96}
97
98/// Radial/focal gradient fill paint expressed in local drawing coordinates.
99#[derive(Clone, Debug, PartialEq)]
100pub struct RadialGradientPaint {
101 pub cx: f64,
102 pub cy: f64,
103 pub r: f64,
104 pub fx: f64,
105 pub fy: f64,
106 pub transform: TransAffine,
107 pub spread: GradientSpread,
108 pub stops: Vec<GradientStop>,
109}
110
111impl RadialGradientPaint {
112 pub fn sample(&self, mut x: f64, mut y: f64) -> Color {
113 if self.stops.is_empty() {
114 return Color::transparent();
115 }
116
117 self.transform.inverse_transform(&mut x, &mut y);
118
119 let dx = x - self.fx;
120 let dy = y - self.fy;
121 let fx = self.fx - self.cx;
122 let fy = self.fy - self.cy;
123 let a = dx * dx + dy * dy;
124 let t = if a <= f64::EPSILON || self.r <= f64::EPSILON {
125 0.0
126 } else {
127 let b = 2.0 * (fx * dx + fy * dy);
128 let c = fx * fx + fy * fy - self.r * self.r;
129 let disc = (b * b - 4.0 * a * c).max(0.0);
130 let k = (-b + disc.sqrt()) / (2.0 * a);
131 if k > f64::EPSILON {
132 1.0 / k
133 } else {
134 0.0
135 }
136 };
137 sample_stops(&self.stops, apply_spread(t, self.spread))
138 }
139}
140
141/// Repeating raster pattern paint expressed in SVG/user drawing coordinates.
142#[derive(Clone, Debug, PartialEq)]
143pub struct PatternPaint {
144 pub x: f64,
145 pub y: f64,
146 pub width: f64,
147 pub height: f64,
148 pub transform: TransAffine,
149 /// Straight-alpha RGBA tile pixels in bottom-up row order.
150 pub pixels: Arc<Vec<u8>>,
151 pub pixel_width: u32,
152 pub pixel_height: u32,
153}
154
155impl PatternPaint {
156 pub fn sample(&self, mut x: f64, mut y: f64) -> Color {
157 if self.width <= f64::EPSILON
158 || self.height <= f64::EPSILON
159 || self.pixel_width == 0
160 || self.pixel_height == 0
161 || self.pixels.is_empty()
162 {
163 return Color::transparent();
164 }
165
166 self.transform.inverse_transform(&mut x, &mut y);
167 let tx = (x - self.x).rem_euclid(self.width);
168 let ty_down = (y - self.y).rem_euclid(self.height);
169 let px = ((tx / self.width) * self.pixel_width as f64)
170 .floor()
171 .clamp(0.0, self.pixel_width.saturating_sub(1) as f64) as usize;
172 let py = (((self.height - ty_down) / self.height) * self.pixel_height as f64)
173 .floor()
174 .clamp(0.0, self.pixel_height.saturating_sub(1) as f64) as usize;
175 let i = (py * self.pixel_width as usize + px) * 4;
176 if i + 3 >= self.pixels.len() {
177 return Color::transparent();
178 }
179
180 Color::rgba(
181 self.pixels[i] as f32 / 255.0,
182 self.pixels[i + 1] as f32 / 255.0,
183 self.pixels[i + 2] as f32 / 255.0,
184 self.pixels[i + 3] as f32 / 255.0,
185 )
186 }
187}
188
189fn apply_spread(t: f64, spread: GradientSpread) -> f64 {
190 match spread {
191 GradientSpread::Pad => t.clamp(0.0, 1.0),
192 GradientSpread::Repeat => t - t.floor(),
193 GradientSpread::Reflect => {
194 let period = t.rem_euclid(2.0);
195 if period <= 1.0 {
196 period
197 } else {
198 2.0 - period
199 }
200 }
201 }
202}
203
204fn sample_stops(stops: &[GradientStop], t: f64) -> Color {
205 if t <= stops[0].offset {
206 return stops[0].color;
207 }
208 for pair in stops.windows(2) {
209 let a = pair[0];
210 let b = pair[1];
211 if t <= b.offset {
212 let span = (b.offset - a.offset).max(f64::EPSILON);
213 let u = ((t - a.offset) / span).clamp(0.0, 1.0) as f32;
214 return lerp_color(a.color, b.color, u);
215 }
216 }
217 stops[stops.len() - 1].color
218}
219
220fn lerp_color(a: Color, b: Color, t: f32) -> Color {
221 Color::rgba(
222 a.r + (b.r - a.r) * t,
223 a.g + (b.g - a.g) * t,
224 a.b + (b.b - a.b) * t,
225 a.a + (b.a - a.a) * t,
226 )
227}
228
229// ---------------------------------------------------------------------------
230// GL paint hook
231// ---------------------------------------------------------------------------
232
233/// Trait for widgets that want to render 3-D (or other GPU) content inline
234/// during the widget paint pass.
235///
236/// `DrawCtx::gl_paint` calls this with an opaque `gl` handle — implementations
237/// downcast it to `glow::Context` (or whatever GL type the platform provides).
238/// The software `GfxCtx` never calls `paint`; see [`DrawCtx::gl_paint`].
239pub trait GlPaint {
240 /// Execute GPU draw calls for the widget's 3-D content.
241 ///
242 /// `gl` — opaque platform GL context; downcast via `std::any::Any`.
243 /// `screen_rect` — Y-up screen-space rect for this widget (for viewport/scissor).
244 /// `full_w`, `full_h` — full viewport dimensions (for restoring after).
245 /// `parent_clip` — current framework scissor rect `[x, y, w, h]` in GL/Y-up
246 /// pixels, or `None` if no clip is active. Implementations **must intersect**
247 /// any scissor they set with this rect so that parent widget clips (e.g. a
248 /// collapsed window) correctly hide GPU-rendered content.
249 fn gl_paint(
250 &mut self,
251 gl: &dyn std::any::Any,
252 screen_rect: Rect,
253 full_w: i32,
254 full_h: i32,
255 parent_clip: Option<[i32; 4]>,
256 );
257}
258
259/// Unified 2-D drawing context.
260///
261/// All coordinate parameters use the **Y-up, first-quadrant** convention:
262/// origin at the bottom-left, positive-Y upward. This matches `GfxCtx` and
263/// the widget tree layout invariant.
264pub trait DrawCtx {
265 // ── State ─────────────────────────────────────────────────────────────────
266
267 fn set_fill_color(&mut self, color: Color);
268 fn set_stroke_color(&mut self, color: Color);
269 fn set_fill_linear_gradient(&mut self, _gradient: LinearGradientPaint) {}
270 fn set_fill_radial_gradient(&mut self, _gradient: RadialGradientPaint) {}
271 fn set_fill_pattern(&mut self, _pattern: PatternPaint) {}
272 fn set_stroke_linear_gradient(&mut self, _gradient: LinearGradientPaint) {}
273 fn set_stroke_radial_gradient(&mut self, _gradient: RadialGradientPaint) {}
274 fn set_stroke_pattern(&mut self, _pattern: PatternPaint) {}
275 fn supports_fill_linear_gradient(&self) -> bool {
276 false
277 }
278 fn supports_fill_radial_gradient(&self) -> bool {
279 false
280 }
281 fn supports_fill_pattern(&self) -> bool {
282 false
283 }
284 fn supports_stroke_linear_gradient(&self) -> bool {
285 false
286 }
287 fn supports_stroke_radial_gradient(&self) -> bool {
288 false
289 }
290 fn supports_stroke_pattern(&self) -> bool {
291 false
292 }
293 fn set_line_width(&mut self, w: f64);
294 fn set_line_join(&mut self, join: LineJoin);
295 fn set_line_cap(&mut self, cap: LineCap);
296 fn set_miter_limit(&mut self, limit: f64);
297 fn set_line_dash(&mut self, dashes: &[f64], offset: f64);
298 fn set_blend_mode(&mut self, mode: CompOp);
299 fn set_global_alpha(&mut self, alpha: f64);
300 fn set_fill_rule(&mut self, rule: FillRule);
301
302 // ── Font ──────────────────────────────────────────────────────────────────
303
304 fn set_font(&mut self, font: Arc<Font>);
305 fn set_font_size(&mut self, size: f64);
306
307 // ── Clipping ──────────────────────────────────────────────────────────────
308
309 fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64);
310 fn reset_clip(&mut self);
311
312 // ── Clear ─────────────────────────────────────────────────────────────────
313
314 /// Fill the entire render target with `color`, ignoring the current clip.
315 fn clear(&mut self, color: Color);
316
317 // ── Path building ─────────────────────────────────────────────────────────
318
319 fn begin_path(&mut self);
320 fn move_to(&mut self, x: f64, y: f64);
321 fn line_to(&mut self, x: f64, y: f64);
322 fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64);
323 fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64);
324 fn arc_to(&mut self, cx: f64, cy: f64, r: f64, start_angle: f64, end_angle: f64, ccw: bool);
325
326 /// Add a full circle contour to the current path.
327 fn circle(&mut self, cx: f64, cy: f64, r: f64);
328
329 /// Add an axis-aligned rectangle contour to the current path.
330 fn rect(&mut self, x: f64, y: f64, w: f64, h: f64);
331
332 /// Add a rounded-rectangle contour to the current path.
333 fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64);
334
335 fn close_path(&mut self);
336
337 // ── Path drawing ──────────────────────────────────────────────────────────
338
339 fn fill(&mut self);
340 fn stroke(&mut self);
341 fn fill_and_stroke(&mut self);
342
343 /// Submit **pre-tessellated** AA triangles with per-vertex coverage
344 /// (`x`, `y`, `alpha`) and triangle indices.
345 ///
346 /// This is the fast path for callers that tessellate their geometry
347 /// ONCE at load time (e.g. the Lion demo, SVG icons): they do the
348 /// `tessellate_path_aa` pass themselves, cache the vertex+index
349 /// buffers, then submit them every frame with only a cheap CPU
350 /// transform applied to the x/y components. Compared to issuing
351 /// `move_to` / `line_to` / `fill` every frame, this keeps the polygon
352 /// set deterministic (no tess2 re-running on subtly-different
353 /// coordinates), avoids thousands of re-tessellations per frame, and
354 /// produces identical output regardless of the widget's transform.
355 ///
356 /// Vertices are `(x_logical_pixels, y_logical_pixels, alpha_0_to_1)`.
357 /// `alpha` is multiplied into the supplied `color.a` in the AA shader
358 /// so halo-strip edge AA survives this fast path.
359 ///
360 /// The software `GfxCtx` ignores the alpha attribute and rasterises
361 /// each triangle as a solid fill — correct but without edge AA, which
362 /// matches the software path's existing stroke/fill behaviour.
363 fn draw_triangles_aa(
364 &mut self,
365 vertices: &[[f32; 3]],
366 indices: &[u32],
367 color: crate::color::Color,
368 );
369
370 // ── Text ──────────────────────────────────────────────────────────────────
371
372 /// Draw `text` with the bottom of the baseline at `(x, y)`.
373 fn fill_text(&mut self, text: &str, x: f64, y: f64);
374
375 /// Draw `text` using the built-in AGG Glyph-Stroke-Vector font at `size`
376 /// pixels. Useful before a proper font is loaded.
377 fn fill_text_gsv(&mut self, text: &str, x: f64, y: f64, size: f64);
378
379 /// Measure `text` with the current font and font-size settings.
380 fn measure_text(&self, text: &str) -> Option<TextMetrics>;
381
382 // ── Transform ─────────────────────────────────────────────────────────────
383
384 /// Current accumulated transform (CTM).
385 fn transform(&self) -> TransAffine;
386
387 /// Current transform expressed in the root render target's coordinate
388 /// space, even when drawing inside an offscreen layer whose local CTM was
389 /// reset to identity. Global overlays use this to submit app-level bounds.
390 fn root_transform(&self) -> TransAffine {
391 self.transform()
392 }
393
394 fn save(&mut self);
395 fn restore(&mut self);
396 fn translate(&mut self, tx: f64, ty: f64);
397 fn rotate(&mut self, radians: f64);
398 fn scale(&mut self, sx: f64, sy: f64);
399 fn set_transform(&mut self, m: TransAffine);
400 fn reset_transform(&mut self);
401
402 /// **Opt-in** pixel snapping. Strips the fractional part of the current
403 /// CTM translation so subsequent integer-coordinate `rect` / `fill` /
404 /// `stroke` / `draw_image_rgba*` calls land exactly on the physical pixel
405 /// grid — no AA fringe on edges, no LINEAR-filter blur on 1:1 texture
406 /// blits.
407 ///
408 /// Call this ONLY when the widget genuinely wants pixel-aligned drawing
409 /// (text backbuffers, pixel-alignment diagnostics, crisp UI strokes).
410 /// Sub-pixel positioning remains the default — e.g. a smooth-scrolling
411 /// panel or an animated marker may legitimately want a fractional offset.
412 /// Typical usage:
413 /// ```ignore
414 /// ctx.save();
415 /// ctx.snap_to_pixel();
416 /// ctx.rect(0.0, 0.0, 10.0, 10.0);
417 /// ctx.fill();
418 /// ctx.restore();
419 /// ```
420 ///
421 /// Only the translation component is affected; rotations and non-uniform
422 /// scales pass through untouched (pixel alignment under those transforms
423 /// isn't well defined, and forcing a snap would visibly jitter rotated
424 /// content).
425 fn snap_to_pixel(&mut self) {
426 let t = self.transform();
427 let fx = t.tx - t.tx.floor();
428 let fy = t.ty - t.ty.floor();
429 if fx != 0.0 || fy != 0.0 {
430 self.translate(-fx, -fy);
431 }
432 }
433
434 // ── Compositing layers ────────────────────────────────────────────────────
435
436 /// Begin a new transparent compositing layer of the given pixel dimensions.
437 ///
438 /// All subsequent drawing (by this widget and its descendants) is redirected
439 /// into the new layer until [`pop_layer`] is called. Layers nest: each
440 /// `push_layer` must be matched by exactly one `pop_layer`.
441 ///
442 /// The current accumulated transform records the layer's screen-space origin;
443 /// drawing inside the layer uses a fresh local-space transform (origin 0,0).
444 ///
445 /// Implementations that do not support layers (e.g. the GL path) may leave
446 /// this as a no-op — the widget renders pass-through into the parent target.
447 fn push_layer(&mut self, _width: f64, _height: f64) {}
448
449 /// Whether this backend implements real offscreen compositing layers.
450 ///
451 /// The default is `false` so widgets can opt into layer-based rendering
452 /// without forcing every backend to pay for, or emulate, that feature.
453 fn supports_compositing_layers(&self) -> bool {
454 false
455 }
456
457 /// Whether this backend can retain named offscreen layers across frames.
458 ///
459 /// Generic compositing support is enough for isolated opacity groups, but
460 /// retained widget backbuffers need a backend-owned surface keyed by ID.
461 fn supports_retained_layers(&self) -> bool {
462 false
463 }
464
465 /// Begin a new transparent compositing layer that will be multiplied by
466 /// `alpha` when composited back into the parent target.
467 ///
468 /// Backends that do not support layer alpha can fall back to `push_layer`;
469 /// callers gate this through [`supports_compositing_layers`].
470 fn push_layer_with_alpha(&mut self, width: f64, height: f64, _alpha: f64) {
471 self.push_layer(width, height);
472 }
473
474 /// Constrain subsequent drawing in the current layer to a rounded-rect
475 /// mask. Used by window layers after shadows are drawn so chrome/content
476 /// cannot write into rounded transparent corners.
477 ///
478 /// This is a containment clip, not the visual antialiasing edge. Backends
479 /// should leave enough room for partially-transparent edge pixels so the
480 /// caller's normal alpha coverage can feather corners and edges.
481 fn set_layer_rounded_clip(&mut self, _x: f64, _y: f64, _w: f64, _h: f64, _r: f64) {}
482
483 /// Composite a previously retained backend layer. Returns `true` when
484 /// the backend had a retained surface for `key` and drew it.
485 fn composite_retained_layer(
486 &mut self,
487 _key: u64,
488 _width: f64,
489 _height: f64,
490 _alpha: f64,
491 ) -> bool {
492 false
493 }
494
495 /// Begin rendering into a retained backend layer identified by `key`.
496 /// Backends that do not retain layers may fall back to a transient layer.
497 fn push_retained_layer_with_alpha(&mut self, _key: u64, width: f64, height: f64, alpha: f64) {
498 self.push_layer_with_alpha(width, height, alpha);
499 }
500
501 /// Composite the current layer back into the previous render target using
502 /// SrcOver alpha blending, then discard the layer.
503 ///
504 /// Must be called after a matching `push_layer`. Unmatched calls are ignored.
505 fn pop_layer(&mut self) {}
506
507 // ── GL / GPU content ──────────────────────────────────────────────────────
508
509 /// Render GPU content (3-D scene, video frame, etc.) inline at the correct
510 /// painter-order position.
511 ///
512 /// `screen_rect` is the widget's screen-space rect in Y-up coordinates
513 /// (i.e. `ctx.transform()` origin + `widget.bounds().size`).
514 ///
515 /// The GL implementation executes `painter.gl_paint()` immediately so that
516 /// any 2-D widgets painted after this call naturally overdraw the GPU
517 /// content — correct back-to-front ordering with no post-frame fixup.
518 ///
519 /// The **software (`GfxCtx`) path is a no-op**: widgets should draw a 2-D
520 /// placeholder before calling this method so the software render has
521 /// something visible.
522 fn gl_paint(&mut self, _screen_rect: Rect, _painter: &mut dyn GlPaint) {}
523
524 // ── LCD mask compositing ──────────────────────────────────────────────────
525
526 /// Composite a pre-rasterized LCD subpixel mask onto the current
527 /// render target, mixing `src_color` into the destination through
528 /// per-channel coverage.
529 ///
530 /// `mask` is three bytes per pixel (`cov_r`, `cov_g`, `cov_b`) as
531 /// produced by [`crate::text_lcd::rasterize_lcd_mask`]. The caller
532 /// specifies `(dst_x, dst_y)` in local coordinates (Y-up in our
533 /// convention) and `mask_w × mask_h` to tell the backend the mask's
534 /// dimensions.
535 ///
536 /// Per-channel source-over blend:
537 /// ```text
538 /// dst.r = src.r * mask.r + dst.r * (1 - mask.r)
539 /// dst.g = src.g * mask.g + dst.g * (1 - mask.g)
540 /// dst.b = src.b * mask.b + dst.b * (1 - mask.b)
541 /// ```
542 ///
543 /// **This is the universal "composite LCD text onto arbitrary bg"
544 /// primitive** — it replaces the prior walk / sample / pre-fill
545 /// approach. Software ctx implements it as an inner-loop blend; the
546 /// GL ctx implements it via a dual-source-blend fragment shader.
547 /// Backends that haven't wired it yet use the default no-op, which
548 /// makes callers fall back to grayscale AA.
549 fn draw_lcd_mask(
550 &mut self,
551 _mask: &[u8],
552 _mask_w: u32,
553 _mask_h: u32,
554 _src_color: Color,
555 _dst_x: f64,
556 _dst_y: f64,
557 ) {
558 }
559
560 /// Arc-keyed variant so GL backends can cache the uploaded texture
561 /// on the `Arc`'s pointer identity — one `glTexImage2D` per unique
562 /// raster, lifetime tied to the mask's strong-ref count. Software
563 /// backends fall through to the slice path.
564 fn draw_lcd_mask_arc(
565 &mut self,
566 mask: &std::sync::Arc<Vec<u8>>,
567 mask_w: u32,
568 mask_h: u32,
569 src_color: Color,
570 dst_x: f64,
571 dst_y: f64,
572 ) {
573 self.draw_lcd_mask(mask.as_slice(), mask_w, mask_h, src_color, dst_x, dst_y);
574 }
575
576 /// Returns `true` if this backend supports [`draw_lcd_mask`] — i.e.
577 /// it can composite per-channel LCD coverage onto the active target.
578 /// Label queries this to decide between the LCD and grayscale AA
579 /// paths; a backend that returns `false` will never see LCD text.
580 fn has_lcd_mask_composite(&self) -> bool {
581 false
582 }
583
584 // ── Image blitting ────────────────────────────────────────────────────────
585
586 /// Returns `true` if this context implements `draw_image_rgba` with actual
587 /// pixel blitting. `Label` (and any other widget that uses a software
588 /// backbuffer) gates its cache path on this method so it can fall back to
589 /// direct `fill_text()` on render targets that don't support blitting
590 /// (e.g. the GL path).
591 ///
592 /// Default: `false`. Override to `true` in `GfxCtx`.
593 fn has_image_blit(&self) -> bool {
594 false
595 }
596
597 /// Draw raw RGBA pixel data into `dst_rect` (Y-up local coordinates).
598 ///
599 /// `data` must be `img_w * img_h * 4` bytes of tightly-packed RGBA8 data
600 /// in row-major order, **top-row first** (Y-down image storage convention).
601 /// The image is scaled to fit `(dst_x, dst_y, dst_w, dst_h)`.
602 ///
603 /// Default implementation: no-op (GL path or software paths that do not
604 /// implement blitting can leave this as a placeholder).
605 fn draw_image_rgba(
606 &mut self,
607 data: &[u8],
608 img_w: u32,
609 img_h: u32,
610 dst_x: f64,
611 dst_y: f64,
612 dst_w: f64,
613 dst_h: f64,
614 ) {
615 let _ = (data, img_w, img_h, dst_x, dst_y, dst_w, dst_h);
616 }
617
618 /// Same as [`draw_image_rgba`] but accepts an `Arc<Vec<u8>>` so the GL
619 /// backend can key its texture cache on the `Arc`'s pointer identity and
620 /// hold a `Weak` ref for automatic cleanup when the underlying buffer is
621 /// dropped — the pattern MatterCAD implements with C# `ConditionalWeakTable`.
622 ///
623 /// Used by `Label` (and future glyph-atlas consumers) in tandem with the
624 /// crate-level [`image_cache`](crate::image_cache) so that rebuilt widget
625 /// trees with unchanged content never re-rasterize OR re-upload.
626 ///
627 /// Default implementation: forward to [`draw_image_rgba`] via slice
628 /// borrow. Software backends don't benefit from GPU texture caching so
629 /// the default is usually fine; the GL backend overrides.
630 fn draw_image_rgba_arc(
631 &mut self,
632 data: &std::sync::Arc<Vec<u8>>,
633 img_w: u32,
634 img_h: u32,
635 dst_x: f64,
636 dst_y: f64,
637 dst_w: f64,
638 dst_h: f64,
639 ) {
640 self.draw_image_rgba(data.as_slice(), img_w, img_h, dst_x, dst_y, dst_w, dst_h);
641 }
642
643 // ── LCD backbuffer blit ───────────────────────────────────────────────────
644
645 /// Composite a two-plane `LcdCoverage`-mode backbuffer onto the active
646 /// render target at `(dst_x, dst_y)` with size `(dst_w, dst_h)` (in
647 /// local coords). Inputs are two `Arc<Vec<u8>>`, each 3 bytes per
648 /// pixel, **top-row-first**:
649 ///
650 /// - `color`: premultiplied per-channel RGB.
651 /// - `alpha`: per-channel alpha (coverage).
652 ///
653 /// The compositor applies per-channel premultiplied src-over:
654 ///
655 /// ```text
656 /// dst.ch := src.color_ch + dst.ch * (1 - src.alpha_ch)
657 /// ```
658 ///
659 /// which preserves LCD subpixel chroma through the cache round-trip.
660 /// Used by [`crate::widget::paint_subtree_backbuffered`] when a widget's
661 /// [`crate::widget::BackbufferMode::LcdCoverage`] cache is ready to
662 /// composite onto its parent.
663 ///
664 /// **Default:** collapses the two planes into a single straight-alpha
665 /// RGBA8 image (max of channel alphas, divided back to straight colour)
666 /// and forwards to [`draw_image_rgba`]. Correct for any content where
667 /// the three channel alphas agree; lossy of LCD chroma where they
668 /// diverge. Backends that want full subpixel quality through the
669 /// cache override this with a two-texture shader path.
670 fn draw_lcd_backbuffer_arc(
671 &mut self,
672 color: &std::sync::Arc<Vec<u8>>,
673 alpha: &std::sync::Arc<Vec<u8>>,
674 w: u32,
675 h: u32,
676 dst_x: f64,
677 dst_y: f64,
678 dst_w: f64,
679 dst_h: f64,
680 ) {
681 // Collapse to straight-alpha RGBA8 on the fly. Matches the same
682 // math `LcdBuffer::to_rgba8_top_down_collapsed` uses internally,
683 // except applied to a top-down pair rather than a Y-up pair.
684 let w_u = w as usize;
685 let h_u = h as usize;
686 if color.len() < w_u * h_u * 3 || alpha.len() < w_u * h_u * 3 {
687 return;
688 }
689 let mut rgba = vec![0u8; w_u * h_u * 4];
690 for i in 0..(w_u * h_u) {
691 let ci = i * 3;
692 let ra = alpha[ci];
693 let ga = alpha[ci + 1];
694 let ba = alpha[ci + 2];
695 let a = ra.max(ga).max(ba);
696 if a == 0 {
697 continue;
698 }
699 let af = a as f32 / 255.0;
700 let rc = color[ci] as f32 / 255.0;
701 let gc = color[ci + 1] as f32 / 255.0;
702 let bc = color[ci + 2] as f32 / 255.0;
703 let di = i * 4;
704 rgba[di] = ((rc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
705 rgba[di + 1] = ((gc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
706 rgba[di + 2] = ((bc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
707 rgba[di + 3] = a;
708 }
709 self.draw_image_rgba(&rgba, w, h, dst_x, dst_y, dst_w, dst_h);
710 }
711
712 // ── Theme / Visuals ───────────────────────────────────────────────────────
713
714 /// Return the currently-active [`Visuals`] palette.
715 ///
716 /// Delegates to [`crate::theme::current_visuals`], which reads the
717 /// thread-local set by [`crate::theme::set_visuals`]. Widget `paint()`
718 /// implementations call this to get colours instead of hardcoding them.
719 fn visuals(&self) -> Visuals {
720 crate::theme::current_visuals()
721 }
722}