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