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//! # Coordinate system
7//!
8//! All coordinates are **first-quadrant (Y-up)**. Origin is the bottom-left
9//! corner of the framebuffer. Positive X goes right, positive Y goes up.
10
11use std::f64::consts::PI;
12use std::sync::Arc;
13
14use agg_rust::arc::Arc as AggArc;
15use agg_rust::basics::FillingRule;
16use agg_rust::basics::VertexSource;
17use agg_rust::basics::PATH_FLAGS_NONE;
18use agg_rust::bounding_rect::bounding_rect_single;
19use agg_rust::comp_op::{CompOp, PixfmtRgba32CompOp};
20use agg_rust::conv_curve::ConvCurve;
21use agg_rust::conv_dash::ConvDash;
22use agg_rust::conv_stroke::ConvStroke;
23use agg_rust::conv_transform::ConvTransform;
24use agg_rust::gsv_text::GsvText;
25use agg_rust::math_stroke::{LineCap, LineJoin};
26use agg_rust::path_storage::PathStorage;
27use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
28use agg_rust::renderer_base::RendererBase;
29use agg_rust::renderer_scanline::render_scanlines_aa_solid;
30use agg_rust::rendering_buffer::RowAccessor;
31use agg_rust::rounded_rect::RoundedRect;
32use agg_rust::scanline_u::ScanlineU8;
33use agg_rust::trans_affine::TransAffine;
34
35use crate::color::Color;
36use crate::draw_ctx::{FillRule, LinearGradientPaint, PatternPaint, RadialGradientPaint};
37use crate::framebuffer::Framebuffer;
38use crate::text::{measure_advance, shape_text, Font, TextMetrics};
39
40// ---------------------------------------------------------------------------
41// Layer stack entry
42// ---------------------------------------------------------------------------
43
44/// One entry on the `GfxCtx` layer stack, created by `push_layer`.
45struct LayerEntry {
46    /// The offscreen framebuffer for this layer.
47    fb: Framebuffer,
48    /// GfxState snapshot at the moment `push_layer` was called.
49    /// Restored verbatim on `pop_layer`.
50    saved_state: GfxState,
51    /// State-stack snapshot at the moment `push_layer` was called.
52    saved_stack: Vec<GfxState>,
53    /// Screen-space X origin of this layer (= CTM tx at push time, Y-up).
54    origin_x: f64,
55    /// Screen-space Y origin of this layer (= CTM ty at push time, Y-up).
56    origin_y: f64,
57    /// Alpha multiplier applied when the layer is composited back.
58    alpha: f64,
59    /// When `Some`, this is a **clip layer** created by `clip_path`: an 8-bit
60    /// coverage mask the size of `fb` that is multiplied into the layer's
61    /// alpha just before compositing, producing an anti-aliased path clip.
62    /// Clip layers are popped implicitly by the `restore()` matching the
63    /// `save()` that preceded the `clip_path()` call.
64    clip_mask: Option<Vec<u8>>,
65    /// For a clip layer, the current path as it was when `clip_path()` ran; it
66    /// is put back when the layer is popped so the caller can stroke or re-fill
67    /// the outline it clipped with.
68    clip_saved_path: Option<PathStorage>,
69}
70
71// Re-export so callers don't need to import agg_rust directly.
72pub use agg_rust::comp_op::CompOp as BlendMode;
73
74/// Snapshot of drawing state, pushed/popped by `save()`/`restore()`.
75#[derive(Clone)]
76struct GfxState {
77    transform: TransAffine,
78    fill_color: Color,
79    fill_linear_gradient: Option<LinearGradientPaint>,
80    fill_radial_gradient: Option<RadialGradientPaint>,
81    fill_pattern: Option<PatternPaint>,
82    stroke_color: Color,
83    stroke_linear_gradient: Option<LinearGradientPaint>,
84    stroke_radial_gradient: Option<RadialGradientPaint>,
85    stroke_pattern: Option<PatternPaint>,
86    fill_rule: FillRule,
87    line_width: f64,
88    line_join: LineJoin,
89    line_cap: LineCap,
90    miter_limit: f64,
91    line_dash: Vec<f64>,
92    dash_offset: f64,
93    blend_mode: CompOp,
94    clip: Option<(f64, f64, f64, f64)>,
95    global_alpha: f64,
96    font: Option<Arc<Font>>,
97    font_size: f64,
98}
99
100impl Default for GfxState {
101    fn default() -> Self {
102        Self {
103            transform: TransAffine::new(),
104            fill_color: Color::black(),
105            fill_linear_gradient: None,
106            fill_radial_gradient: None,
107            fill_pattern: None,
108            stroke_color: Color::black(),
109            stroke_linear_gradient: None,
110            stroke_radial_gradient: None,
111            stroke_pattern: None,
112            fill_rule: FillRule::NonZero,
113            line_width: 1.0,
114            line_join: LineJoin::Round,
115            line_cap: LineCap::Round,
116            miter_limit: 4.0,
117            line_dash: Vec::new(),
118            dash_offset: 0.0,
119            blend_mode: CompOp::SrcOver,
120            clip: None,
121            global_alpha: 1.0,
122            font: None,
123            font_size: 16.0,
124        }
125    }
126}
127
128/// Cairo-style stateful 2D graphics context.
129///
130/// All widget painting goes through `GfxCtx`. Create one per frame from a
131/// [`Framebuffer`], draw into it, then let it drop — the framebuffer retains
132/// the rendered pixels.
133///
134/// # Layer compositing
135///
136/// Call `push_layer(w, h)` to redirect all subsequent drawing into an offscreen
137/// framebuffer.  Call `pop_layer()` to SrcOver-composite that buffer back into
138/// the previous target (which may itself be a layer or the base framebuffer).
139/// Layers nest; each `push` must be matched by exactly one `pop`.
140pub struct GfxCtx<'a> {
141    base_fb: &'a mut Framebuffer,
142    /// Offscreen layer stack.  Empty when rendering directly to `base_fb`.
143    layer_stack: Vec<LayerEntry>,
144    state: GfxState,
145    state_stack: Vec<GfxState>,
146    /// Accumulated path, reset by `begin_path()`.
147    path: PathStorage,
148    /// When true, `fill_text` routes through the 3× horizontal LCD
149    /// subpixel pipeline (see `lcd_coverage.rs`) and composites per-channel
150    /// onto the active framebuffer.  Controlled by the backbuffer mode —
151    /// set to true when this ctx is writing into an `LcdCoverage` widget
152    /// backbuffer, false for `Rgba`.  Main render loops set it at frame
153    /// start from `font_settings::lcd_enabled()`.
154    lcd_mode: bool,
155}
156
157impl<'a> GfxCtx<'a> {
158    /// Create a new graphics context for the given framebuffer.
159    pub fn new(fb: &'a mut Framebuffer) -> Self {
160        Self {
161            base_fb: fb,
162            layer_stack: Vec::new(),
163            state: GfxState::default(),
164            state_stack: Vec::new(),
165            path: PathStorage::new(),
166            lcd_mode: false,
167        }
168    }
169
170    // -------------------------------------------------------------------------
171    // State stack
172    // -------------------------------------------------------------------------
173
174    pub fn save(&mut self) {
175        self.state_stack.push(self.state.clone());
176    }
177
178    /// Pop the graphics state pushed by the matching `save()`.
179    ///
180    /// Any clip layers opened by `clip_path()` since that `save()` are
181    /// composited (and their masks applied) first — a clip layer's lifetime
182    /// ends at the `restore()` matching the `save()` that preceded it, which
183    /// is exactly canvas-2D `clip()` semantics.
184    pub fn restore(&mut self) {
185        // A clip layer took ownership of the state stack at push time, so an
186        // empty local stack means this restore would pop past the state that
187        // was live when `clip_path()` ran.
188        while self.state_stack.is_empty()
189            && self
190                .layer_stack
191                .last()
192                .is_some_and(|l| l.clip_mask.is_some())
193        {
194            self.pop_layer();
195        }
196        if let Some(state) = self.state_stack.pop() {
197            self.state = state;
198        }
199    }
200
201    // -------------------------------------------------------------------------
202    // Transform (Y-up, CCW-positive rotations)
203    // -------------------------------------------------------------------------
204
205    /// Append a translation. Uses pre-multiply (Cairo semantics).
206    pub fn translate(&mut self, tx: f64, ty: f64) {
207        self.state
208            .transform
209            .premultiply(&TransAffine::new_translation(tx, ty));
210    }
211
212    /// Append a CCW rotation in radians. Uses pre-multiply semantics.
213    pub fn rotate(&mut self, radians: f64) {
214        self.state
215            .transform
216            .premultiply(&TransAffine::new_rotation(radians));
217    }
218
219    /// Append a scale. Uses pre-multiply semantics.
220    pub fn scale(&mut self, sx: f64, sy: f64) {
221        self.state
222            .transform
223            .premultiply(&TransAffine::new_scaling(sx, sy));
224    }
225
226    pub fn set_transform(&mut self, m: TransAffine) {
227        self.state.transform = m;
228    }
229    pub fn reset_transform(&mut self) {
230        self.state.transform = TransAffine::new();
231    }
232    /// Return the current accumulated transform (cumulative translation + scale
233    /// from all parent `save/translate/restore` calls). The `tx`/`ty` fields
234    /// give the widget's bottom-left corner in framebuffer (Y-up) coordinates.
235    pub fn transform(&self) -> TransAffine {
236        self.state.transform
237    }
238
239    // -------------------------------------------------------------------------
240    // Style
241    // -------------------------------------------------------------------------
242
243    pub fn set_fill_color(&mut self, color: Color) {
244        self.state.fill_color = color;
245        self.state.fill_linear_gradient = None;
246        self.state.fill_radial_gradient = None;
247        self.state.fill_pattern = None;
248    }
249    pub fn set_fill_linear_gradient(&mut self, gradient: LinearGradientPaint) {
250        self.state.fill_linear_gradient = Some(gradient);
251        self.state.fill_radial_gradient = None;
252        self.state.fill_pattern = None;
253    }
254    pub fn set_fill_radial_gradient(&mut self, gradient: RadialGradientPaint) {
255        self.state.fill_linear_gradient = None;
256        self.state.fill_radial_gradient = Some(gradient);
257        self.state.fill_pattern = None;
258    }
259    pub fn set_fill_pattern(&mut self, pattern: PatternPaint) {
260        self.state.fill_linear_gradient = None;
261        self.state.fill_radial_gradient = None;
262        self.state.fill_pattern = Some(pattern);
263    }
264    pub fn set_stroke_color(&mut self, color: Color) {
265        self.state.stroke_color = color;
266        self.state.stroke_linear_gradient = None;
267        self.state.stroke_radial_gradient = None;
268        self.state.stroke_pattern = None;
269    }
270    pub fn set_stroke_linear_gradient(&mut self, gradient: LinearGradientPaint) {
271        self.state.stroke_linear_gradient = Some(gradient);
272        self.state.stroke_radial_gradient = None;
273        self.state.stroke_pattern = None;
274    }
275    pub fn set_stroke_radial_gradient(&mut self, gradient: RadialGradientPaint) {
276        self.state.stroke_linear_gradient = None;
277        self.state.stroke_radial_gradient = Some(gradient);
278        self.state.stroke_pattern = None;
279    }
280    pub fn set_stroke_pattern(&mut self, pattern: PatternPaint) {
281        self.state.stroke_linear_gradient = None;
282        self.state.stroke_radial_gradient = None;
283        self.state.stroke_pattern = Some(pattern);
284    }
285    pub fn set_line_width(&mut self, w: f64) {
286        self.state.line_width = w;
287    }
288    pub fn set_line_join(&mut self, join: LineJoin) {
289        self.state.line_join = join;
290    }
291    pub fn set_line_cap(&mut self, cap: LineCap) {
292        self.state.line_cap = cap;
293    }
294    pub fn set_miter_limit(&mut self, limit: f64) {
295        self.state.miter_limit = limit.max(1.0);
296    }
297    pub fn set_line_dash(&mut self, dashes: &[f64], offset: f64) {
298        self.state.line_dash.clear();
299        self.state
300            .line_dash
301            .extend(dashes.iter().copied().filter(|v| *v > 0.0));
302        self.state.dash_offset = offset;
303    }
304    pub fn set_fill_rule(&mut self, rule: FillRule) {
305        self.state.fill_rule = rule;
306    }
307
308    /// Set the Porter-Duff compositing mode. Default: `SrcOver`.
309    pub fn set_blend_mode(&mut self, mode: CompOp) {
310        self.state.blend_mode = mode;
311    }
312
313    /// Global alpha multiplier (0.0–1.0) applied on top of each color's alpha.
314    pub fn set_global_alpha(&mut self, alpha: f64) {
315        self.state.global_alpha = alpha.clamp(0.0, 1.0);
316    }
317
318    // -------------------------------------------------------------------------
319    // Font
320    // -------------------------------------------------------------------------
321
322    /// Set the current font. Shared via `Arc` — cheap to clone across widgets.
323    pub fn set_font(&mut self, font: Arc<Font>) {
324        self.state.font = Some(font);
325    }
326
327    /// Set the font size in pixels (distance from baseline to cap height).
328    pub fn set_font_size(&mut self, size: f64) {
329        self.state.font_size = size.max(1.0);
330    }
331
332    /// Enable/disable LCD subpixel rendering on this ctx.  When true,
333    /// `fill_text` uses the per-channel coverage pipeline; when false
334    /// grayscale AA.  Set by `paint_subtree_backbuffered` for
335    /// `LcdCoverage` widget buffers, and by the main render loop for
336    /// direct-to-screen text.
337    pub fn set_lcd_mode(&mut self, on: bool) {
338        self.lcd_mode = on;
339    }
340
341    /// Read the ctx's current LCD mode.
342    pub fn lcd_mode(&self) -> bool {
343        self.lcd_mode
344    }
345
346    // -------------------------------------------------------------------------
347    // Clipping
348    // -------------------------------------------------------------------------
349
350    /// Intersect the current clip with a rectangle in the **current local
351    /// coordinate space** (i.e. after all accumulated `translate` / `scale`
352    /// calls).  The four corners are mapped through the current transform to
353    /// produce an axis-aligned screen-space bounding box, which is then
354    /// intersected with any existing clip.
355    ///
356    /// For the common case of pure translations this is equivalent to the old
357    /// "screen-space rectangle" API, but it now works correctly when called
358    /// from inside a `paint()` method that runs after the framework has already
359    /// translated the context to the widget's origin.
360    pub fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
361        // Map all four corners through the CTM and take the AABB.
362        let t = &self.state.transform;
363        let corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)];
364        let mut sx_min = f64::INFINITY;
365        let mut sy_min = f64::INFINITY;
366        let mut sx_max = f64::NEG_INFINITY;
367        let mut sy_max = f64::NEG_INFINITY;
368        for (lx, ly) in corners {
369            let mut sx = lx;
370            let mut sy = ly;
371            t.transform(&mut sx, &mut sy);
372            if sx < sx_min {
373                sx_min = sx;
374            }
375            if sx > sx_max {
376                sx_max = sx;
377            }
378            if sy < sy_min {
379                sy_min = sy;
380            }
381            if sy > sy_max {
382                sy_max = sy;
383            }
384        }
385        let sw = (sx_max - sx_min).max(0.0);
386        let sh = (sy_max - sy_min).max(0.0);
387        if let Some((cx, cy, cw, ch)) = self.state.clip {
388            let x1 = sx_min.max(cx);
389            let y1 = sy_min.max(cy);
390            let x2 = sx_max.min(cx + cw);
391            let y2 = sy_max.min(cy + ch);
392            self.state.clip = Some((x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0)));
393        } else {
394            self.state.clip = Some((sx_min, sy_min, sw, sh));
395        }
396    }
397
398    pub fn reset_clip(&mut self) {
399        self.state.clip = None;
400    }
401
402    // -------------------------------------------------------------------------
403    // Clear
404    // -------------------------------------------------------------------------
405
406    /// Fill the entire active framebuffer with `color`, ignoring transform and clip.
407    pub fn clear(&mut self, color: Color) {
408        let rgba = color.to_rgba8();
409        for chunk in active_fb(&mut self.base_fb, &mut self.layer_stack)
410            .pixels_mut()
411            .chunks_exact_mut(4)
412        {
413            chunk[0] = rgba.r as u8;
414            chunk[1] = rgba.g as u8;
415            chunk[2] = rgba.b as u8;
416            chunk[3] = rgba.a as u8;
417        }
418    }
419
420    // -------------------------------------------------------------------------
421    // Path construction
422    // -------------------------------------------------------------------------
423
424    pub fn begin_path(&mut self) {
425        self.path = PathStorage::new();
426    }
427
428    pub fn move_to(&mut self, x: f64, y: f64) {
429        self.path.move_to(x, y);
430    }
431    pub fn line_to(&mut self, x: f64, y: f64) {
432        self.path.line_to(x, y);
433    }
434
435    pub fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64) {
436        self.path.curve4(cx1, cy1, cx2, cy2, x, y);
437    }
438
439    pub fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64) {
440        self.path.curve3(cx, cy, x, y);
441    }
442
443    pub fn arc_to(
444        &mut self,
445        cx: f64,
446        cy: f64,
447        r: f64,
448        start_angle: f64,
449        end_angle: f64,
450        ccw: bool,
451    ) {
452        let mut arc = AggArc::new(cx, cy, r, r, start_angle, end_angle, ccw);
453        self.path.concat_path(&mut arc, 0);
454    }
455
456    /// Full circle at `(cx, cy)` with radius `r`.
457    pub fn circle(&mut self, cx: f64, cy: f64, r: f64) {
458        self.arc_to(cx, cy, r, 0.0, 2.0 * PI, true);
459        self.path.close_polygon(PATH_FLAGS_NONE);
460    }
461
462    /// Axis-aligned rectangle — bottom-left `(x, y)`, size `w × h`.
463    pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
464        self.path.move_to(x, y);
465        self.path.line_to(x + w, y);
466        self.path.line_to(x + w, y + h);
467        self.path.line_to(x, y + h);
468        self.path.close_polygon(PATH_FLAGS_NONE);
469    }
470
471    /// Rounded rectangle — bottom-left `(x, y)`, size `w × h`, corner radius `r`.
472    pub fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
473        let r = r.min(w * 0.5).min(h * 0.5).max(0.0);
474        let mut rr = RoundedRect::new(x, y, x + w, y + h, r);
475        rr.normalize_radius();
476        self.path.concat_path(&mut rr, 0);
477    }
478
479    pub fn close_path(&mut self) {
480        self.path.close_polygon(PATH_FLAGS_NONE);
481    }
482
483    // -------------------------------------------------------------------------
484    // Drawing
485    // -------------------------------------------------------------------------
486
487    /// Fill the accumulated path.
488    pub fn fill(&mut self) {
489        let mode = self.state.blend_mode;
490        let clip = self.state.clip;
491        let fill_rule = self.state.fill_rule;
492        let transform = self.state.transform.clone();
493        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
494        if let Some(gradient) = self.state.fill_linear_gradient.clone() {
495            sampled::rasterize_linear_gradient_fill(
496                fb,
497                &mut self.path,
498                &gradient,
499                self.state.global_alpha as f32,
500                mode,
501                clip,
502                fill_rule,
503                &transform,
504            );
505        } else if let Some(gradient) = self.state.fill_radial_gradient.clone() {
506            sampled::rasterize_radial_gradient_fill(
507                fb,
508                &mut self.path,
509                &gradient,
510                self.state.global_alpha as f32,
511                mode,
512                clip,
513                fill_rule,
514                &transform,
515            );
516        } else if let Some(pattern) = self.state.fill_pattern.clone() {
517            sampled::rasterize_pattern_fill(
518                fb,
519                &mut self.path,
520                &pattern,
521                self.state.global_alpha as f32,
522                mode,
523                clip,
524                fill_rule,
525                &transform,
526            );
527        } else {
528            let mut color = self.state.fill_color;
529            color.a *= self.state.global_alpha as f32;
530            let rgba = color.to_rgba8();
531            rasterize_fill(fb, &mut self.path, &rgba, mode, clip, fill_rule, &transform);
532        }
533    }
534
535    /// Stroke the accumulated path.
536    pub fn stroke(&mut self) {
537        let mut color = self.state.stroke_color;
538        color.a *= self.state.global_alpha as f32;
539        let rgba = color.to_rgba8();
540        let width = self.state.line_width;
541        let join = self.state.line_join;
542        let cap = self.state.line_cap;
543        let miter_limit = self.state.miter_limit;
544        let dashes = self.state.line_dash.clone();
545        let dash_offset = self.state.dash_offset;
546        let mode = self.state.blend_mode;
547        let clip = self.state.clip;
548        let transform = self.state.transform.clone();
549        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
550        if let Some(gradient) = self.state.stroke_linear_gradient.clone() {
551            let mut outline = stroke::materialize_stroke_outline(
552                &mut self.path,
553                width,
554                join,
555                cap,
556                miter_limit,
557                &dashes,
558                dash_offset,
559            );
560            sampled::rasterize_linear_gradient_fill(
561                fb,
562                &mut outline,
563                &gradient,
564                self.state.global_alpha as f32,
565                mode,
566                clip,
567                FillRule::NonZero,
568                &transform,
569            );
570        } else if let Some(gradient) = self.state.stroke_radial_gradient.clone() {
571            let mut outline = stroke::materialize_stroke_outline(
572                &mut self.path,
573                width,
574                join,
575                cap,
576                miter_limit,
577                &dashes,
578                dash_offset,
579            );
580            sampled::rasterize_radial_gradient_fill(
581                fb,
582                &mut outline,
583                &gradient,
584                self.state.global_alpha as f32,
585                mode,
586                clip,
587                FillRule::NonZero,
588                &transform,
589            );
590        } else if let Some(pattern) = self.state.stroke_pattern.clone() {
591            let mut outline = stroke::materialize_stroke_outline(
592                &mut self.path,
593                width,
594                join,
595                cap,
596                miter_limit,
597                &dashes,
598                dash_offset,
599            );
600            sampled::rasterize_pattern_fill(
601                fb,
602                &mut outline,
603                &pattern,
604                self.state.global_alpha as f32,
605                mode,
606                clip,
607                FillRule::NonZero,
608                &transform,
609            );
610        } else {
611            rasterize_stroke(
612                fb,
613                &mut self.path,
614                &rgba,
615                width,
616                join,
617                cap,
618                miter_limit,
619                &dashes,
620                dash_offset,
621                mode,
622                clip,
623                &transform,
624            );
625        }
626    }
627
628    /// Fill then stroke the accumulated path in one call.
629    pub fn fill_and_stroke(&mut self) {
630        self.fill();
631        self.stroke();
632    }
633
634    // -------------------------------------------------------------------------
635    // Text
636    // -------------------------------------------------------------------------
637
638    /// Draw `text` at position `(x, y)` using the current font and fill color.
639    ///
640    /// `(x, y)` is the **baseline-left** position in Y-up screen coordinates.
641    /// Glyphs extend upward (higher Y) for ascenders and downward (lower Y)
642    /// for descenders — correct for Y-up rendering with no Y-flip.
643    ///
644    /// Requires a font to be set via [`set_font`](Self::set_font). Does nothing
645    /// if no font has been set.
646    pub fn fill_text(&mut self, text: &str, x: f64, y: f64) {
647        let font = match self.state.font.clone() {
648            Some(f) => f,
649            None => return,
650        };
651        let font_size = self.state.font_size;
652
653        let mut color = self.state.fill_color;
654        color.a *= self.state.global_alpha as f32;
655
656        // LCD subpixel path — gated on this ctx's `lcd_mode` flag,
657        // which is set by `paint_subtree_backbuffered` when the widget
658        // chose `BackbufferMode::LcdCoverage` and by the main render
659        // loop for direct-to-screen text when the global font setting
660        // says so.  Mask raster is cached keyed on `(text, font, size)`
661        // and colour is applied at composite time.
662        //
663        // HiDPI: rasterise the mask at the **physical** font size (logical
664        // × CTM scale) so the 1:1 texel-to-pixel composite fills the
665        // expected number of physical pixels.  Without this the mask
666        // renders at logical size and ends up half-size (or stretched by a
667        // separate scale call) on 2×/3× displays.
668        //
669        // **Y-axis baseline alignment**: when the global hinting toggle
670        // is ON, both renderers place the baseline on the same integer
671        // physical pixel row — see the in-mask `by` snap inside
672        // `rasterize_text_lcd_cached` paired with `shape_text`'s own
673        // hint-driven `gy` snap.  When hinting is OFF, the RGBA path
674        // produces baseline at the exact fractional `y`, while the LCD
675        // path's intrinsic composite-rounding (`sy.round()` in
676        // `draw_lcd_mask`, required for X-subpixel coherence) lands the
677        // baseline at the nearest integer plus the fractional descender
678        // — a subpixel residual that's impossible to remove without
679        // breaking LCD chroma.  This is a deliberate trade-off matching
680        // the user's "snap should be a checkbox, not always on".
681        if self.lcd_mode {
682            let t = &self.state.transform;
683            let ctm_scale = (t.sx * t.sx + t.shy * t.shy).sqrt().max(1e-6);
684            let phys_size = font_size * ctm_scale;
685            let cached = crate::lcd_coverage::rasterize_text_lcd_cached(&font, text, phys_size);
686            // `baseline_*_in_mask` is in physical mask pixels; divide by
687            // `ctm_scale` so the offset stays in logical units that the
688            // CTM then multiplies back to physical at blit time.
689            let dst_x = x - cached.baseline_x_in_mask / ctm_scale;
690            let dst_y = y - cached.baseline_y_in_mask / ctm_scale;
691            <Self as crate::DrawCtx>::draw_lcd_mask_arc(
692                self,
693                &cached.pixels,
694                cached.width,
695                cached.height,
696                color,
697                dst_x,
698                dst_y,
699            );
700            return;
701        }
702
703        let rgba = color.to_rgba8();
704        let mode = self.state.blend_mode;
705        let clip = self.state.clip;
706        let transform = self.state.transform.clone();
707
708        // Shape text and collect per-glyph outline paths.
709        let (glyph_paths, _) = shape_text(&font, text, font_size, x, y);
710        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
711        for mut path in glyph_paths {
712            rasterize_fill(
713                fb,
714                &mut path,
715                &rgba,
716                mode,
717                clip,
718                FillRule::NonZero,
719                &transform,
720            );
721        }
722    }
723
724    /// Measure the advance width and metrics of `text` in the current font.
725    ///
726    /// Returns `None` if no font has been set.
727    pub fn measure_text(&self, text: &str) -> Option<TextMetrics> {
728        let font = self.state.font.as_ref()?;
729        let size = self.state.font_size;
730        Some(TextMetrics {
731            width: measure_advance(font, text, size),
732            ascent: font.ascender_px(size),
733            descent: font.descender_px(size),
734            line_height: font.line_height_px(size),
735        })
736    }
737
738    // -------------------------------------------------------------------------
739    // Convenience: built-in stroked vector font (no font file required)
740    // -------------------------------------------------------------------------
741
742    /// Draw text using AGG's built-in vector font (no external font needed).
743    ///
744    /// Useful for labels before a full font is loaded.
745    pub fn fill_text_gsv(&mut self, text: &str, x: f64, y: f64, size: f64) {
746        let mut color = self.state.fill_color;
747        color.a *= self.state.global_alpha as f32;
748        let rgba = color.to_rgba8();
749        let mode = self.state.blend_mode;
750        let clip = self.state.clip;
751        let transform = self.state.transform.clone();
752
753        let fb = active_fb(&mut self.base_fb, &mut self.layer_stack);
754        let w = fb.width();
755        let h = fb.height();
756        let stride = (w * 4) as i32;
757        let mut ra = RowAccessor::new();
758        unsafe { ra.attach(fb.pixels_mut().as_mut_ptr(), w, h, stride) };
759        let pf = PixfmtRgba32CompOp::new_with_op(&mut ra, mode);
760        let mut rb = RendererBase::new(pf);
761        apply_clip(&mut rb, clip);
762
763        let mut ras = RasterizerScanlineAa::new();
764        let mut sl = ScanlineU8::new();
765
766        let mut gsv = GsvText::new();
767        gsv.size(size, 0.0);
768        gsv.start_point(x, y);
769        gsv.text(text);
770
771        let mut stroke = ConvStroke::new(&mut gsv);
772        stroke.set_width(size * 0.1);
773        let mut transformed = ConvTransform::new(&mut stroke, transform);
774        ras.add_path(&mut transformed, 0);
775        render_scanlines_aa_solid(&mut ras, &mut sl, &mut rb, &rgba);
776    }
777}
778
779mod draw_impl;
780mod layers;
781mod sampled;
782mod stroke;
783
784use draw_impl::{active_fb, composite_framebuffers};
785pub(crate) use draw_impl::{apply_clip, rasterize_fill, rasterize_stroke};