Skip to main content

agg_gui/
gfx_ctx.rs

1//! Graphics context — the primary drawing API for widget painting.
2//!
3//! `GfxCtx` is modeled after Cairo's `cairo_t`. All drawing goes through this
4//! type. It owns a stateful transform + style stack and writes pixels into a
5//! [`Framebuffer`] via AGG.
6//!
7//! # Coordinate system
8//!
9//! All coordinates are **first-quadrant (Y-up)**. Origin is the bottom-left
10//! corner of the framebuffer. Positive X goes right, positive Y goes up.
11//! Positive angles rotate counter-clockwise (mathematically standard).
12
13use std::f64::consts::PI;
14use std::sync::Arc;
15
16use agg_rust::arc::Arc as AggArc;
17use agg_rust::basics::PATH_FLAGS_NONE;
18use agg_rust::comp_op::{CompOp, PixfmtRgba32CompOp};
19use agg_rust::conv_curve::ConvCurve;
20use agg_rust::conv_stroke::ConvStroke;
21use agg_rust::conv_transform::ConvTransform;
22use agg_rust::gsv_text::GsvText;
23use agg_rust::math_stroke::{LineCap, LineJoin};
24use agg_rust::path_storage::PathStorage;
25use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
26use agg_rust::renderer_base::RendererBase;
27use agg_rust::renderer_scanline::render_scanlines_aa_solid;
28use agg_rust::rendering_buffer::RowAccessor;
29use agg_rust::rounded_rect::RoundedRect;
30use agg_rust::scanline_u::ScanlineU8;
31use agg_rust::trans_affine::TransAffine;
32
33use crate::color::Color;
34use crate::framebuffer::Framebuffer;
35use crate::text::{shape_text, measure_advance, Font, TextMetrics};
36
37// ---------------------------------------------------------------------------
38// Layer stack entry
39// ---------------------------------------------------------------------------
40
41/// One entry on the `GfxCtx` layer stack, created by `push_layer`.
42struct LayerEntry {
43    /// The offscreen framebuffer for this layer.
44    fb:             Framebuffer,
45    /// GfxState snapshot at the moment `push_layer` was called.
46    /// Restored verbatim on `pop_layer`.
47    saved_state:    GfxState,
48    /// State-stack snapshot at the moment `push_layer` was called.
49    saved_stack:    Vec<GfxState>,
50    /// Screen-space X origin of this layer (= CTM tx at push time, Y-up).
51    origin_x:       f64,
52    /// Screen-space Y origin of this layer (= CTM ty at push time, Y-up).
53    origin_y:       f64,
54}
55
56// Re-export so callers don't need to import agg_rust directly.
57pub use agg_rust::comp_op::CompOp as BlendMode;
58
59/// Snapshot of drawing state, pushed/popped by `save()`/`restore()`.
60#[derive(Clone)]
61struct GfxState {
62    transform: TransAffine,
63    fill_color: Color,
64    stroke_color: Color,
65    line_width: f64,
66    line_join: LineJoin,
67    line_cap: LineCap,
68    blend_mode: CompOp,
69    /// Scissor clip in Y-up screen space: `(x, y, width, height)`.
70    clip: Option<(f64, f64, f64, f64)>,
71    /// Global alpha multiplier applied to fill and stroke at draw time.
72    global_alpha: f64,
73    /// Current font (shared).
74    font: Option<Arc<Font>>,
75    /// Font size in pixels (height from baseline to top of cap height).
76    font_size: f64,
77}
78
79impl Default for GfxState {
80    fn default() -> Self {
81        Self {
82            transform: TransAffine::new(),
83            fill_color: Color::black(),
84            stroke_color: Color::black(),
85            line_width: 1.0,
86            line_join: LineJoin::Round,
87            line_cap: LineCap::Round,
88            blend_mode: CompOp::SrcOver,
89            clip: None,
90            global_alpha: 1.0,
91            font: None,
92            font_size: 16.0,
93        }
94    }
95}
96
97/// Cairo-style stateful 2D graphics context.
98///
99/// All widget painting goes through `GfxCtx`. Create one per frame from a
100/// [`Framebuffer`], draw into it, then let it drop — the framebuffer retains
101/// the rendered pixels.
102///
103/// # Layer compositing
104///
105/// Call `push_layer(w, h)` to redirect all subsequent drawing into an offscreen
106/// framebuffer.  Call `pop_layer()` to SrcOver-composite that buffer back into
107/// the previous target (which may itself be a layer or the base framebuffer).
108/// Layers nest; each `push` must be matched by exactly one `pop`.
109pub struct GfxCtx<'a> {
110    base_fb:     &'a mut Framebuffer,
111    /// Offscreen layer stack.  Empty when rendering directly to `base_fb`.
112    layer_stack: Vec<LayerEntry>,
113    state:       GfxState,
114    state_stack: Vec<GfxState>,
115    /// Accumulated path, reset by `begin_path()`.
116    path:        PathStorage,
117    /// When true, `fill_text` routes through the 3× horizontal LCD
118    /// subpixel pipeline (see `lcd_coverage.rs`) and composites per-channel
119    /// onto the active framebuffer.  Controlled by the backbuffer mode —
120    /// set to true when this ctx is writing into an `LcdCoverage` widget
121    /// backbuffer, false for `Rgba`.  Main render loops set it at frame
122    /// start from `font_settings::lcd_enabled()`.
123    lcd_mode: bool,
124}
125
126impl<'a> GfxCtx<'a> {
127    /// Create a new graphics context for the given framebuffer.
128    pub fn new(fb: &'a mut Framebuffer) -> Self {
129        Self {
130            base_fb: fb,
131            layer_stack: Vec::new(),
132            state: GfxState::default(),
133            state_stack: Vec::new(),
134            path: PathStorage::new(),
135            lcd_mode: false,
136        }
137    }
138
139    // -------------------------------------------------------------------------
140    // Layer compositing
141    // -------------------------------------------------------------------------
142
143    /// Begin an offscreen compositing layer of `width × height` pixels.
144    ///
145    /// All draw calls until the matching `pop_layer` are redirected into a fresh
146    /// transparent `Framebuffer`.  The current CTM's translation records the
147    /// layer's screen-space origin; drawing inside uses a reset local transform.
148    pub fn push_layer(&mut self, width: f64, height: f64) {
149        let origin_x = self.state.transform.tx;
150        let origin_y = self.state.transform.ty;
151        let saved_state = self.state.clone();
152        let saved_stack = std::mem::take(&mut self.state_stack);
153        let layer_fb = Framebuffer::new(width.ceil() as u32, height.ceil() as u32);
154        self.layer_stack.push(LayerEntry {
155            fb: layer_fb,
156            saved_state,
157            saved_stack,
158            origin_x,
159            origin_y,
160        });
161        // Reset to local-space origin for the new layer.
162        self.state.transform = TransAffine::new();
163        self.state.clip = None;
164    }
165
166    /// SrcOver-composite the current layer into the previous render target, then
167    /// restore the graphics state that was active at the matching `push_layer`.
168    pub fn pop_layer(&mut self) {
169        let Some(layer) = self.layer_stack.pop() else { return; };
170        let ox = layer.origin_x as i32;
171        let oy = layer.origin_y as i32;
172        self.state       = layer.saved_state;
173        self.state_stack = layer.saved_stack;
174        // Composite: src = layer.fb, dst = now-active framebuffer.
175        if let Some(top) = self.layer_stack.last_mut() {
176            composite_framebuffers(&mut top.fb, &layer.fb, ox, oy);
177        } else {
178            composite_framebuffers(self.base_fb, &layer.fb, ox, oy);
179        }
180    }
181
182    // -------------------------------------------------------------------------
183    // State stack
184    // -------------------------------------------------------------------------
185
186    pub fn save(&mut self) {
187        self.state_stack.push(self.state.clone());
188    }
189
190    pub fn restore(&mut self) {
191        if let Some(state) = self.state_stack.pop() {
192            self.state = state;
193        }
194    }
195
196    // -------------------------------------------------------------------------
197    // Transform (Y-up, CCW-positive rotations)
198    // -------------------------------------------------------------------------
199
200    /// Append a translation. Uses pre-multiply (Cairo semantics).
201    pub fn translate(&mut self, tx: f64, ty: f64) {
202        self.state.transform.premultiply(&TransAffine::new_translation(tx, ty));
203    }
204
205    /// Append a CCW rotation in radians. Uses pre-multiply semantics.
206    pub fn rotate(&mut self, radians: f64) {
207        self.state.transform.premultiply(&TransAffine::new_rotation(radians));
208    }
209
210    /// Append a scale. Uses pre-multiply semantics.
211    pub fn scale(&mut self, sx: f64, sy: f64) {
212        self.state.transform.premultiply(&TransAffine::new_scaling(sx, sy));
213    }
214
215    pub fn set_transform(&mut self, m: TransAffine) { self.state.transform = m; }
216    pub fn reset_transform(&mut self) { self.state.transform = TransAffine::new(); }
217    /// Return the current accumulated transform (cumulative translation + scale
218    /// from all parent `save/translate/restore` calls). The `tx`/`ty` fields
219    /// give the widget's bottom-left corner in framebuffer (Y-up) coordinates.
220    pub fn transform(&self) -> TransAffine { self.state.transform }
221
222    // -------------------------------------------------------------------------
223    // Style
224    // -------------------------------------------------------------------------
225
226    pub fn set_fill_color(&mut self, color: Color) { self.state.fill_color = color; }
227    pub fn set_stroke_color(&mut self, color: Color) { self.state.stroke_color = color; }
228    pub fn set_line_width(&mut self, w: f64) { self.state.line_width = w; }
229    pub fn set_line_join(&mut self, join: LineJoin) { self.state.line_join = join; }
230    pub fn set_line_cap(&mut self, cap: LineCap) { self.state.line_cap = cap; }
231
232    /// Set the Porter-Duff compositing mode. Default: `SrcOver`.
233    pub fn set_blend_mode(&mut self, mode: CompOp) { self.state.blend_mode = mode; }
234
235    /// Global alpha multiplier (0.0–1.0) applied on top of each color's alpha.
236    pub fn set_global_alpha(&mut self, alpha: f64) {
237        self.state.global_alpha = alpha.clamp(0.0, 1.0);
238    }
239
240    // -------------------------------------------------------------------------
241    // Font
242    // -------------------------------------------------------------------------
243
244    /// Set the current font. Shared via `Arc` — cheap to clone across widgets.
245    pub fn set_font(&mut self, font: Arc<Font>) {
246        self.state.font = Some(font);
247    }
248
249    /// Set the font size in pixels (distance from baseline to cap height).
250    pub fn set_font_size(&mut self, size: f64) {
251        self.state.font_size = size.max(1.0);
252    }
253
254    /// Enable/disable LCD subpixel rendering on this ctx.  When true,
255    /// `fill_text` uses the per-channel coverage pipeline; when false
256    /// grayscale AA.  Set by `paint_subtree_backbuffered` for
257    /// `LcdCoverage` widget buffers, and by the main render loop for
258    /// direct-to-screen text.
259    pub fn set_lcd_mode(&mut self, on: bool) { self.lcd_mode = on; }
260
261    /// Read the ctx's current LCD mode.
262    pub fn lcd_mode(&self) -> bool { self.lcd_mode }
263
264    // -------------------------------------------------------------------------
265    // Clipping
266    // -------------------------------------------------------------------------
267
268    /// Intersect the current clip with a rectangle in the **current local
269    /// coordinate space** (i.e. after all accumulated `translate` / `scale`
270    /// calls).  The four corners are mapped through the current transform to
271    /// produce an axis-aligned screen-space bounding box, which is then
272    /// intersected with any existing clip.
273    ///
274    /// For the common case of pure translations this is equivalent to the old
275    /// "screen-space rectangle" API, but it now works correctly when called
276    /// from inside a `paint()` method that runs after the framework has already
277    /// translated the context to the widget's origin.
278    pub fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
279        // Map all four corners through the CTM and take the AABB.
280        let t = &self.state.transform;
281        let corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)];
282        let mut sx_min = f64::INFINITY;
283        let mut sy_min = f64::INFINITY;
284        let mut sx_max = f64::NEG_INFINITY;
285        let mut sy_max = f64::NEG_INFINITY;
286        for (lx, ly) in corners {
287            let mut sx = lx;
288            let mut sy = ly;
289            t.transform(&mut sx, &mut sy);
290            if sx < sx_min { sx_min = sx; }
291            if sx > sx_max { sx_max = sx; }
292            if sy < sy_min { sy_min = sy; }
293            if sy > sy_max { sy_max = sy; }
294        }
295        let sw = (sx_max - sx_min).max(0.0);
296        let sh = (sy_max - sy_min).max(0.0);
297        if let Some((cx, cy, cw, ch)) = self.state.clip {
298            let x1 = sx_min.max(cx);
299            let y1 = sy_min.max(cy);
300            let x2 = sx_max.min(cx + cw);
301            let y2 = sy_max.min(cy + ch);
302            self.state.clip = Some((x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0)));
303        } else {
304            self.state.clip = Some((sx_min, sy_min, sw, sh));
305        }
306    }
307
308    pub fn reset_clip(&mut self) { self.state.clip = None; }
309
310    // -------------------------------------------------------------------------
311    // Clear
312    // -------------------------------------------------------------------------
313
314    /// Fill the entire active framebuffer with `color`, ignoring transform and clip.
315    pub fn clear(&mut self, color: Color) {
316        let rgba = color.to_rgba8();
317        for chunk in active_fb(&mut self.base_fb, &mut self.layer_stack).pixels_mut().chunks_exact_mut(4) {
318            chunk[0] = rgba.r as u8;
319            chunk[1] = rgba.g as u8;
320            chunk[2] = rgba.b as u8;
321            chunk[3] = rgba.a as u8;
322        }
323    }
324
325    // -------------------------------------------------------------------------
326    // Path construction
327    // -------------------------------------------------------------------------
328
329    pub fn begin_path(&mut self) { self.path = PathStorage::new(); }
330
331    pub fn move_to(&mut self, x: f64, y: f64) { self.path.move_to(x, y); }
332    pub fn line_to(&mut self, x: f64, y: f64) { self.path.line_to(x, y); }
333
334    pub fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64) {
335        self.path.curve4(cx1, cy1, cx2, cy2, x, y);
336    }
337
338    pub fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64) {
339        self.path.curve3(cx, cy, x, y);
340    }
341
342    pub fn arc_to(&mut self, cx: f64, cy: f64, r: f64, start_angle: f64, end_angle: f64, ccw: bool) {
343        let mut arc = AggArc::new(cx, cy, r, r, start_angle, end_angle, ccw);
344        self.path.concat_path(&mut arc, 0);
345    }
346
347    /// Full circle at `(cx, cy)` with radius `r`.
348    pub fn circle(&mut self, cx: f64, cy: f64, r: f64) {
349        self.arc_to(cx, cy, r, 0.0, 2.0 * PI, true);
350        self.path.close_polygon(PATH_FLAGS_NONE);
351    }
352
353    /// Axis-aligned rectangle — bottom-left `(x, y)`, size `w × h`.
354    pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
355        self.path.move_to(x, y);
356        self.path.line_to(x + w, y);
357        self.path.line_to(x + w, y + h);
358        self.path.line_to(x, y + h);
359        self.path.close_polygon(PATH_FLAGS_NONE);
360    }
361
362    /// Rounded rectangle — bottom-left `(x, y)`, size `w × h`, corner radius `r`.
363    pub fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
364        let r = r.min(w * 0.5).min(h * 0.5).max(0.0);
365        let mut rr = RoundedRect::new(x, y, x + w, y + h, r);
366        rr.normalize_radius();
367        self.path.concat_path(&mut rr, 0);
368    }
369
370    pub fn close_path(&mut self) { self.path.close_polygon(PATH_FLAGS_NONE); }
371
372    // -------------------------------------------------------------------------
373    // Drawing
374    // -------------------------------------------------------------------------
375
376    /// Fill the accumulated path.
377    pub fn fill(&mut self) {
378        let mut color = self.state.fill_color;
379        color.a *= self.state.global_alpha as f32;
380        let rgba = color.to_rgba8();
381        let mode = self.state.blend_mode;
382        let clip = self.state.clip;
383        let transform = self.state.transform.clone();
384        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
385        rasterize_fill(fb, &mut self.path, &rgba, mode, clip, &transform);
386    }
387
388    /// Stroke the accumulated path.
389    pub fn stroke(&mut self) {
390        let mut color = self.state.stroke_color;
391        color.a *= self.state.global_alpha as f32;
392        let rgba = color.to_rgba8();
393        let width = self.state.line_width;
394        let join = self.state.line_join;
395        let cap = self.state.line_cap;
396        let mode = self.state.blend_mode;
397        let clip = self.state.clip;
398        let transform = self.state.transform.clone();
399        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
400        rasterize_stroke(fb, &mut self.path, &rgba, width, join, cap, mode, clip, &transform);
401    }
402
403    /// Fill then stroke the accumulated path in one call.
404    pub fn fill_and_stroke(&mut self) {
405        self.fill();
406        self.stroke();
407    }
408
409    // -------------------------------------------------------------------------
410    // Text
411    // -------------------------------------------------------------------------
412
413    /// Draw `text` at position `(x, y)` using the current font and fill color.
414    ///
415    /// `(x, y)` is the **baseline-left** position in Y-up screen coordinates.
416    /// Glyphs extend upward (higher Y) for ascenders and downward (lower Y)
417    /// for descenders — correct for Y-up rendering with no Y-flip.
418    ///
419    /// Requires a font to be set via [`set_font`](Self::set_font). Does nothing
420    /// if no font has been set.
421    pub fn fill_text(&mut self, text: &str, x: f64, y: f64) {
422        let font = match self.state.font.clone() {
423            Some(f) => f,
424            None => return,
425        };
426        let font_size = self.state.font_size;
427
428        let mut color = self.state.fill_color;
429        color.a *= self.state.global_alpha as f32;
430
431        // LCD subpixel path — gated on this ctx's `lcd_mode` flag,
432        // which is set by `paint_subtree_backbuffered` when the widget
433        // chose `BackbufferMode::LcdCoverage` and by the main render
434        // loop for direct-to-screen text when the global font setting
435        // says so.  Mask raster is cached keyed on `(text, font, size)`
436        // and colour is applied at composite time.
437        //
438        // HiDPI: rasterise the mask at the **physical** font size (logical
439        // × CTM scale) so the 1:1 texel-to-pixel composite fills the
440        // expected number of physical pixels.  Without this the mask
441        // renders at logical size and ends up half-size (or stretched by a
442        // separate scale call) on 2×/3× displays.
443        //
444        // **Y-axis baseline alignment**: when the global hinting toggle
445        // is ON, both renderers place the baseline on the same integer
446        // physical pixel row — see the in-mask `by` snap inside
447        // `rasterize_text_lcd_cached` paired with `shape_text`'s own
448        // hint-driven `gy` snap.  When hinting is OFF, the RGBA path
449        // produces baseline at the exact fractional `y`, while the LCD
450        // path's intrinsic composite-rounding (`sy.round()` in
451        // `draw_lcd_mask`, required for X-subpixel coherence) lands the
452        // baseline at the nearest integer plus the fractional descender
453        // — a subpixel residual that's impossible to remove without
454        // breaking LCD chroma.  This is a deliberate trade-off matching
455        // the user's "snap should be a checkbox, not always on".
456        if self.lcd_mode {
457            let t = &self.state.transform;
458            let ctm_scale = (t.sx * t.sx + t.shy * t.shy).sqrt().max(1e-6);
459            let phys_size = font_size * ctm_scale;
460            let cached = crate::lcd_coverage::rasterize_text_lcd_cached(
461                &font, text, phys_size,
462            );
463            // `baseline_*_in_mask` is in physical mask pixels; divide by
464            // `ctm_scale` so the offset stays in logical units that the
465            // CTM then multiplies back to physical at blit time.
466            let dst_x = x - cached.baseline_x_in_mask / ctm_scale;
467            let dst_y = y - cached.baseline_y_in_mask / ctm_scale;
468            <Self as crate::DrawCtx>::draw_lcd_mask_arc(
469                self,
470                &cached.pixels, cached.width, cached.height,
471                color, dst_x, dst_y,
472            );
473            return;
474        }
475
476        let rgba = color.to_rgba8();
477        let mode = self.state.blend_mode;
478        let clip = self.state.clip;
479        let transform = self.state.transform.clone();
480
481        // Shape text and collect per-glyph outline paths.
482        let (glyph_paths, _) = shape_text(&font, text, font_size, x, y);
483        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
484        for mut path in glyph_paths {
485            rasterize_fill(fb, &mut path, &rgba, mode, clip, &transform);
486        }
487    }
488
489    /// Measure the advance width and metrics of `text` in the current font.
490    ///
491    /// Returns `None` if no font has been set.
492    pub fn measure_text(&self, text: &str) -> Option<TextMetrics> {
493        let font = self.state.font.as_ref()?;
494        let size = self.state.font_size;
495        Some(TextMetrics {
496            width: measure_advance(font, text, size),
497            ascent: font.ascender_px(size),
498            descent: font.descender_px(size),
499            line_height: font.line_height_px(size),
500        })
501    }
502
503    // -------------------------------------------------------------------------
504    // Convenience: built-in stroked vector font (no font file required)
505    // -------------------------------------------------------------------------
506
507    /// Draw text using AGG's built-in vector font (no external font needed).
508    ///
509    /// Useful for labels before a full font is loaded.
510    pub fn fill_text_gsv(&mut self, text: &str, x: f64, y: f64, size: f64) {
511        let mut color = self.state.fill_color;
512        color.a *= self.state.global_alpha as f32;
513        let rgba = color.to_rgba8();
514        let mode = self.state.blend_mode;
515        let clip = self.state.clip;
516        let transform = self.state.transform.clone();
517
518        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
519        let w = fb.width();
520        let h = fb.height();
521        let stride = (w * 4) as i32;
522        let mut ra = RowAccessor::new();
523        unsafe { ra.attach(fb.pixels_mut().as_mut_ptr(), w, h, stride) };
524        let pf = PixfmtRgba32CompOp::new_with_op(&mut ra, mode);
525        let mut rb = RendererBase::new(pf);
526        apply_clip(&mut rb, clip);
527
528        let mut ras = RasterizerScanlineAa::new();
529        let mut sl = ScanlineU8::new();
530
531        let mut gsv = GsvText::new();
532        gsv.size(size, 0.0);
533        gsv.start_point(x, y);
534        gsv.text(text);
535
536        let mut stroke = ConvStroke::new(&mut gsv);
537        stroke.set_width(size * 0.1);
538        let mut transformed = ConvTransform::new(&mut stroke, transform);
539        ras.add_path(&mut transformed, 0);
540        render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &rgba);
541    }
542}
543
544// ---------------------------------------------------------------------------
545// Active-framebuffer helper
546// ---------------------------------------------------------------------------
547
548/// Return a `&mut Framebuffer` for the currently active render target.
549///
550/// If any layers are on the stack, returns the top layer's framebuffer.
551/// Otherwise returns the base framebuffer.  Accepts the two fields as
552/// separate `&mut` references so callers can simultaneously borrow other
553/// `GfxCtx` fields (e.g. `state`, `path`) without triggering borrow
554/// conflicts on `self`.
555#[inline]
556fn active_fb<'a>(
557    base_fb:     &'a mut Framebuffer,
558    layer_stack: &'a mut Vec<LayerEntry>,
559) -> &'a mut Framebuffer {
560    if let Some(top) = layer_stack.last_mut() {
561        &mut top.fb
562    } else {
563        base_fb
564    }
565}
566
567
568// ---------------------------------------------------------------------------
569// SrcOver layer compositing
570// ---------------------------------------------------------------------------
571
572/// Composite `src` onto `dst` using SrcOver alpha blending.
573///
574/// AGG writes **premultiplied** RGBA into framebuffers.  The premultiplied
575/// SrcOver formula is:
576///
577/// ```text
578/// out_channel = src_premul + dst_premul × (1 − src_alpha_norm)
579/// ```
580///
581/// This applies identically to all four channels (R, G, B, A), which makes
582/// the implementation straightforward and avoids the division step needed for
583/// straight-alpha compositing.
584///
585/// `dest_x` / `dest_y` are the Y-up pixel coordinates in `dst` where the
586/// bottom-left corner of `src` lands.  Out-of-bounds pixels are silently clipped.
587fn composite_framebuffers(dst: &mut Framebuffer, src: &Framebuffer, dest_x: i32, dest_y: i32) {
588    let src_w = src.width() as i32;
589    let src_h = src.height() as i32;
590    let dst_w = dst.width() as i32;
591    let dst_h = dst.height() as i32;
592
593    let src_px = src.pixels();
594    let dst_px = dst.pixels_mut();
595
596    for sy in 0..src_h {
597        let dy = dest_y + sy;
598        if dy < 0 || dy >= dst_h { continue; }
599        for sx in 0..src_w {
600            let dx = dest_x + sx;
601            if dx < 0 || dx >= dst_w { continue; }
602            let si = ((sy * src_w + sx) * 4) as usize;
603            let di = ((dy * dst_w + dx) * 4) as usize;
604            let sa = src_px[si + 3] as f32 / 255.0;
605            if sa < 1e-4 { continue; } // fully transparent source — skip
606            let inv_sa = 1.0 - sa;
607            // Premultiplied SrcOver — same formula for all four channels.
608            for k in 0..4 {
609                let s = src_px[si + k] as f32;
610                let d = dst_px[di + k] as f32;
611                dst_px[di + k] = (s + d * inv_sa).round().clamp(0.0, 255.0) as u8;
612            }
613        }
614    }
615}
616
617// ---------------------------------------------------------------------------
618// Free rasterization helpers — take explicit path and fb references so they
619// can be called for both self.path draws and per-glyph text draws without
620// borrow-checker conflicts.
621// ---------------------------------------------------------------------------
622
623pub(crate) fn rasterize_fill(
624    fb: &mut Framebuffer,
625    path: &mut PathStorage,
626    color: &agg_rust::color::Rgba8,
627    mode: CompOp,
628    clip: Option<(f64, f64, f64, f64)>,
629    transform: &TransAffine,
630) {
631    let w = fb.width();
632    let h = fb.height();
633    let stride = (w * 4) as i32;
634    let mut ra = RowAccessor::new();
635    unsafe { ra.attach(fb.pixels_mut().as_mut_ptr(), w, h, stride) };
636    let pf = PixfmtRgba32CompOp::new_with_op(&mut ra, mode);
637    let mut rb = RendererBase::new(pf);
638    apply_clip(&mut rb, clip);
639
640    let mut ras = RasterizerScanlineAa::new();
641    let mut sl = ScanlineU8::new();
642    let mut curves = ConvCurve::new(path);
643    let mut transformed = ConvTransform::new(&mut curves, transform.clone());
644    ras.add_path(&mut transformed, 0);
645    render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
646}
647
648pub(crate) fn rasterize_stroke(
649    fb: &mut Framebuffer,
650    path: &mut PathStorage,
651    color: &agg_rust::color::Rgba8,
652    width: f64,
653    join: LineJoin,
654    cap: LineCap,
655    mode: CompOp,
656    clip: Option<(f64, f64, f64, f64)>,
657    transform: &TransAffine,
658) {
659    let w = fb.width();
660    let h = fb.height();
661    let stride = (w * 4) as i32;
662    let mut ra = RowAccessor::new();
663    unsafe { ra.attach(fb.pixels_mut().as_mut_ptr(), w, h, stride) };
664    let pf = PixfmtRgba32CompOp::new_with_op(&mut ra, mode);
665    let mut rb = RendererBase::new(pf);
666    apply_clip(&mut rb, clip);
667
668    let mut ras = RasterizerScanlineAa::new();
669    let mut sl = ScanlineU8::new();
670    let mut curves = ConvCurve::new(path);
671    let mut stroke = ConvStroke::new(&mut curves);
672    stroke.set_width(width);
673    stroke.set_line_join(join);
674    stroke.set_line_cap(cap);
675    let mut transformed = ConvTransform::new(&mut stroke, transform.clone());
676    ras.add_path(&mut transformed, 0);
677    render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
678}
679
680// ---------------------------------------------------------------------------
681// DrawCtx blanket impl for GfxCtx
682// ---------------------------------------------------------------------------
683
684impl crate::draw_ctx::DrawCtx for GfxCtx<'_> {
685    fn set_fill_color(&mut self, c: crate::color::Color)     { self.set_fill_color(c) }
686    fn set_stroke_color(&mut self, c: crate::color::Color)   { self.set_stroke_color(c) }
687    fn set_line_width(&mut self, w: f64)                      { self.set_line_width(w) }
688    fn set_line_join(&mut self, j: agg_rust::math_stroke::LineJoin) { self.set_line_join(j) }
689    fn set_line_cap(&mut self, c: agg_rust::math_stroke::LineCap)   { self.set_line_cap(c) }
690    fn set_blend_mode(&mut self, m: agg_rust::comp_op::CompOp)      { self.set_blend_mode(m) }
691    fn set_global_alpha(&mut self, a: f64)                   { self.set_global_alpha(a) }
692    fn set_font(&mut self, f: Arc<crate::text::Font>)        { self.set_font(f) }
693    fn set_font_size(&mut self, s: f64)                      { self.set_font_size(s) }
694    fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64) { self.clip_rect(x, y, w, h) }
695    fn reset_clip(&mut self)                                  { self.reset_clip() }
696    fn clear(&mut self, c: crate::color::Color)              { self.clear(c) }
697    fn begin_path(&mut self)                                  { self.begin_path() }
698    fn move_to(&mut self, x: f64, y: f64)                    { self.move_to(x, y) }
699    fn line_to(&mut self, x: f64, y: f64)                    { self.line_to(x, y) }
700    fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64) {
701        self.cubic_to(cx1, cy1, cx2, cy2, x, y)
702    }
703    fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64) { self.quad_to(cx, cy, x, y) }
704    fn arc_to(&mut self, cx: f64, cy: f64, r: f64, a1: f64, a2: f64, ccw: bool) {
705        self.arc_to(cx, cy, r, a1, a2, ccw)
706    }
707    fn circle(&mut self, cx: f64, cy: f64, r: f64)          { self.circle(cx, cy, r) }
708    fn rect(&mut self, x: f64, y: f64, w: f64, h: f64)      { self.rect(x, y, w, h) }
709    fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
710        self.rounded_rect(x, y, w, h, r)
711    }
712    fn close_path(&mut self)                                  { self.close_path() }
713    fn fill(&mut self)                                        { self.fill() }
714    fn stroke(&mut self)                                      { self.stroke() }
715    fn fill_and_stroke(&mut self)                             { self.fill_and_stroke() }
716
717    fn draw_triangles_aa(
718        &mut self,
719        vertices: &[[f32; 3]],
720        indices:  &[u32],
721        color:    crate::color::Color,
722    ) {
723        // Software fallback: rasterise each triangle as a solid filled
724        // polygon.  The per-vertex `alpha` is ignored (software already has
725        // analytic AA via the scanline rasteriser), so halo quads from the
726        // GPU pipeline end up as redundant thin slivers — visually harmless
727        // but inefficient.  Callers that care should check `has_image_blit`
728        // / a similar capability flag; for now this keeps parity with the
729        // trait so the Lion demo renders correctly on the CPU path too.
730        let saved_fill = self.state.fill_color;
731        self.set_fill_color(color);
732        let n_tris = indices.len() / 3;
733        for t in 0..n_tris {
734            let i0 = indices[t * 3    ] as usize;
735            let i1 = indices[t * 3 + 1] as usize;
736            let i2 = indices[t * 3 + 2] as usize;
737            if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() { continue; }
738            let v0 = vertices[i0];
739            let v1 = vertices[i1];
740            let v2 = vertices[i2];
741            self.begin_path();
742            self.move_to(v0[0] as f64, v0[1] as f64);
743            self.line_to(v1[0] as f64, v1[1] as f64);
744            self.line_to(v2[0] as f64, v2[1] as f64);
745            self.close_path();
746            self.fill();
747        }
748        self.set_fill_color(saved_fill);
749    }
750    fn fill_text(&mut self, t: &str, x: f64, y: f64)        { self.fill_text(t, x, y) }
751    fn fill_text_gsv(&mut self, t: &str, x: f64, y: f64, s: f64) { self.fill_text_gsv(t, x, y, s) }
752    fn measure_text(&self, t: &str) -> Option<crate::text::TextMetrics> { self.measure_text(t) }
753    fn transform(&self) -> agg_rust::trans_affine::TransAffine { self.transform() }
754    fn save(&mut self)                                        { self.save() }
755    fn restore(&mut self)                                     { self.restore() }
756    fn translate(&mut self, tx: f64, ty: f64)                { self.translate(tx, ty) }
757    fn rotate(&mut self, r: f64)                             { self.rotate(r) }
758    fn scale(&mut self, sx: f64, sy: f64)                    { self.scale(sx, sy) }
759    fn set_transform(&mut self, m: agg_rust::trans_affine::TransAffine) { self.set_transform(m) }
760    fn reset_transform(&mut self)                             { self.reset_transform() }
761    fn push_layer(&mut self, w: f64, h: f64)                 { self.push_layer(w, h) }
762    fn pop_layer(&mut self)                                   { self.pop_layer() }
763
764    fn has_image_blit(&self) -> bool { true }
765
766    fn draw_image_rgba_arc(
767        &mut self,
768        data:  &Arc<Vec<u8>>,
769        img_w: u32,
770        img_h: u32,
771        dst_x: f64,
772        dst_y: f64,
773        dst_w: f64,
774        dst_h: f64,
775    ) {
776        // Software backend has no GPU texture cache; the CPU composite path
777        // is the same as the slice entry point.
778        self.draw_image_rgba(data.as_slice(), img_w, img_h, dst_x, dst_y, dst_w, dst_h);
779    }
780
781    fn draw_lcd_backbuffer_arc(
782        &mut self,
783        color: &Arc<Vec<u8>>,
784        alpha: &Arc<Vec<u8>>,
785        w: u32,
786        h: u32,
787        dst_x: f64,
788        dst_y: f64,
789        _dst_w: f64,
790        _dst_h: f64,
791    ) {
792        // Per-channel premultiplied src-over directly onto the active
793        // framebuffer.  Preserves LCD chroma: each subpixel's alpha
794        // drives the src-over of that subpixel's colour into the
795        // destination independently of the other two.
796        //
797        // Inputs are **top-row-first** (matches the cache layout); the
798        // destination `Framebuffer` is Y-up with row 0 at the bottom, so
799        // src row `sy` maps to dst row `origin_y + (h-1-sy)`.
800        if w == 0 || h == 0 { return; }
801        let w_u = w as usize;
802        let h_u = h as usize;
803        if color.len() < w_u * h_u * 3 || alpha.len() < w_u * h_u * 3 { return; }
804
805        let t = &self.state.transform;
806        let sx = (dst_x * t.sx + dst_y * t.shx + t.tx).round() as i32;
807        let sy = (dst_x * t.shy + dst_y * t.sy + t.ty).round() as i32;
808        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
809        let fw = fb.width()  as i32;
810        let fh = fb.height() as i32;
811        let fw_u = fw as usize;
812        let pixels = fb.pixels_mut();
813
814        for src_y in 0..h_u {
815            // Top-row-first src → Y-up dst: src row 0 (visually top)
816            // lands at dst_y + h - 1 (the visually-top dst row).
817            let dy = sy + (h_u - 1 - src_y) as i32;
818            if dy < 0 || dy >= fh { continue; }
819            let dy_u = dy as usize;
820            for src_x in 0..w_u {
821                let dx = sx + src_x as i32;
822                if dx < 0 || dx >= fw { continue; }
823                let ci = (src_y * w_u + src_x) * 3;
824
825                let sa_r = alpha[ci]     as f32 / 255.0;
826                let sa_g = alpha[ci + 1] as f32 / 255.0;
827                let sa_b = alpha[ci + 2] as f32 / 255.0;
828                if sa_r == 0.0 && sa_g == 0.0 && sa_b == 0.0 { continue; }
829
830                let sc_r = color[ci]     as f32 / 255.0;
831                let sc_g = color[ci + 1] as f32 / 255.0;
832                let sc_b = color[ci + 2] as f32 / 255.0;
833
834                let di = (dy_u * fw_u + dx as usize) * 4;
835                // Framebuffer holds premultiplied RGBA.  Per-channel
836                // src-over is `dst = src + dst * (1 - src_a)` since src
837                // is already premultiplied.  Alpha composites via
838                // max-channel-alpha so the destination picks up full
839                // opacity wherever any subpixel was painted — matches
840                // "this pixel was drawn on" for subsequent SrcOver blits.
841                let dc_r = pixels[di]     as f32 / 255.0;
842                let dc_g = pixels[di + 1] as f32 / 255.0;
843                let dc_b = pixels[di + 2] as f32 / 255.0;
844                let da   = pixels[di + 3] as f32 / 255.0;
845
846                let rc_r = sc_r + dc_r * (1.0 - sa_r);
847                let rc_g = sc_g + dc_g * (1.0 - sa_g);
848                let rc_b = sc_b + dc_b * (1.0 - sa_b);
849                let src_a_max = sa_r.max(sa_g).max(sa_b);
850                let ra = src_a_max + da * (1.0 - src_a_max);
851
852                pixels[di]     = (rc_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
853                pixels[di + 1] = (rc_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
854                pixels[di + 2] = (rc_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
855                pixels[di + 3] = (ra   * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
856            }
857        }
858    }
859
860    fn has_lcd_mask_composite(&self) -> bool { true }
861
862    fn draw_lcd_mask(
863        &mut self,
864        mask:      &[u8],
865        mask_w:    u32,
866        mask_h:    u32,
867        src_color: Color,
868        dst_x:     f64,
869        dst_y:     f64,
870    ) {
871        // Resolve to the active target (base fb or topmost layer) with
872        // the current CTM applied to the placement origin.  Both the
873        // mask and the Framebuffer are Y-up (row 0 = bottom), so mask
874        // row `my` maps directly to dst row `sy + my`.
875        if mask.len() < (mask_w as usize) * (mask_h as usize) * 3 { return; }
876        let t = &self.state.transform;
877        let sx = dst_x * t.sx + dst_y * t.shx + t.tx;
878        let sy = dst_x * t.shy + dst_y * t.sy + t.ty;
879        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
880        let fw = fb.width();
881        let fh = fb.height();
882        let origin_x = sx.round() as i32;
883        let origin_y = sy.round() as i32;
884
885        let sa = src_color.a.clamp(0.0, 1.0);
886        let sr = src_color.r.clamp(0.0, 1.0);
887        let sg = src_color.g.clamp(0.0, 1.0);
888        let sb = src_color.b.clamp(0.0, 1.0);
889        let fw_i = fw as i32;
890        let fh_i = fh as i32;
891        let mw_i = mask_w as i32;
892        let mh_i = mask_h as i32;
893        let pixels = fb.pixels_mut();
894
895        for my in 0..mh_i {
896            // Mask row `my` (Y-up: 0 = bottom) → dst row `origin_y + my`
897            // in the Y-up framebuffer.  No flip.
898            let dy = origin_y + my;
899            if dy < 0 || dy >= fh_i { continue; }
900            for mx in 0..mw_i {
901                let dx = origin_x + mx;
902                if dx < 0 || dx >= fw_i { continue; }
903                let mi = ((my * mw_i + mx) * 3) as usize;
904                // Per-channel coverage × src alpha — partial-alpha src
905                // (e.g. `text_dim` placeholder colour) fades proportionally.
906                let cr = (mask[mi]     as f32 / 255.0) * sa;
907                let cg = (mask[mi + 1] as f32 / 255.0) * sa;
908                let cb = (mask[mi + 2] as f32 / 255.0) * sa;
909                if cr == 0.0 && cg == 0.0 && cb == 0.0 { continue; }
910                let di = ((dy * fw_i + dx) * 4) as usize;
911                let dr = pixels[di]     as f32 / 255.0;
912                let dg = pixels[di + 1] as f32 / 255.0;
913                let db = pixels[di + 2] as f32 / 255.0;
914                let rr  = sr * cr + dr * (1.0 - cr);
915                let rg  = sg * cg + dg * (1.0 - cg);
916                let rbb = sb * cb + db * (1.0 - cb);
917                pixels[di]     = (rr  * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
918                pixels[di + 1] = (rg  * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
919                pixels[di + 2] = (rbb * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
920                // Alpha unchanged — we're writing onto an existing opaque
921                // (or semi-transparent) surface without introducing new
922                // transparency.
923            }
924        }
925    }
926
927    fn draw_image_rgba(
928        &mut self,
929        data:  &[u8],
930        img_w: u32,
931        img_h: u32,
932        dst_x: f64,
933        dst_y: f64,
934        dst_w: f64,
935        dst_h: f64,
936    ) {
937        // Scale the source image into a temporary Framebuffer at dst size,
938        // then composite it onto the current render target using the CTM origin.
939        if img_w == 0 || img_h == 0 || dst_w < 1.0 || dst_h < 1.0 { return; }
940
941        let out_w = dst_w.round() as u32;
942        let out_h = dst_h.round() as u32;
943        let mut scaled = crate::framebuffer::Framebuffer::new(out_w, out_h);
944
945        // Nearest-neighbour scale — sufficient for README screenshots / badges.
946        // `data` is straight-alpha by the `draw_image_rgba` convention; AGG
947        // framebuffers store **premultiplied** RGBA, so we premultiply each
948        // sampled pixel on the way in so `composite_framebuffers` (which uses
949        // premultiplied SrcOver) blends with correct intensity.
950        let px = scaled.pixels_mut();
951        for dy in 0..out_h {
952            for dx in 0..out_w {
953                let sx = (dx as f64 / out_w as f64 * img_w as f64) as u32;
954                // Image is top-row-first; Y-up dst means we flip sy.
955                let sy_img = ((1.0 - (dy as f64 + 0.5) / out_h as f64) * img_h as f64)
956                    .floor()
957                    .clamp(0.0, (img_h - 1) as f64) as u32;
958                let si = ((sy_img * img_w + sx) * 4) as usize;
959                let di = ((dy * out_w + dx) * 4) as usize;
960                if si + 3 < data.len() && di + 3 < px.len() {
961                    let a = data[si + 3] as u32;
962                    if a == 255 {
963                        px[di]     = data[si];
964                        px[di + 1] = data[si + 1];
965                        px[di + 2] = data[si + 2];
966                        px[di + 3] = 255;
967                    } else {
968                        // Premultiply: (c * a + 127) / 255 (round-half-up).
969                        px[di]     = (((data[si]     as u32) * a + 127) / 255) as u8;
970                        px[di + 1] = (((data[si + 1] as u32) * a + 127) / 255) as u8;
971                        px[di + 2] = (((data[si + 2] as u32) * a + 127) / 255) as u8;
972                        px[di + 3] = a as u8;
973                    }
974                }
975            }
976        }
977
978        // Apply CTM translation to get screen-space origin.
979        let (tx, ty) = { let t = self.transform(); (t.tx, t.ty) };
980        let screen_x = (tx + dst_x).round() as i32;
981        let screen_y = (ty + dst_y).round() as i32;
982        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
983        composite_framebuffers(fb, &scaled, screen_x, screen_y);
984    }
985}
986
987/// Apply a Y-up scissor clip to a `RendererBase` (pixel-inclusive coordinates).
988pub(crate) fn apply_clip<PF: agg_rust::pixfmt_rgba::PixelFormat>(
989    rb: &mut RendererBase<PF>,
990    clip: Option<(f64, f64, f64, f64)>,
991) {
992    if let Some((x, y, w, h)) = clip {
993        let x1 = x.floor() as i32;
994        let y1 = y.floor() as i32;
995        let x2 = (x + w).ceil() as i32 - 1;
996        let y2 = (y + h).ceil() as i32 - 1;
997        rb.clip_box_i(x1, y1, x2, y2);
998    }
999}