Skip to main content

agg_gui/
lcd_gfx_ctx.rs

1//! `LcdGfxCtx` — a [`DrawCtx`] implementation whose render target is an
2//! [`LcdBuffer`] (3 bytes/pixel coverage store) instead of a regular
3//! RGBA [`Framebuffer`].  All paint primitives flow through the LCD
4//! pipeline (3× horizontal supersample → 5-tap filter → per-channel
5//! src-over) so the buffer accumulates the same per-channel coverage
6//! representation regardless of whether each call originated from a
7//! text raster, a path fill, or a future image blit.
8//!
9//! # Where this fits in the architecture
10//!
11//! When a widget opts into [`crate::widget::BackbufferMode::LcdCoverage`],
12//! `paint_subtree_backbuffered` allocates an `LcdBuffer` and hands its
13//! children an `LcdGfxCtx` (rather than a `GfxCtx` over an RGBA
14//! `Framebuffer`).  Children paint normally — the same `DrawCtx`
15//! methods, the same widget code — but every primitive flows through
16//! the LCD pipeline.  When all children have painted, the host either:
17//!
18//!   - composites the buffer onto the destination RGBA framebuffer via
19//!     [`crate::lcd_coverage::composite_lcd_mask`] (software path), or
20//!   - uploads the buffer as an RGB texture and runs the dual-source-blend
21//!     shader (GL path).
22//!
23//! Either way, the buffer is the single intermediate that decouples
24//! "what was painted" from "how it lands on the destination."
25//!
26//! # Status
27//!
28//! Step 2 of the LCD-architecture migration.  The MVP implements the
29//! primitives needed to drive an end-to-end equivalence test against
30//! the legacy `GfxCtx + lcd_mode=true` path: state setters, transform
31//! stack, axis-aligned `rect`/`fill`, `fill_text`, `clear`, and
32//! `draw_lcd_mask`.  Curve / stroke / image-blit / clip primitives are
33//! marked `// TODO step 2c` and will land before any widget actually
34//! paints into an `LcdGfxCtx`.
35
36use std::f64::consts::PI;
37use std::sync::Arc;
38
39use agg_rust::arc::Arc as AggArc;
40use agg_rust::basics::{VertexSource, PATH_FLAGS_NONE};
41use agg_rust::comp_op::CompOp;
42use agg_rust::conv_curve::ConvCurve;
43use agg_rust::conv_dash::ConvDash;
44use agg_rust::conv_stroke::ConvStroke;
45use agg_rust::gsv_text::GsvText;
46use agg_rust::math_stroke::{LineCap, LineJoin};
47use agg_rust::path_storage::PathStorage;
48use agg_rust::rounded_rect::RoundedRect;
49use agg_rust::trans_affine::TransAffine;
50
51use crate::color::Color;
52use crate::draw_ctx::{DrawCtx, FillRule, LinearGradientPaint, PatternPaint, RadialGradientPaint};
53use crate::lcd_coverage::{rasterize_text_lcd_cached, LcdBuffer, LcdMask};
54use crate::text::{measure_text_metrics, Font, TextMetrics};
55
56mod gradient;
57mod image;
58mod stroke;
59
60// ── State ──────────────────────────────────────────────────────────────────
61//
62// Mirror of `GfxCtx`'s private `GfxState` so widgets that already set up
63// fill colour / font / transform on a `GfxCtx` see the same field shape
64// here — and so Step 2c can copy over any logic from the existing fill
65// paths without translation.
66
67#[derive(Clone)]
68struct LcdState {
69    transform: TransAffine,
70    fill_color: Color,
71    fill_linear_gradient: Option<LinearGradientPaint>,
72    fill_radial_gradient: Option<RadialGradientPaint>,
73    fill_pattern: Option<PatternPaint>,
74    stroke_color: Color,
75    stroke_linear_gradient: Option<LinearGradientPaint>,
76    stroke_radial_gradient: Option<RadialGradientPaint>,
77    stroke_pattern: Option<PatternPaint>,
78    fill_rule: FillRule,
79    line_width: f64,
80    line_join: LineJoin,
81    line_cap: LineCap,
82    miter_limit: f64,
83    line_dash: Vec<f64>,
84    dash_offset: f64,
85    blend_mode: CompOp,
86    global_alpha: f64,
87    font: Option<Arc<Font>>,
88    font_size: f64,
89    /// Scissor clip in Y-up screen space `(x, y, w, h)`.  Stored but not
90    /// yet enforced — `LcdMaskBuilder` doesn't accept a clip param yet.
91    /// Step 2c.
92    clip: Option<(f64, f64, f64, f64)>,
93}
94
95impl Default for LcdState {
96    fn default() -> Self {
97        Self {
98            transform: TransAffine::new(),
99            fill_color: Color::black(),
100            fill_linear_gradient: None,
101            fill_radial_gradient: None,
102            fill_pattern: None,
103            stroke_color: Color::black(),
104            stroke_linear_gradient: None,
105            stroke_radial_gradient: None,
106            stroke_pattern: None,
107            fill_rule: FillRule::NonZero,
108            line_width: 1.0,
109            line_join: LineJoin::Round,
110            line_cap: LineCap::Round,
111            miter_limit: 4.0,
112            line_dash: Vec::new(),
113            dash_offset: 0.0,
114            blend_mode: CompOp::SrcOver,
115            global_alpha: 1.0,
116            font: None,
117            font_size: 16.0,
118            clip: None,
119        }
120    }
121}
122
123// ── LcdLayer ───────────────────────────────────────────────────────────────
124//
125// One entry on the `LcdGfxCtx` layer stack, created by `push_layer`.
126// Owns its own `LcdBuffer`; `pop_layer` flushes it back into the
127// previously-active buffer at the recorded origin.
128//
129// Mirrors the role of `gfx_ctx::LayerEntry` — the field shape is kept
130// close so widget code that uses `push_layer` / `pop_layer` has the
131// same mental model on either ctx type.  The compositing semantics
132// differ (see `LcdBuffer::composite_buffer`): RGBA layers do
133// alpha-aware SrcOver, LCD layers do full-replace, because the
134// coverage buffer has no alpha to distinguish "untouched" from
135// "intentionally black".
136
137struct LcdLayer {
138    buffer: LcdBuffer,
139    /// State snapshot at the moment `push_layer` was called.  Restored
140    /// verbatim on `pop_layer` so transform / clip / colour all return
141    /// to their pre-layer values.
142    saved_state: LcdState,
143    saved_stack: Vec<LcdState>,
144    /// Where the layer's bottom-left lands in the parent buffer's
145    /// coords.  Captured from the CTM's translation at push time.
146    origin_x: f64,
147    origin_y: f64,
148}
149
150// ── LcdGfxCtx ──────────────────────────────────────────────────────────────
151
152/// Cairo-style stateful 2D graphics context whose render target is an
153/// [`LcdBuffer`].  Borrows the buffer mutably for the lifetime of the
154/// ctx; let the ctx drop and the buffer is free to be uploaded /
155/// composited / read.
156pub struct LcdGfxCtx<'a> {
157    base_buffer: &'a mut LcdBuffer,
158    /// Offscreen layer stack.  Empty when rendering directly to
159    /// `base_buffer`.  Each `push_layer` pushes a new owned
160    /// `LcdBuffer`; subsequent paint primitives target the topmost
161    /// layer until the matching `pop_layer` flushes it back.
162    layer_stack: Vec<LcdLayer>,
163    state: LcdState,
164    state_stack: Vec<LcdState>,
165    /// Accumulated path, reset by `begin_path`.  Same role as in
166    /// `GfxCtx` — the `fill` / `stroke` calls consume it.
167    path: PathStorage,
168}
169
170impl<'a> LcdGfxCtx<'a> {
171    pub fn new(buffer: &'a mut LcdBuffer) -> Self {
172        Self {
173            base_buffer: buffer,
174            layer_stack: Vec::new(),
175            state: LcdState::default(),
176            state_stack: Vec::new(),
177            path: PathStorage::new(),
178        }
179    }
180
181    /// Read-only view of the underlying buffer — for callers that need
182    /// to inspect output without releasing the ctx.  Returns the base
183    /// buffer; callers inspecting mid-paint while a layer is active
184    /// see only state committed before the current layer's push.
185    pub fn buffer(&self) -> &LcdBuffer {
186        self.base_buffer
187    }
188
189    /// Active paint target: the topmost layer's buffer if any, else
190    /// the base buffer.  Every paint primitive routes through this so
191    /// `push_layer`/`pop_layer` redirects automatically.
192    fn active_buffer(&mut self) -> &mut LcdBuffer {
193        if let Some(layer) = self.layer_stack.last_mut() {
194            &mut layer.buffer
195        } else {
196            &mut *self.base_buffer
197        }
198    }
199}
200
201// ── DrawCtx impl ───────────────────────────────────────────────────────────
202
203impl<'a> DrawCtx for LcdGfxCtx<'a> {
204    // ── State ─────────────────────────────────────────────────────────────
205    fn set_fill_color(&mut self, color: Color) {
206        self.state.fill_color = color;
207        self.state.fill_linear_gradient = None;
208        self.state.fill_radial_gradient = None;
209        self.state.fill_pattern = None;
210    }
211    fn set_fill_linear_gradient(&mut self, gradient: LinearGradientPaint) {
212        self.state.fill_linear_gradient = Some(gradient);
213        self.state.fill_radial_gradient = None;
214        self.state.fill_pattern = None;
215    }
216    fn supports_fill_linear_gradient(&self) -> bool {
217        true
218    }
219    fn set_fill_radial_gradient(&mut self, gradient: RadialGradientPaint) {
220        self.state.fill_linear_gradient = None;
221        self.state.fill_radial_gradient = Some(gradient);
222        self.state.fill_pattern = None;
223    }
224    fn supports_fill_radial_gradient(&self) -> bool {
225        true
226    }
227    fn set_fill_pattern(&mut self, pattern: PatternPaint) {
228        self.state.fill_linear_gradient = None;
229        self.state.fill_radial_gradient = None;
230        self.state.fill_pattern = Some(pattern);
231    }
232    fn supports_fill_pattern(&self) -> bool {
233        true
234    }
235    fn set_stroke_color(&mut self, color: Color) {
236        self.state.stroke_color = color;
237        self.state.stroke_linear_gradient = None;
238        self.state.stroke_radial_gradient = None;
239        self.state.stroke_pattern = None;
240    }
241    fn set_stroke_linear_gradient(&mut self, gradient: LinearGradientPaint) {
242        self.state.stroke_linear_gradient = Some(gradient);
243        self.state.stroke_radial_gradient = None;
244        self.state.stroke_pattern = None;
245    }
246    fn supports_stroke_linear_gradient(&self) -> bool {
247        true
248    }
249    fn set_stroke_radial_gradient(&mut self, gradient: RadialGradientPaint) {
250        self.state.stroke_linear_gradient = None;
251        self.state.stroke_radial_gradient = Some(gradient);
252        self.state.stroke_pattern = None;
253    }
254    fn supports_stroke_radial_gradient(&self) -> bool {
255        true
256    }
257    fn set_stroke_pattern(&mut self, pattern: PatternPaint) {
258        self.state.stroke_linear_gradient = None;
259        self.state.stroke_radial_gradient = None;
260        self.state.stroke_pattern = Some(pattern);
261    }
262    fn supports_stroke_pattern(&self) -> bool {
263        true
264    }
265    fn set_line_width(&mut self, w: f64) {
266        self.state.line_width = w;
267    }
268    fn set_line_join(&mut self, j: LineJoin) {
269        self.state.line_join = j;
270    }
271    fn set_line_cap(&mut self, c: LineCap) {
272        self.state.line_cap = c;
273    }
274    fn set_miter_limit(&mut self, limit: f64) {
275        self.state.miter_limit = limit.max(1.0);
276    }
277    fn set_line_dash(&mut self, dashes: &[f64], offset: f64) {
278        self.state.line_dash.clear();
279        self.state
280            .line_dash
281            .extend(dashes.iter().copied().filter(|v| *v > 0.0));
282        self.state.dash_offset = offset;
283    }
284    fn set_blend_mode(&mut self, m: CompOp) {
285        self.state.blend_mode = m;
286    }
287    fn set_global_alpha(&mut self, a: f64) {
288        self.state.global_alpha = a.clamp(0.0, 1.0);
289    }
290    fn set_fill_rule(&mut self, r: FillRule) {
291        self.state.fill_rule = r;
292    }
293
294    // ── Font ──────────────────────────────────────────────────────────────
295    fn set_font(&mut self, f: Arc<Font>) {
296        self.state.font = Some(f);
297    }
298    fn set_font_size(&mut self, s: f64) {
299        self.state.font_size = s.max(1.0);
300    }
301
302    // ── Clipping ──────────────────────────────────────────────────────────
303    fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
304        // TODO step 2c — currently stored but not enforced; LcdMaskBuilder
305        // needs a clip-aware variant before we can honour it during fill.
306        let t = &self.state.transform;
307        let corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)];
308        let mut sx_min = f64::INFINITY;
309        let mut sy_min = f64::INFINITY;
310        let mut sx_max = f64::NEG_INFINITY;
311        let mut sy_max = f64::NEG_INFINITY;
312        for (lx, ly) in corners {
313            let mut sx = lx;
314            let mut sy = ly;
315            t.transform(&mut sx, &mut sy);
316            if sx < sx_min {
317                sx_min = sx;
318            }
319            if sx > sx_max {
320                sx_max = sx;
321            }
322            if sy < sy_min {
323                sy_min = sy;
324            }
325            if sy > sy_max {
326                sy_max = sy;
327            }
328        }
329        let new_clip = (
330            sx_min,
331            sy_min,
332            (sx_max - sx_min).max(0.0),
333            (sy_max - sy_min).max(0.0),
334        );
335        self.state.clip = Some(match self.state.clip {
336            Some((cx, cy, cw, ch)) => {
337                let x1 = sx_min.max(cx);
338                let y1 = sy_min.max(cy);
339                let x2 = (new_clip.0 + new_clip.2).min(cx + cw);
340                let y2 = (new_clip.1 + new_clip.3).min(cy + ch);
341                (x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0))
342            }
343            None => new_clip,
344        });
345    }
346    fn reset_clip(&mut self) {
347        self.state.clip = None;
348    }
349
350    // ── Clear ─────────────────────────────────────────────────────────────
351    fn clear(&mut self, color: Color) {
352        self.active_buffer().clear(color);
353    }
354
355    // ── Path building ─────────────────────────────────────────────────────
356    fn begin_path(&mut self) {
357        self.path = PathStorage::new();
358    }
359    fn move_to(&mut self, x: f64, y: f64) {
360        self.path.move_to(x, y);
361    }
362    fn line_to(&mut self, x: f64, y: f64) {
363        self.path.line_to(x, y);
364    }
365    fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64) {
366        self.path.curve4(cx1, cy1, cx2, cy2, x, y);
367    }
368    fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64) {
369        self.path.curve3(cx, cy, x, y);
370    }
371    fn arc_to(&mut self, cx: f64, cy: f64, r: f64, start_angle: f64, end_angle: f64, ccw: bool) {
372        let mut arc = AggArc::new(cx, cy, r, r, start_angle, end_angle, ccw);
373        self.path.concat_path(&mut arc, 0);
374    }
375    fn circle(&mut self, cx: f64, cy: f64, r: f64) {
376        self.arc_to(cx, cy, r, 0.0, 2.0 * PI, true);
377        self.path.close_polygon(PATH_FLAGS_NONE);
378    }
379    fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
380        self.path.move_to(x, y);
381        self.path.line_to(x + w, y);
382        self.path.line_to(x + w, y + h);
383        self.path.line_to(x, y + h);
384        self.path.close_polygon(PATH_FLAGS_NONE);
385    }
386    fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
387        let r = r.min(w * 0.5).min(h * 0.5).max(0.0);
388        let mut rr = RoundedRect::new(x, y, x + w, y + h, r);
389        rr.normalize_radius();
390        self.path.concat_path(&mut rr, 0);
391    }
392    fn close_path(&mut self) {
393        self.path.close_polygon(PATH_FLAGS_NONE);
394    }
395
396    // ── Path drawing ──────────────────────────────────────────────────────
397    fn fill(&mut self) {
398        let xform = self.state.transform;
399        let clip = self.state.clip;
400        let rule = self.state.fill_rule;
401        // Borrow gymnastics: `fill_path` needs `&mut path` AND `&mut buffer`,
402        // both fields of `self`.  Take the path out, fill into the active
403        // buffer, then put the path back — preserves the "path persists
404        // across fill calls" GfxCtx contract.
405        let mut path = std::mem::replace(&mut self.path, PathStorage::new());
406        if let Some(gradient) = self.state.fill_linear_gradient.clone() {
407            let global_alpha = self.state.global_alpha as f32;
408            gradient::fill_linear_gradient(
409                self.active_buffer(),
410                &mut path,
411                &gradient,
412                global_alpha,
413                &xform,
414                clip,
415                rule,
416            );
417        } else if let Some(gradient) = self.state.fill_radial_gradient.clone() {
418            let global_alpha = self.state.global_alpha as f32;
419            gradient::fill_radial_gradient(
420                self.active_buffer(),
421                &mut path,
422                &gradient,
423                global_alpha,
424                &xform,
425                clip,
426                rule,
427            );
428        } else if let Some(pattern) = self.state.fill_pattern.clone() {
429            let global_alpha = self.state.global_alpha as f32;
430            gradient::fill_pattern(
431                self.active_buffer(),
432                &mut path,
433                &pattern,
434                global_alpha,
435                &xform,
436                clip,
437                rule,
438            );
439        } else {
440            let mut color = self.state.fill_color;
441            color.a *= self.state.global_alpha as f32;
442            self.active_buffer()
443                .fill_path(&mut path, color, &xform, clip, rule);
444        }
445        self.path = path;
446    }
447    fn stroke(&mut self) {
448        stroke::stroke(self);
449    }
450    fn fill_and_stroke(&mut self) {
451        self.fill();
452        self.stroke();
453    }
454
455    fn draw_triangles_aa(
456        &mut self,
457        vertices: &[[f32; 3]],
458        indices: &[u32],
459        color: crate::color::Color,
460    ) {
461        // LCD-coverage-cache backbuffer doesn't have a dedicated halo-AA
462        // path; rasterise each triangle as a solid fill, same as the
463        // software `GfxCtx` path.
464        let saved_fill = self.state.fill_color;
465        self.state.fill_color = color;
466        let n = indices.len() / 3;
467        for t in 0..n {
468            let i0 = indices[t * 3] as usize;
469            let i1 = indices[t * 3 + 1] as usize;
470            let i2 = indices[t * 3 + 2] as usize;
471            if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() {
472                continue;
473            }
474            let v0 = vertices[i0];
475            let v1 = vertices[i1];
476            let v2 = vertices[i2];
477            self.begin_path();
478            self.move_to(v0[0] as f64, v0[1] as f64);
479            self.line_to(v1[0] as f64, v1[1] as f64);
480            self.line_to(v2[0] as f64, v2[1] as f64);
481            self.close_path();
482            self.fill();
483        }
484        self.state.fill_color = saved_fill;
485    }
486
487    // ── Text ──────────────────────────────────────────────────────────────
488    fn fill_text(&mut self, text: &str, x: f64, y: f64) {
489        let font = match self.state.font.clone() {
490            Some(f) => f,
491            None => return,
492        };
493        let mut color = self.state.fill_color;
494        color.a *= self.state.global_alpha as f32;
495
496        // HiDPI: rasterise at the **physical** font size (logical × CTM
497        // scale).  See `gfx_ctx::fill_text` for the long version; short
498        // version: the mask composites 1:1 at its rasterised pixel count,
499        // so caching at logical size would shrink text on 2×/3× displays.
500        let t = &self.state.transform;
501        let ctm_scale = (t.sx * t.sx + t.shy * t.shy).sqrt().max(1e-6);
502        let phys_size = self.state.font_size * ctm_scale;
503        let cached = rasterize_text_lcd_cached(&font, text, phys_size);
504        // Match the legacy CPU LCD compositor: apply CTM to the destination
505        // origin, then snap to integer pixels.  Sub-pixel placement of an
506        // LCD mask smears the per-channel phase pattern across pixel
507        // boundaries (see `gfx_ctx::draw_lcd_mask` for the long story).
508        // Divide `baseline_*_in_mask` by `ctm_scale` so offsets stay in
509        // logical units that the CTM multiplies back to physical.
510        let dst_x = x - cached.baseline_x_in_mask / ctm_scale;
511        let dst_y = y - cached.baseline_y_in_mask / ctm_scale;
512        let sx = (dst_x * t.sx + dst_y * t.shx + t.tx).round() as i32;
513        let sy = (dst_x * t.shy + dst_y * t.sy + t.ty).round() as i32;
514
515        // Construct a borrowed-shape `LcdMask` for the cached bytes.  The
516        // clone is wasteful — Step 2b should give `composite_mask` a
517        // slice variant so we can hand it `&cached.pixels[..]` with no
518        // allocation.  For an MVP it doesn't matter.
519        let mask = LcdMask {
520            data: (*cached.pixels).clone(),
521            width: cached.width,
522            height: cached.height,
523        };
524        let clip_i = self.state.clip.map(crate::lcd_coverage::rect_to_pixel_clip);
525        self.active_buffer()
526            .composite_mask(&mask, color, sx, sy, clip_i);
527    }
528
529    fn fill_text_gsv(&mut self, text: &str, x: f64, y: f64, size: f64) {
530        // GSV is AGG's stroke-vector font — used for placeholder text
531        // before the real font is loaded.  We materialize the stroked
532        // outline into a flat path and feed it through `fill_path`,
533        // same shape as `stroke`.  Stroke width follows GfxCtx's choice
534        // of `size * 0.1` for visual parity.
535        let mut color = self.state.fill_color;
536        color.a *= self.state.global_alpha as f32;
537        let mut gsv = GsvText::new();
538        gsv.size(size, 0.0);
539        gsv.start_point(x, y);
540        gsv.text(text);
541        let mut materialized = PathStorage::new();
542        {
543            let mut stroke = ConvStroke::new(&mut gsv);
544            stroke.set_width(size * 0.1);
545            materialized.concat_path(&mut stroke, 0);
546        }
547        let xform = self.state.transform;
548        let clip = self.state.clip;
549        self.active_buffer()
550            .fill_path(&mut materialized, color, &xform, clip, FillRule::NonZero);
551    }
552
553    fn measure_text(&self, text: &str) -> Option<TextMetrics> {
554        let font = self.state.font.as_ref()?;
555        Some(measure_text_metrics(font, text, self.state.font_size))
556    }
557
558    // ── Transform ─────────────────────────────────────────────────────────
559    fn transform(&self) -> TransAffine {
560        self.state.transform
561    }
562    fn root_transform(&self) -> TransAffine {
563        let mut t = self.state.transform;
564        for layer in self.layer_stack.iter().rev() {
565            t.premultiply(&TransAffine::new_translation(
566                layer.origin_x,
567                layer.origin_y,
568            ));
569        }
570        t
571    }
572    fn save(&mut self) {
573        self.state_stack.push(self.state.clone());
574    }
575    fn restore(&mut self) {
576        if let Some(s) = self.state_stack.pop() {
577            self.state = s;
578        }
579    }
580    fn translate(&mut self, tx: f64, ty: f64) {
581        self.state
582            .transform
583            .premultiply(&TransAffine::new_translation(tx, ty));
584    }
585    fn rotate(&mut self, radians: f64) {
586        self.state
587            .transform
588            .premultiply(&TransAffine::new_rotation(radians));
589    }
590    fn scale(&mut self, sx: f64, sy: f64) {
591        self.state
592            .transform
593            .premultiply(&TransAffine::new_scaling(sx, sy));
594    }
595    fn set_transform(&mut self, m: TransAffine) {
596        self.state.transform = m;
597    }
598    fn reset_transform(&mut self) {
599        self.state.transform = TransAffine::new();
600    }
601
602    // ── Compositing layers ────────────────────────────────────────────────
603    //
604    // `push_layer` redirects subsequent paint into a fresh `LcdBuffer`;
605    // `pop_layer` flushes that buffer back into the previously-active
606    // one at the layer's recorded origin (the CTM translation at push
607    // time).  Compositing is full-replace — see `LcdBuffer::composite_buffer`
608    // for why LCD layers can't do alpha-aware SrcOver and what that
609    // means for callers.
610
611    fn push_layer(&mut self, width: f64, height: f64) {
612        let origin_x = self.state.transform.tx;
613        let origin_y = self.state.transform.ty;
614        let lw = width.ceil().max(1.0) as u32;
615        let lh = height.ceil().max(1.0) as u32;
616        let mut layer_buffer = LcdBuffer::new(lw, lh);
617
618        // Seed the layer with the parent's pixels at the layer's bounds.
619        // Without this, an LCD layer would composite-replace the parent
620        // region with its zero-init (black) wherever the user didn't paint —
621        // any "untouched" pixel inside the layer would visibly clear the
622        // parent on pop.  GfxCtx's RGBA layer dodges this with
623        // alpha=0 + SrcOver; LcdBuffer has no alpha, so we inherit the
624        // parent's content as the "neutral" starting state instead.
625        let dx = -(origin_x.round() as i32);
626        let dy = -(origin_y.round() as i32);
627        let parent_ref: &LcdBuffer = if let Some(layer) = self.layer_stack.last() {
628            &layer.buffer
629        } else {
630            &*self.base_buffer
631        };
632        layer_buffer.composite_buffer(parent_ref, dx, dy, None);
633
634        let saved_state = self.state.clone();
635        let saved_stack = std::mem::take(&mut self.state_stack);
636        self.layer_stack.push(LcdLayer {
637            buffer: layer_buffer,
638            saved_state,
639            saved_stack,
640            origin_x,
641            origin_y,
642        });
643        // Drawing inside the layer uses local coords (origin = layer's
644        // bottom-left).  Match `GfxCtx::push_layer` semantics — the new
645        // sub-region paints into a clean transform / no clip.
646        self.state.transform = TransAffine::new();
647        self.state.clip = None;
648    }
649
650    fn pop_layer(&mut self) {
651        let Some(layer) = self.layer_stack.pop() else {
652            return;
653        };
654        // Restore the state snapshot captured at push time.
655        self.state = layer.saved_state;
656        self.state_stack = layer.saved_stack;
657        // Composite the layer onto whatever buffer is now active (could
658        // be the base buffer, or another layer if we were nested).
659        // Origin is in the parent's coords; round so the layer lands on
660        // the integer pixel grid (same reason `draw_lcd_mask` rounds).
661        let dst_x = layer.origin_x.round() as i32;
662        let dst_y = layer.origin_y.round() as i32;
663        let clip_i = self.state.clip.map(crate::lcd_coverage::rect_to_pixel_clip);
664        self.active_buffer()
665            .composite_buffer(&layer.buffer, dst_x, dst_y, clip_i);
666    }
667
668    // ── LCD mask compositing — native format for this ctx ─────────────────
669    //
670    // Unlike `GfxCtx` (which has a separate `lcd_mode` flag), an
671    // `LcdGfxCtx`'s render target IS an LCD coverage buffer.  Compositing
672    // an `LcdMask` is the most direct primitive available.
673
674    fn draw_lcd_mask(
675        &mut self,
676        mask: &[u8],
677        mask_w: u32,
678        mask_h: u32,
679        src_color: Color,
680        dst_x: f64,
681        dst_y: f64,
682    ) {
683        if mask.len() < (mask_w as usize) * (mask_h as usize) * 3 {
684            return;
685        }
686        let lcd_mask = LcdMask {
687            data: mask.to_vec(),
688            width: mask_w,
689            height: mask_h,
690        };
691        let t = &self.state.transform;
692        let sx = (dst_x * t.sx + dst_y * t.shx + t.tx).round() as i32;
693        let sy = (dst_x * t.shy + dst_y * t.sy + t.ty).round() as i32;
694        let clip_i = self.state.clip.map(crate::lcd_coverage::rect_to_pixel_clip);
695        self.active_buffer()
696            .composite_mask(&lcd_mask, src_color, sx, sy, clip_i);
697    }
698
699    fn has_lcd_mask_composite(&self) -> bool {
700        true
701    }
702
703    // ── Image blitting ────────────────────────────────────────────────────
704    //
705    // Images are written as plain colour content — every subpixel of every
706    // destination pixel mixes the source colour by the source's alpha.
707    // We deliberately DON'T run image data through the 3× supersample +
708    // 5-tap filter (the pipeline is for coverage, not colour) — that would
709    // smear chroma across pixel boundaries and tint sharp icon edges with
710    // R/G/B fringing.  This matches the convention of every LCD text
711    // renderer (FreeType / CoreText / DirectWrite): subpixel treatment is
712    // for glyph coverage; bitmaps go through standard alpha compositing.
713
714    fn has_image_blit(&self) -> bool {
715        true
716    }
717
718    fn draw_image_rgba(
719        &mut self,
720        data: &[u8],
721        img_w: u32,
722        img_h: u32,
723        dst_x: f64,
724        dst_y: f64,
725        dst_w: f64,
726        dst_h: f64,
727    ) {
728        let transform = self.state.transform;
729        let global_alpha = self.state.global_alpha as f32;
730        let clip = self.state.clip;
731        image::draw_image_rgba(
732            self.active_buffer(),
733            data,
734            img_w,
735            img_h,
736            dst_x,
737            dst_y,
738            dst_w,
739            dst_h,
740            &transform,
741            global_alpha,
742            clip,
743        );
744    }
745}
746
747fn configure_stroke<VS: VertexSource>(
748    stroke: &mut ConvStroke<VS>,
749    width: f64,
750    join: LineJoin,
751    cap: LineCap,
752    miter_limit: f64,
753) {
754    stroke.set_width(width);
755    stroke.set_line_join(join);
756    stroke.set_line_cap(cap);
757    stroke.set_miter_limit(miter_limit);
758}
759
760fn configure_dashes<VS: VertexSource>(dash: &mut ConvDash<VS>, dashes: &[f64], dash_offset: f64) {
761    let mut chunks = dashes.chunks_exact(2);
762    for pair in &mut chunks {
763        dash.add_dash(pair[0], pair[1]);
764    }
765    if let Some(&last) = chunks.remainder().first() {
766        dash.add_dash(last, last);
767    }
768    dash.dash_start(dash_offset);
769}
770
771// ── Tests ──────────────────────────────────────────────────────────────────
772
773#[cfg(test)]
774mod tests;