Skip to main content

agg_gui/gfx_ctx/
draw_impl.rs

1use super::*;
2
3// ---------------------------------------------------------------------------
4// Active-framebuffer helper
5// ---------------------------------------------------------------------------
6
7/// Return a `&mut Framebuffer` for the currently active render target.
8///
9/// If any layers are on the stack, returns the top layer's framebuffer.
10/// Otherwise returns the base framebuffer.  Accepts the two fields as
11/// separate `&mut` references so callers can simultaneously borrow other
12/// `GfxCtx` fields (e.g. `state`, `path`) without triggering borrow
13/// conflicts on `self`.
14#[inline]
15pub(super) fn active_fb<'a>(
16    base_fb: &'a mut Framebuffer,
17    layer_stack: &'a mut Vec<LayerEntry>,
18) -> &'a mut Framebuffer {
19    if let Some(top) = layer_stack.last_mut() {
20        &mut top.fb
21    } else {
22        base_fb
23    }
24}
25
26// ---------------------------------------------------------------------------
27// SrcOver layer compositing
28// ---------------------------------------------------------------------------
29
30/// Composite `src` onto `dst` using SrcOver alpha blending.
31///
32/// AGG writes **premultiplied** RGBA into framebuffers.  The premultiplied
33/// SrcOver formula is:
34///
35/// ```text
36/// out_channel = src_premul + dst_premul × (1 − src_alpha_norm)
37/// ```
38///
39/// This applies identically to all four channels (R, G, B, A), which makes
40/// the implementation straightforward and avoids the division step needed for
41/// straight-alpha compositing.
42///
43/// `dest_x` / `dest_y` are the Y-up pixel coordinates in `dst` where the
44/// bottom-left corner of `src` lands.  Out-of-bounds pixels are silently clipped.
45pub(super) fn composite_framebuffers(
46    dst: &mut Framebuffer,
47    src: &Framebuffer,
48    dest_x: i32,
49    dest_y: i32,
50    alpha: f64,
51    clip: Option<(f64, f64, f64, f64)>,
52) {
53    let src_w = src.width() as i32;
54    let src_h = src.height() as i32;
55    let dst_w = dst.width() as i32;
56    let dst_h = dst.height() as i32;
57
58    // Destination scissor bounds in Y-up pixel space (half-open).  `clip` is a
59    // screen-space rect in the same coordinates as `dest_x/dest_y`; a composite
60    // (e.g. a popped layer) must not paint outside the scissor that was active
61    // when the layer was pushed.
62    let (cx1, cy1, cx2, cy2) = match clip {
63        Some((cx, cy, cw, ch)) => (
64            cx.floor() as i32,
65            cy.floor() as i32,
66            (cx + cw).ceil() as i32,
67            (cy + ch).ceil() as i32,
68        ),
69        None => (0, 0, dst_w, dst_h),
70    };
71
72    let src_px = src.pixels();
73    let dst_px = dst.pixels_mut();
74
75    for sy in 0..src_h {
76        let dy = dest_y + sy;
77        if dy < 0 || dy >= dst_h || dy < cy1 || dy >= cy2 {
78            continue;
79        }
80        for sx in 0..src_w {
81            let dx = dest_x + sx;
82            if dx < 0 || dx >= dst_w || dx < cx1 || dx >= cx2 {
83                continue;
84            }
85            let si = ((sy * src_w + sx) * 4) as usize;
86            let di = ((dy * dst_w + dx) * 4) as usize;
87            let layer_alpha = alpha.clamp(0.0, 1.0) as f32;
88            let sa = (src_px[si + 3] as f32 / 255.0) * layer_alpha;
89            if sa < 1e-4 {
90                continue;
91            } // fully transparent source — skip
92            let inv_sa = 1.0 - sa;
93            // Premultiplied SrcOver — same formula for all four channels.
94            for k in 0..4 {
95                let s = src_px[si + k] as f32 * layer_alpha;
96                let d = dst_px[di + k] as f32;
97                dst_px[di + k] = (s + d * inv_sa).round().clamp(0.0, 255.0) as u8;
98            }
99        }
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Free rasterization helpers — take explicit path and fb references so they
105// can be called for both self.path draws and per-glyph text draws without
106// borrow-checker conflicts.
107// ---------------------------------------------------------------------------
108
109pub(crate) fn rasterize_fill(
110    fb: &mut Framebuffer,
111    path: &mut PathStorage,
112    color: &agg_rust::color::Rgba8,
113    mode: CompOp,
114    clip: Option<(f64, f64, f64, f64)>,
115    fill_rule: FillRule,
116    transform: &TransAffine,
117) {
118    let w = fb.width();
119    let h = fb.height();
120    let stride = (w * 4) as i32;
121    let mut ra = RowAccessor::new();
122    unsafe { ra.attach(fb.pixels_mut().as_mut_ptr(), w, h, stride) };
123    let pf = PixfmtRgba32CompOp::new_with_op(&mut ra, mode);
124    let mut rb = RendererBase::new(pf);
125    apply_clip(&mut rb, clip);
126
127    let mut ras = RasterizerScanlineAa::new();
128    ras.filling_rule(to_agg_fill_rule(fill_rule));
129    let mut sl = ScanlineU8::new();
130    let mut curves = ConvCurve::new(path);
131    let mut transformed = ConvTransform::new(&mut curves, transform.clone());
132    ras.add_path(&mut transformed, 0);
133    render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
134}
135
136fn to_agg_fill_rule(rule: FillRule) -> FillingRule {
137    match rule {
138        FillRule::NonZero => FillingRule::NonZero,
139        FillRule::EvenOdd => FillingRule::EvenOdd,
140    }
141}
142
143pub(crate) fn rasterize_stroke(
144    fb: &mut Framebuffer,
145    path: &mut PathStorage,
146    color: &agg_rust::color::Rgba8,
147    width: f64,
148    join: LineJoin,
149    cap: LineCap,
150    miter_limit: f64,
151    dashes: &[f64],
152    dash_offset: f64,
153    mode: CompOp,
154    clip: Option<(f64, f64, f64, f64)>,
155    transform: &TransAffine,
156) {
157    let mut curves = ConvCurve::new(path);
158    if dashes.is_empty() {
159        rasterize_stroke_source(
160            fb,
161            &mut curves,
162            color,
163            width,
164            join,
165            cap,
166            miter_limit,
167            mode,
168            clip,
169            transform,
170        );
171    } else {
172        let mut dash = ConvDash::new(&mut curves);
173        configure_dashes(&mut dash, dashes, dash_offset);
174        rasterize_stroke_source(
175            fb,
176            dash,
177            color,
178            width,
179            join,
180            cap,
181            miter_limit,
182            mode,
183            clip,
184            transform,
185        );
186    }
187}
188
189fn rasterize_stroke_source<VS: VertexSource>(
190    fb: &mut Framebuffer,
191    source: VS,
192    color: &agg_rust::color::Rgba8,
193    width: f64,
194    join: LineJoin,
195    cap: LineCap,
196    miter_limit: f64,
197    mode: CompOp,
198    clip: Option<(f64, f64, f64, f64)>,
199    transform: &TransAffine,
200) {
201    let w = fb.width();
202    let h = fb.height();
203    let stride = (w * 4) as i32;
204    let mut ra = RowAccessor::new();
205    unsafe { ra.attach(fb.pixels_mut().as_mut_ptr(), w, h, stride) };
206    let pf = PixfmtRgba32CompOp::new_with_op(&mut ra, mode);
207    let mut rb = RendererBase::new(pf);
208    apply_clip(&mut rb, clip);
209
210    let mut ras = RasterizerScanlineAa::new();
211    let mut sl = ScanlineU8::new();
212    let mut stroke = ConvStroke::new(source);
213    stroke.set_width(width);
214    stroke.set_line_join(join);
215    stroke.set_line_cap(cap);
216    stroke.set_miter_limit(miter_limit);
217    let mut transformed = ConvTransform::new(&mut stroke, transform.clone());
218    ras.add_path(&mut transformed, 0);
219    render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, color);
220}
221
222fn configure_dashes<VS: VertexSource>(dash: &mut ConvDash<VS>, dashes: &[f64], dash_offset: f64) {
223    let mut chunks = dashes.chunks_exact(2);
224    for pair in &mut chunks {
225        dash.add_dash(pair[0], pair[1]);
226    }
227    if let Some(&last) = chunks.remainder().first() {
228        dash.add_dash(last, last);
229    }
230    dash.dash_start(dash_offset);
231}
232
233// ---------------------------------------------------------------------------
234// DrawCtx blanket impl for GfxCtx
235// ---------------------------------------------------------------------------
236
237impl crate::draw_ctx::DrawCtx for GfxCtx<'_> {
238    fn set_fill_color(&mut self, c: crate::color::Color) {
239        self.set_fill_color(c)
240    }
241    fn set_fill_linear_gradient(&mut self, gradient: crate::draw_ctx::LinearGradientPaint) {
242        self.set_fill_linear_gradient(gradient)
243    }
244    fn set_fill_radial_gradient(&mut self, gradient: crate::draw_ctx::RadialGradientPaint) {
245        self.set_fill_radial_gradient(gradient)
246    }
247    fn set_fill_pattern(&mut self, pattern: crate::draw_ctx::PatternPaint) {
248        self.set_fill_pattern(pattern)
249    }
250    fn supports_fill_linear_gradient(&self) -> bool {
251        true
252    }
253    fn supports_fill_radial_gradient(&self) -> bool {
254        true
255    }
256    fn supports_fill_pattern(&self) -> bool {
257        true
258    }
259    fn set_stroke_color(&mut self, c: crate::color::Color) {
260        self.set_stroke_color(c)
261    }
262    fn set_stroke_linear_gradient(&mut self, gradient: crate::draw_ctx::LinearGradientPaint) {
263        self.set_stroke_linear_gradient(gradient)
264    }
265    fn set_stroke_radial_gradient(&mut self, gradient: crate::draw_ctx::RadialGradientPaint) {
266        self.set_stroke_radial_gradient(gradient)
267    }
268    fn set_stroke_pattern(&mut self, pattern: crate::draw_ctx::PatternPaint) {
269        self.set_stroke_pattern(pattern)
270    }
271    fn supports_stroke_linear_gradient(&self) -> bool {
272        true
273    }
274    fn supports_stroke_radial_gradient(&self) -> bool {
275        true
276    }
277    fn supports_stroke_pattern(&self) -> bool {
278        true
279    }
280    fn set_line_width(&mut self, w: f64) {
281        self.set_line_width(w)
282    }
283    fn set_line_join(&mut self, j: agg_rust::math_stroke::LineJoin) {
284        self.set_line_join(j)
285    }
286    fn set_line_cap(&mut self, c: agg_rust::math_stroke::LineCap) {
287        self.set_line_cap(c)
288    }
289    fn set_miter_limit(&mut self, limit: f64) {
290        self.set_miter_limit(limit)
291    }
292    fn set_line_dash(&mut self, dashes: &[f64], offset: f64) {
293        self.set_line_dash(dashes, offset)
294    }
295    fn set_fill_rule(&mut self, rule: crate::draw_ctx::FillRule) {
296        self.set_fill_rule(rule)
297    }
298    fn set_blend_mode(&mut self, m: agg_rust::comp_op::CompOp) {
299        self.set_blend_mode(m)
300    }
301    fn set_global_alpha(&mut self, a: f64) {
302        self.set_global_alpha(a)
303    }
304    fn set_font(&mut self, f: Arc<crate::text::Font>) {
305        self.set_font(f)
306    }
307    fn set_font_size(&mut self, s: f64) {
308        self.set_font_size(s)
309    }
310    fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
311        self.clip_rect(x, y, w, h)
312    }
313    fn reset_clip(&mut self) {
314        self.reset_clip()
315    }
316    fn clear(&mut self, c: crate::color::Color) {
317        self.clear(c)
318    }
319    fn begin_path(&mut self) {
320        self.begin_path()
321    }
322    fn move_to(&mut self, x: f64, y: f64) {
323        self.move_to(x, y)
324    }
325    fn line_to(&mut self, x: f64, y: f64) {
326        self.line_to(x, y)
327    }
328    fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64) {
329        self.cubic_to(cx1, cy1, cx2, cy2, x, y)
330    }
331    fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64) {
332        self.quad_to(cx, cy, x, y)
333    }
334    fn arc_to(&mut self, cx: f64, cy: f64, r: f64, a1: f64, a2: f64, ccw: bool) {
335        self.arc_to(cx, cy, r, a1, a2, ccw)
336    }
337    fn circle(&mut self, cx: f64, cy: f64, r: f64) {
338        self.circle(cx, cy, r)
339    }
340    fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
341        self.rect(x, y, w, h)
342    }
343    fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
344        self.rounded_rect(x, y, w, h, r)
345    }
346    fn close_path(&mut self) {
347        self.close_path()
348    }
349    fn fill(&mut self) {
350        self.fill()
351    }
352    fn stroke(&mut self) {
353        self.stroke()
354    }
355    fn fill_and_stroke(&mut self) {
356        self.fill_and_stroke()
357    }
358
359    fn draw_triangles_aa(
360        &mut self,
361        vertices: &[[f32; 3]],
362        indices: &[u32],
363        color: crate::color::Color,
364    ) {
365        // Software fallback: rasterise each triangle as a solid filled
366        // polygon.  The per-vertex `alpha` is ignored (software already has
367        // analytic AA via the scanline rasteriser), so halo quads from the
368        // GPU pipeline end up as redundant thin slivers — visually harmless
369        // but inefficient.  Callers that care should check `has_image_blit`
370        // / a similar capability flag; for now this keeps parity with the
371        // trait so the Lion demo renders correctly on the CPU path too.
372        let saved_fill = self.state.fill_color;
373        self.set_fill_color(color);
374        let n_tris = indices.len() / 3;
375        for t in 0..n_tris {
376            let i0 = indices[t * 3] as usize;
377            let i1 = indices[t * 3 + 1] as usize;
378            let i2 = indices[t * 3 + 2] as usize;
379            if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() {
380                continue;
381            }
382            let v0 = vertices[i0];
383            let v1 = vertices[i1];
384            let v2 = vertices[i2];
385            self.begin_path();
386            self.move_to(v0[0] as f64, v0[1] as f64);
387            self.line_to(v1[0] as f64, v1[1] as f64);
388            self.line_to(v2[0] as f64, v2[1] as f64);
389            self.close_path();
390            self.fill();
391        }
392        self.set_fill_color(saved_fill);
393    }
394    fn fill_text(&mut self, t: &str, x: f64, y: f64) {
395        self.fill_text(t, x, y)
396    }
397    fn fill_text_gsv(&mut self, t: &str, x: f64, y: f64, s: f64) {
398        self.fill_text_gsv(t, x, y, s)
399    }
400    fn measure_text(&self, t: &str) -> Option<crate::text::TextMetrics> {
401        self.measure_text(t)
402    }
403    fn transform(&self) -> agg_rust::trans_affine::TransAffine {
404        self.transform()
405    }
406    fn root_transform(&self) -> agg_rust::trans_affine::TransAffine {
407        let mut t = self.transform();
408        for layer in self.layer_stack.iter().rev() {
409            t.premultiply(&agg_rust::trans_affine::TransAffine::new_translation(
410                layer.origin_x,
411                layer.origin_y,
412            ));
413        }
414        t
415    }
416    fn save(&mut self) {
417        self.save()
418    }
419    fn restore(&mut self) {
420        self.restore()
421    }
422    fn translate(&mut self, tx: f64, ty: f64) {
423        self.translate(tx, ty)
424    }
425    fn rotate(&mut self, r: f64) {
426        self.rotate(r)
427    }
428    fn scale(&mut self, sx: f64, sy: f64) {
429        self.scale(sx, sy)
430    }
431    fn set_transform(&mut self, m: agg_rust::trans_affine::TransAffine) {
432        self.set_transform(m)
433    }
434    fn reset_transform(&mut self) {
435        self.reset_transform()
436    }
437    fn push_layer(&mut self, w: f64, h: f64) {
438        self.push_layer(w, h)
439    }
440    fn supports_compositing_layers(&self) -> bool {
441        true
442    }
443    fn push_layer_with_alpha(&mut self, w: f64, h: f64, alpha: f64) {
444        self.push_layer_with_alpha(w, h, alpha)
445    }
446    fn pop_layer(&mut self) {
447        self.pop_layer()
448    }
449
450    fn has_image_blit(&self) -> bool {
451        true
452    }
453
454    fn draw_image_rgba_arc(
455        &mut self,
456        data: &Arc<Vec<u8>>,
457        img_w: u32,
458        img_h: u32,
459        dst_x: f64,
460        dst_y: f64,
461        dst_w: f64,
462        dst_h: f64,
463    ) {
464        // Software backend has no GPU texture cache; the CPU composite path
465        // is the same as the slice entry point.
466        self.draw_image_rgba(data.as_slice(), img_w, img_h, dst_x, dst_y, dst_w, dst_h);
467    }
468
469    fn draw_lcd_backbuffer_arc(
470        &mut self,
471        color: &Arc<Vec<u8>>,
472        alpha: &Arc<Vec<u8>>,
473        _content_version: u64,
474        w: u32,
475        h: u32,
476        dst_x: f64,
477        dst_y: f64,
478        _dst_w: f64,
479        _dst_h: f64,
480    ) {
481        // Software backend composites straight from the CPU planes every frame,
482        // so it has no texture cache to key on `_content_version`.
483        // Per-channel premultiplied src-over directly onto the active
484        // framebuffer.  Preserves LCD chroma: each subpixel's alpha
485        // drives the src-over of that subpixel's colour into the
486        // destination independently of the other two.
487        //
488        // Inputs are **top-row-first** (matches the cache layout); the
489        // destination `Framebuffer` is Y-up with row 0 at the bottom, so
490        // src row `sy` maps to dst row `origin_y + (h-1-sy)`.
491        if w == 0 || h == 0 {
492            return;
493        }
494        let w_u = w as usize;
495        let h_u = h as usize;
496        if color.len() < w_u * h_u * 3 || alpha.len() < w_u * h_u * 3 {
497            return;
498        }
499
500        let t = &self.state.transform;
501        let sx = (dst_x * t.sx + dst_y * t.shx + t.tx).round() as i32;
502        let sy = (dst_x * t.shy + dst_y * t.sy + t.ty).round() as i32;
503        // Honor `global_alpha`: scale both the premultiplied source colour and
504        // the per-channel coverage so LCD-cached text inside a faded subtree
505        // fades with the group.  Multiplying colour and alpha by the same
506        // factor keeps the plane premultiplied-consistent.
507        let ga = self.state.global_alpha.clamp(0.0, 1.0) as f32;
508        // Honor the active clip (screen-space Y-up AABB) so over-scan band
509        // backbuffers (see `Widget::backbuffer_band`) crop their margins to the
510        // widget bounds instead of painting over sibling widgets.
511        let clip = self.state.clip;
512        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
513        let fw = fb.width() as i32;
514        let fh = fb.height() as i32;
515        let (cx1, cy1, cx2, cy2) = match clip {
516            Some((cx, cy, cw, ch)) => (
517                cx.floor() as i32,
518                cy.floor() as i32,
519                (cx + cw).ceil() as i32,
520                (cy + ch).ceil() as i32,
521            ),
522            None => (0, 0, fw, fh),
523        };
524        let fw_u = fw as usize;
525        let pixels = fb.pixels_mut();
526
527        for src_y in 0..h_u {
528            // Top-row-first src → Y-up dst: src row 0 (visually top)
529            // lands at dst_y + h - 1 (the visually-top dst row).
530            let dy = sy + (h_u - 1 - src_y) as i32;
531            if dy < 0 || dy >= fh || dy < cy1 || dy >= cy2 {
532                continue;
533            }
534            let dy_u = dy as usize;
535            for src_x in 0..w_u {
536                let dx = sx + src_x as i32;
537                if dx < 0 || dx >= fw || dx < cx1 || dx >= cx2 {
538                    continue;
539                }
540                let ci = (src_y * w_u + src_x) * 3;
541
542                let sa_r = (alpha[ci] as f32 / 255.0) * ga;
543                let sa_g = (alpha[ci + 1] as f32 / 255.0) * ga;
544                let sa_b = (alpha[ci + 2] as f32 / 255.0) * ga;
545                if sa_r == 0.0 && sa_g == 0.0 && sa_b == 0.0 {
546                    continue;
547                }
548
549                let sc_r = (color[ci] as f32 / 255.0) * ga;
550                let sc_g = (color[ci + 1] as f32 / 255.0) * ga;
551                let sc_b = (color[ci + 2] as f32 / 255.0) * ga;
552
553                let di = (dy_u * fw_u + dx as usize) * 4;
554                // Framebuffer holds premultiplied RGBA.  Per-channel
555                // src-over is `dst = src + dst * (1 - src_a)` since src
556                // is already premultiplied.  Alpha composites via
557                // max-channel-alpha so the destination picks up full
558                // opacity wherever any subpixel was painted — matches
559                // "this pixel was drawn on" for subsequent SrcOver blits.
560                let dc_r = pixels[di] as f32 / 255.0;
561                let dc_g = pixels[di + 1] as f32 / 255.0;
562                let dc_b = pixels[di + 2] as f32 / 255.0;
563                let da = pixels[di + 3] as f32 / 255.0;
564
565                let rc_r = sc_r + dc_r * (1.0 - sa_r);
566                let rc_g = sc_g + dc_g * (1.0 - sa_g);
567                let rc_b = sc_b + dc_b * (1.0 - sa_b);
568                let src_a_max = sa_r.max(sa_g).max(sa_b);
569                let ra = src_a_max + da * (1.0 - src_a_max);
570
571                pixels[di] = (rc_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
572                pixels[di + 1] = (rc_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
573                pixels[di + 2] = (rc_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
574                pixels[di + 3] = (ra * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
575            }
576        }
577    }
578
579    fn has_lcd_mask_composite(&self) -> bool {
580        true
581    }
582
583    fn draw_lcd_mask(
584        &mut self,
585        mask: &[u8],
586        mask_w: u32,
587        mask_h: u32,
588        src_color: Color,
589        dst_x: f64,
590        dst_y: f64,
591    ) {
592        // Resolve to the active target (base fb or topmost layer) with
593        // the current CTM applied to the placement origin.  Both the
594        // mask and the Framebuffer are Y-up (row 0 = bottom), so mask
595        // row `my` maps directly to dst row `sy + my`.
596        if mask.len() < (mask_w as usize) * (mask_h as usize) * 3 {
597            return;
598        }
599        let t = &self.state.transform;
600        let sx = dst_x * t.sx + dst_y * t.shx + t.tx;
601        let sy = dst_x * t.shy + dst_y * t.sy + t.ty;
602        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
603        let fw = fb.width();
604        let fh = fb.height();
605        let origin_x = sx.round() as i32;
606        let origin_y = sy.round() as i32;
607
608        let sa = src_color.a.clamp(0.0, 1.0);
609        let sr = src_color.r.clamp(0.0, 1.0);
610        let sg = src_color.g.clamp(0.0, 1.0);
611        let sb = src_color.b.clamp(0.0, 1.0);
612        let fw_i = fw as i32;
613        let fh_i = fh as i32;
614        let mw_i = mask_w as i32;
615        let mh_i = mask_h as i32;
616        let pixels = fb.pixels_mut();
617
618        for my in 0..mh_i {
619            // Mask row `my` (Y-up: 0 = bottom) → dst row `origin_y + my`
620            // in the Y-up framebuffer.  No flip.
621            let dy = origin_y + my;
622            if dy < 0 || dy >= fh_i {
623                continue;
624            }
625            for mx in 0..mw_i {
626                let dx = origin_x + mx;
627                if dx < 0 || dx >= fw_i {
628                    continue;
629                }
630                let mi = ((my * mw_i + mx) * 3) as usize;
631                // Per-channel coverage × src alpha — partial-alpha src
632                // (e.g. `text_dim` placeholder colour) fades proportionally.
633                let cr = (mask[mi] as f32 / 255.0) * sa;
634                let cg = (mask[mi + 1] as f32 / 255.0) * sa;
635                let cb = (mask[mi + 2] as f32 / 255.0) * sa;
636                if cr == 0.0 && cg == 0.0 && cb == 0.0 {
637                    continue;
638                }
639                let di = ((dy * fw_i + dx) * 4) as usize;
640                let dr = pixels[di] as f32 / 255.0;
641                let dg = pixels[di + 1] as f32 / 255.0;
642                let db = pixels[di + 2] as f32 / 255.0;
643                let rr = sr * cr + dr * (1.0 - cr);
644                let rg = sg * cg + dg * (1.0 - cg);
645                let rbb = sb * cb + db * (1.0 - cb);
646                pixels[di] = (rr * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
647                pixels[di + 1] = (rg * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
648                pixels[di + 2] = (rbb * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
649                // Alpha unchanged — we're writing onto an existing opaque
650                // (or semi-transparent) surface without introducing new
651                // transparency.
652            }
653        }
654    }
655
656    fn draw_image_rgba(
657        &mut self,
658        data: &[u8],
659        img_w: u32,
660        img_h: u32,
661        dst_x: f64,
662        dst_y: f64,
663        dst_w: f64,
664        dst_h: f64,
665    ) {
666        // Scale the source image into a temporary Framebuffer at dst size,
667        // then composite it onto the current render target using the CTM origin.
668        if img_w == 0 || img_h == 0 || dst_w < 1.0 || dst_h < 1.0 {
669            return;
670        }
671
672        let out_w = dst_w.round() as u32;
673        let out_h = dst_h.round() as u32;
674        let mut scaled = crate::framebuffer::Framebuffer::new(out_w, out_h);
675
676        // Nearest-neighbour scale — sufficient for README screenshots / badges.
677        // `data` is straight-alpha by the `draw_image_rgba` convention; AGG
678        // framebuffers store **premultiplied** RGBA, so we premultiply each
679        // sampled pixel on the way in so `composite_framebuffers` (which uses
680        // premultiplied SrcOver) blends with correct intensity.
681        let px = scaled.pixels_mut();
682        for dy in 0..out_h {
683            for dx in 0..out_w {
684                let sx = (dx as f64 / out_w as f64 * img_w as f64) as u32;
685                // Image is top-row-first; Y-up dst means we flip sy.
686                let sy_img = ((1.0 - (dy as f64 + 0.5) / out_h as f64) * img_h as f64)
687                    .floor()
688                    .clamp(0.0, (img_h - 1) as f64) as u32;
689                let si = ((sy_img * img_w + sx) * 4) as usize;
690                let di = ((dy * out_w + dx) * 4) as usize;
691                if si + 3 < data.len() && di + 3 < px.len() {
692                    let a = data[si + 3] as u32;
693                    if a == 255 {
694                        px[di] = data[si];
695                        px[di + 1] = data[si + 1];
696                        px[di + 2] = data[si + 2];
697                        px[di + 3] = 255;
698                    } else {
699                        // Premultiply: (c * a + 127) / 255 (round-half-up).
700                        px[di] = (((data[si] as u32) * a + 127) / 255) as u8;
701                        px[di + 1] = (((data[si + 1] as u32) * a + 127) / 255) as u8;
702                        px[di + 2] = (((data[si + 2] as u32) * a + 127) / 255) as u8;
703                        px[di + 3] = a as u8;
704                    }
705                }
706            }
707        }
708
709        // Apply CTM translation to get screen-space origin.
710        let (tx, ty) = {
711            let t = self.transform();
712            (t.tx, t.ty)
713        };
714        let screen_x = (tx + dst_x).round() as i32;
715        let screen_y = (ty + dst_y).round() as i32;
716        // Honor the active `global_alpha` so backbuffered widgets (Labels,
717        // buttons) inside a faded subtree fade uniformly with the rest of the
718        // group — without this the blit composited at full opacity regardless
719        // of `set_global_alpha`.
720        let ga = self.state.global_alpha;
721        // Honor the active clip (screen-space Y-up AABB) so image blits are
722        // scissored like every other primitive. Over-scan band backbuffers
723        // (see `Widget::backbuffer_band`) rely on this to crop their margins to
724        // the widget bounds; ordinary full-image blits set no tighter clip than
725        // their bounds, so this is a no-op for them.
726        let clip = self.state.clip;
727        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
728        composite_framebuffers(fb, &scaled, screen_x, screen_y, ga, clip);
729    }
730}
731
732/// Apply a Y-up scissor clip to a `RendererBase` (pixel-inclusive coordinates).
733pub(crate) fn apply_clip<PF: agg_rust::pixfmt_rgba::PixelFormat>(
734    rb: &mut RendererBase<PF>,
735    clip: Option<(f64, f64, f64, f64)>,
736) {
737    if let Some((x, y, w, h)) = clip {
738        let x1 = x.floor() as i32;
739        let y1 = y.floor() as i32;
740        let x2 = (x + w).ceil() as i32 - 1;
741        let y2 = (y + h).ceil() as i32 - 1;
742        rb.clip_box_i(x1, y1, x2, y2);
743    }
744}