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// ---------------------------------------------------------------------------
26// GL paint hook
27// ---------------------------------------------------------------------------
28
29/// Trait for widgets that want to render 3-D (or other GPU) content inline
30/// during the widget paint pass.
31///
32/// `DrawCtx::gl_paint` calls this with an opaque `gl` handle — implementations
33/// downcast it to `glow::Context` (or whatever GL type the platform provides).
34/// The software `GfxCtx` never calls `paint`; see [`DrawCtx::gl_paint`].
35pub trait GlPaint {
36    /// Execute GPU draw calls for the widget's 3-D content.
37    ///
38    /// `gl` — opaque platform GL context; downcast via `std::any::Any`.
39    /// `screen_rect` — Y-up screen-space rect for this widget (for viewport/scissor).
40    /// `full_w`, `full_h` — full viewport dimensions (for restoring after).
41    /// `parent_clip` — current framework scissor rect `[x, y, w, h]` in GL/Y-up
42    ///   pixels, or `None` if no clip is active.  Implementations **must intersect**
43    ///   any scissor they set with this rect so that parent widget clips (e.g. a
44    ///   collapsed window) correctly hide GPU-rendered content.
45    fn gl_paint(
46        &mut self,
47        gl:          &dyn std::any::Any,
48        screen_rect: Rect,
49        full_w:      i32,
50        full_h:      i32,
51        parent_clip: Option<[i32; 4]>,
52    );
53}
54
55/// Unified 2-D drawing context.
56///
57/// All coordinate parameters use the **Y-up, first-quadrant** convention:
58/// origin at the bottom-left, positive-Y upward.  This matches `GfxCtx` and
59/// the widget tree layout invariant.
60pub trait DrawCtx {
61    // ── State ─────────────────────────────────────────────────────────────────
62
63    fn set_fill_color(&mut self, color: Color);
64    fn set_stroke_color(&mut self, color: Color);
65    fn set_line_width(&mut self, w: f64);
66    fn set_line_join(&mut self, join: LineJoin);
67    fn set_line_cap(&mut self, cap: LineCap);
68    fn set_blend_mode(&mut self, mode: CompOp);
69    fn set_global_alpha(&mut self, alpha: f64);
70
71    // ── Font ──────────────────────────────────────────────────────────────────
72
73    fn set_font(&mut self, font: Arc<Font>);
74    fn set_font_size(&mut self, size: f64);
75
76    // ── Clipping ──────────────────────────────────────────────────────────────
77
78    fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64);
79    fn reset_clip(&mut self);
80
81    // ── Clear ─────────────────────────────────────────────────────────────────
82
83    /// Fill the entire render target with `color`, ignoring the current clip.
84    fn clear(&mut self, color: Color);
85
86    // ── Path building ─────────────────────────────────────────────────────────
87
88    fn begin_path(&mut self);
89    fn move_to(&mut self, x: f64, y: f64);
90    fn line_to(&mut self, x: f64, y: f64);
91    fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64);
92    fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64);
93    fn arc_to(&mut self, cx: f64, cy: f64, r: f64, start_angle: f64, end_angle: f64, ccw: bool);
94
95    /// Add a full circle contour to the current path.
96    fn circle(&mut self, cx: f64, cy: f64, r: f64);
97
98    /// Add an axis-aligned rectangle contour to the current path.
99    fn rect(&mut self, x: f64, y: f64, w: f64, h: f64);
100
101    /// Add a rounded-rectangle contour to the current path.
102    fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64);
103
104    fn close_path(&mut self);
105
106    // ── Path drawing ──────────────────────────────────────────────────────────
107
108    fn fill(&mut self);
109    fn stroke(&mut self);
110    fn fill_and_stroke(&mut self);
111
112    /// Submit **pre-tessellated** AA triangles with per-vertex coverage
113    /// (`x`, `y`, `alpha`) and triangle indices.
114    ///
115    /// This is the fast path for callers that tessellate their geometry
116    /// ONCE at load time (e.g. the Lion demo, SVG icons): they do the
117    /// `tessellate_path_aa` pass themselves, cache the vertex+index
118    /// buffers, then submit them every frame with only a cheap CPU
119    /// transform applied to the x/y components.  Compared to issuing
120    /// `move_to` / `line_to` / `fill` every frame, this keeps the polygon
121    /// set deterministic (no tess2 re-running on subtly-different
122    /// coordinates), avoids thousands of re-tessellations per frame, and
123    /// produces identical output regardless of the widget's transform.
124    ///
125    /// Vertices are `(x_logical_pixels, y_logical_pixels, alpha_0_to_1)`.
126    /// `alpha` is multiplied into the supplied `color.a` in the AA shader
127    /// so halo-strip edge AA survives this fast path.
128    ///
129    /// The software `GfxCtx` ignores the alpha attribute and rasterises
130    /// each triangle as a solid fill — correct but without edge AA, which
131    /// matches the software path's existing stroke/fill behaviour.
132    fn draw_triangles_aa(
133        &mut self,
134        vertices: &[[f32; 3]],
135        indices:  &[u32],
136        color:    crate::color::Color,
137    );
138
139    // ── Text ──────────────────────────────────────────────────────────────────
140
141    /// Draw `text` with the bottom of the baseline at `(x, y)`.
142    fn fill_text(&mut self, text: &str, x: f64, y: f64);
143
144    /// Draw `text` using the built-in AGG Glyph-Stroke-Vector font at `size`
145    /// pixels.  Useful before a proper font is loaded.
146    fn fill_text_gsv(&mut self, text: &str, x: f64, y: f64, size: f64);
147
148    /// Measure `text` with the current font and font-size settings.
149    fn measure_text(&self, text: &str) -> Option<TextMetrics>;
150
151    // ── Transform ─────────────────────────────────────────────────────────────
152
153    /// Current accumulated transform (CTM).
154    fn transform(&self) -> TransAffine;
155
156    fn save(&mut self);
157    fn restore(&mut self);
158    fn translate(&mut self, tx: f64, ty: f64);
159    fn rotate(&mut self, radians: f64);
160    fn scale(&mut self, sx: f64, sy: f64);
161    fn set_transform(&mut self, m: TransAffine);
162    fn reset_transform(&mut self);
163
164    /// **Opt-in** pixel snapping.  Strips the fractional part of the current
165    /// CTM translation so subsequent integer-coordinate `rect` / `fill` /
166    /// `stroke` / `draw_image_rgba*` calls land exactly on the physical pixel
167    /// grid — no AA fringe on edges, no LINEAR-filter blur on 1:1 texture
168    /// blits.
169    ///
170    /// Call this ONLY when the widget genuinely wants pixel-aligned drawing
171    /// (text backbuffers, pixel-alignment diagnostics, crisp UI strokes).
172    /// Sub-pixel positioning remains the default — e.g. a smooth-scrolling
173    /// panel or an animated marker may legitimately want a fractional offset.
174    /// Typical usage:
175    /// ```ignore
176    /// ctx.save();
177    /// ctx.snap_to_pixel();
178    /// ctx.rect(0.0, 0.0, 10.0, 10.0);
179    /// ctx.fill();
180    /// ctx.restore();
181    /// ```
182    ///
183    /// Only the translation component is affected; rotations and non-uniform
184    /// scales pass through untouched (pixel alignment under those transforms
185    /// isn't well defined, and forcing a snap would visibly jitter rotated
186    /// content).
187    fn snap_to_pixel(&mut self) {
188        let t = self.transform();
189        let fx = t.tx - t.tx.floor();
190        let fy = t.ty - t.ty.floor();
191        if fx != 0.0 || fy != 0.0 {
192            self.translate(-fx, -fy);
193        }
194    }
195
196    // ── Compositing layers ────────────────────────────────────────────────────
197
198    /// Begin a new transparent compositing layer of the given pixel dimensions.
199    ///
200    /// All subsequent drawing (by this widget and its descendants) is redirected
201    /// into the new layer until [`pop_layer`] is called.  Layers nest: each
202    /// `push_layer` must be matched by exactly one `pop_layer`.
203    ///
204    /// The current accumulated transform records the layer's screen-space origin;
205    /// drawing inside the layer uses a fresh local-space transform (origin 0,0).
206    ///
207    /// Implementations that do not support layers (e.g. the GL path) may leave
208    /// this as a no-op — the widget renders pass-through into the parent target.
209    fn push_layer(&mut self, _width: f64, _height: f64) {}
210
211    /// Composite the current layer back into the previous render target using
212    /// SrcOver alpha blending, then discard the layer.
213    ///
214    /// Must be called after a matching `push_layer`.  Unmatched calls are ignored.
215    fn pop_layer(&mut self) {}
216
217    // ── GL / GPU content ──────────────────────────────────────────────────────
218
219    /// Render GPU content (3-D scene, video frame, etc.) inline at the correct
220    /// painter-order position.
221    ///
222    /// `screen_rect` is the widget's screen-space rect in Y-up coordinates
223    /// (i.e. `ctx.transform()` origin + `widget.bounds().size`).
224    ///
225    /// The GL implementation executes `painter.gl_paint()` immediately so that
226    /// any 2-D widgets painted after this call naturally overdraw the GPU
227    /// content — correct back-to-front ordering with no post-frame fixup.
228    ///
229    /// The **software (`GfxCtx`) path is a no-op**: widgets should draw a 2-D
230    /// placeholder before calling this method so the software render has
231    /// something visible.
232    fn gl_paint(&mut self, _screen_rect: Rect, _painter: &mut dyn GlPaint) {}
233
234    // ── LCD mask compositing ──────────────────────────────────────────────────
235
236    /// Composite a pre-rasterized LCD subpixel mask onto the current
237    /// render target, mixing `src_color` into the destination through
238    /// per-channel coverage.
239    ///
240    /// `mask` is three bytes per pixel (`cov_r`, `cov_g`, `cov_b`) as
241    /// produced by [`crate::text_lcd::rasterize_lcd_mask`].  The caller
242    /// specifies `(dst_x, dst_y)` in local coordinates (Y-up in our
243    /// convention) and `mask_w × mask_h` to tell the backend the mask's
244    /// dimensions.
245    ///
246    /// Per-channel source-over blend:
247    /// ```text
248    /// dst.r = src.r * mask.r + dst.r * (1 - mask.r)
249    /// dst.g = src.g * mask.g + dst.g * (1 - mask.g)
250    /// dst.b = src.b * mask.b + dst.b * (1 - mask.b)
251    /// ```
252    ///
253    /// **This is the universal "composite LCD text onto arbitrary bg"
254    /// primitive** — it replaces the prior walk / sample / pre-fill
255    /// approach.  Software ctx implements it as an inner-loop blend; the
256    /// GL ctx implements it via a dual-source-blend fragment shader.
257    /// Backends that haven't wired it yet use the default no-op, which
258    /// makes callers fall back to grayscale AA.
259    fn draw_lcd_mask(
260        &mut self,
261        _mask:      &[u8],
262        _mask_w:    u32,
263        _mask_h:    u32,
264        _src_color: Color,
265        _dst_x:     f64,
266        _dst_y:     f64,
267    ) {}
268
269    /// Arc-keyed variant so GL backends can cache the uploaded texture
270    /// on the `Arc`'s pointer identity — one `glTexImage2D` per unique
271    /// raster, lifetime tied to the mask's strong-ref count.  Software
272    /// backends fall through to the slice path.
273    fn draw_lcd_mask_arc(
274        &mut self,
275        mask:      &std::sync::Arc<Vec<u8>>,
276        mask_w:    u32,
277        mask_h:    u32,
278        src_color: Color,
279        dst_x:     f64,
280        dst_y:     f64,
281    ) {
282        self.draw_lcd_mask(mask.as_slice(), mask_w, mask_h, src_color, dst_x, dst_y);
283    }
284
285    /// Returns `true` if this backend supports [`draw_lcd_mask`] — i.e.
286    /// it can composite per-channel LCD coverage onto the active target.
287    /// Label queries this to decide between the LCD and grayscale AA
288    /// paths; a backend that returns `false` will never see LCD text.
289    fn has_lcd_mask_composite(&self) -> bool { false }
290
291    // ── Image blitting ────────────────────────────────────────────────────────
292
293    /// Returns `true` if this context implements `draw_image_rgba` with actual
294    /// pixel blitting.  `Label` (and any other widget that uses a software
295    /// backbuffer) gates its cache path on this method so it can fall back to
296    /// direct `fill_text()` on render targets that don't support blitting
297    /// (e.g. the GL path).
298    ///
299    /// Default: `false`.  Override to `true` in `GfxCtx`.
300    fn has_image_blit(&self) -> bool { false }
301
302    /// Draw raw RGBA pixel data into `dst_rect` (Y-up local coordinates).
303    ///
304    /// `data` must be `img_w * img_h * 4` bytes of tightly-packed RGBA8 data
305    /// in row-major order, **top-row first** (Y-down image storage convention).
306    /// The image is scaled to fit `(dst_x, dst_y, dst_w, dst_h)`.
307    ///
308    /// Default implementation: no-op (GL path or software paths that do not
309    /// implement blitting can leave this as a placeholder).
310    fn draw_image_rgba(
311        &mut self,
312        data:  &[u8],
313        img_w: u32,
314        img_h: u32,
315        dst_x: f64,
316        dst_y: f64,
317        dst_w: f64,
318        dst_h: f64,
319    ) {
320        let _ = (data, img_w, img_h, dst_x, dst_y, dst_w, dst_h);
321    }
322
323    /// Same as [`draw_image_rgba`] but accepts an `Arc<Vec<u8>>` so the GL
324    /// backend can key its texture cache on the `Arc`'s pointer identity and
325    /// hold a `Weak` ref for automatic cleanup when the underlying buffer is
326    /// dropped — the pattern MatterCAD implements with C# `ConditionalWeakTable`.
327    ///
328    /// Used by `Label` (and future glyph-atlas consumers) in tandem with the
329    /// crate-level [`image_cache`](crate::image_cache) so that rebuilt widget
330    /// trees with unchanged content never re-rasterize OR re-upload.
331    ///
332    /// Default implementation: forward to [`draw_image_rgba`] via slice
333    /// borrow.  Software backends don't benefit from GPU texture caching so
334    /// the default is usually fine; the GL backend overrides.
335    fn draw_image_rgba_arc(
336        &mut self,
337        data:  &std::sync::Arc<Vec<u8>>,
338        img_w: u32,
339        img_h: u32,
340        dst_x: f64,
341        dst_y: f64,
342        dst_w: f64,
343        dst_h: f64,
344    ) {
345        self.draw_image_rgba(data.as_slice(), img_w, img_h, dst_x, dst_y, dst_w, dst_h);
346    }
347
348    // ── LCD backbuffer blit ───────────────────────────────────────────────────
349
350    /// Composite a two-plane `LcdCoverage`-mode backbuffer onto the active
351    /// render target at `(dst_x, dst_y)` with size `(dst_w, dst_h)` (in
352    /// local coords).  Inputs are two `Arc<Vec<u8>>`, each 3 bytes per
353    /// pixel, **top-row-first**:
354    ///
355    /// - `color`: premultiplied per-channel RGB.
356    /// - `alpha`: per-channel alpha (coverage).
357    ///
358    /// The compositor applies per-channel premultiplied src-over:
359    ///
360    /// ```text
361    /// dst.ch := src.color_ch + dst.ch * (1 - src.alpha_ch)
362    /// ```
363    ///
364    /// which preserves LCD subpixel chroma through the cache round-trip.
365    /// Used by [`crate::widget::paint_subtree_backbuffered`] when a widget's
366    /// [`crate::widget::BackbufferMode::LcdCoverage`] cache is ready to
367    /// composite onto its parent.
368    ///
369    /// **Default:** collapses the two planes into a single straight-alpha
370    /// RGBA8 image (max of channel alphas, divided back to straight colour)
371    /// and forwards to [`draw_image_rgba`].  Correct for any content where
372    /// the three channel alphas agree; lossy of LCD chroma where they
373    /// diverge.  Backends that want full subpixel quality through the
374    /// cache override this with a two-texture shader path.
375    fn draw_lcd_backbuffer_arc(
376        &mut self,
377        color: &std::sync::Arc<Vec<u8>>,
378        alpha: &std::sync::Arc<Vec<u8>>,
379        w: u32,
380        h: u32,
381        dst_x: f64,
382        dst_y: f64,
383        dst_w: f64,
384        dst_h: f64,
385    ) {
386        // Collapse to straight-alpha RGBA8 on the fly.  Matches the same
387        // math `LcdBuffer::to_rgba8_top_down_collapsed` uses internally,
388        // except applied to a top-down pair rather than a Y-up pair.
389        let w_u = w as usize;
390        let h_u = h as usize;
391        if color.len() < w_u * h_u * 3 || alpha.len() < w_u * h_u * 3 { return; }
392        let mut rgba = vec![0u8; w_u * h_u * 4];
393        for i in 0..(w_u * h_u) {
394            let ci = i * 3;
395            let ra = alpha[ci];
396            let ga = alpha[ci + 1];
397            let ba = alpha[ci + 2];
398            let a  = ra.max(ga).max(ba);
399            if a == 0 { continue; }
400            let af = a as f32 / 255.0;
401            let rc = color[ci]     as f32 / 255.0;
402            let gc = color[ci + 1] as f32 / 255.0;
403            let bc = color[ci + 2] as f32 / 255.0;
404            let di = i * 4;
405            rgba[di]     = ((rc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
406            rgba[di + 1] = ((gc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
407            rgba[di + 2] = ((bc / af) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
408            rgba[di + 3] = a;
409        }
410        self.draw_image_rgba(&rgba, w, h, dst_x, dst_y, dst_w, dst_h);
411    }
412
413    // ── Theme / Visuals ───────────────────────────────────────────────────────
414
415    /// Return the currently-active [`Visuals`] palette.
416    ///
417    /// Delegates to [`crate::theme::current_visuals`], which reads the
418    /// thread-local set by [`crate::theme::set_visuals`].  Widget `paint()`
419    /// implementations call this to get colours instead of hardcoding them.
420    fn visuals(&self) -> Visuals {
421        crate::theme::current_visuals()
422    }
423}