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}