Skip to main content

agg_gui/widget/
paint.rs

1use std::sync::Arc;
2
3use crate::framebuffer::Framebuffer;
4use crate::gfx_ctx::GfxCtx;
5use crate::lcd_coverage::LcdBuffer;
6
7use super::*;
8
9std::thread_local! {
10    static PAINT_CLIP_STACK: std::cell::RefCell<Vec<Rect>> =
11        std::cell::RefCell::new(Vec::new());
12}
13
14/// Current visible paint clip in root coordinates, if painting is inside a
15/// clipped subtree. Widgets can use this to avoid starting expensive work for
16/// content that traversal visits but the active clip will discard.
17pub fn current_paint_clip() -> Option<Rect> {
18    PAINT_CLIP_STACK.with(|stack| stack.borrow().last().copied())
19}
20
21// ---------------------------------------------------------------------------
22// Tree traversal helpers (free functions operating on &mut dyn Widget)
23// ---------------------------------------------------------------------------
24
25/// Paint `widget` and all its descendants. The caller must ensure `ctx` is
26/// already translated so that (0,0) maps to `widget`'s bottom-left corner.
27///
28/// If the widget returns `Some` from [`Widget::backbuffer_cache_mut`], the
29/// whole subtree (widget + children + overlay) is rendered once into a CPU
30/// [`Framebuffer`] via a software [`GfxCtx`], cached as an
31/// `Arc<Vec<u8>>` on the widget, and blitted through
32/// [`DrawCtx::draw_image_rgba_arc`].  Subsequent frames that find
33/// `cache.dirty == false` skip the re-raster entirely and just blit the
34/// existing bitmap — identical fast path to MatterCAD's `DoubleBuffer`.
35pub fn paint_subtree(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
36    if !widget.is_visible() {
37        if paint_subtree_unified_backbuffer(widget, ctx, true) {
38            return;
39        }
40        if ctx.supports_compositing_layers() {
41            if let Some(layer) = widget.compositing_layer() {
42                paint_subtree_layer(widget, ctx, true, layer);
43            }
44        }
45        return;
46    }
47
48    // Snap CTM at paint_subtree ENTRY — see the commentary preserved
49    // below inside `paint_subtree_direct` for the full rationale.  The
50    // backbuffer path bypasses this because the bitmap is already at
51    // integer texel positions by construction.
52    if paint_subtree_unified_backbuffer(widget, ctx, true) {
53        return;
54    } else if widget.backbuffer_cache_mut().is_some() {
55        paint_subtree_backbuffered(widget, ctx);
56    } else {
57        paint_subtree_direct(widget, ctx);
58    }
59}
60
61fn paint_subtree_unified_backbuffer(
62    widget: &mut dyn Widget,
63    ctx: &mut dyn DrawCtx,
64    include_overlay: bool,
65) -> bool {
66    let spec = widget.backbuffer_spec();
67    if spec.kind == BackbufferKind::None {
68        return false;
69    }
70
71    match spec.kind {
72        BackbufferKind::GlFbo if ctx.supports_retained_layers() => {
73            paint_subtree_gl_backbuffer(widget, ctx, include_overlay, spec);
74            true
75        }
76        BackbufferKind::SoftwareRgba | BackbufferKind::SoftwareLcd => {
77            // Existing CPU widgets still use `backbuffer_cache_mut`; the
78            // unified spec provides the migration point without changing their
79            // current behavior.
80            if widget.backbuffer_cache_mut().is_some() {
81                paint_subtree_backbuffered(widget, ctx);
82                true
83            } else {
84                false
85            }
86        }
87        _ => false,
88    }
89}
90
91fn paint_subtree_gl_backbuffer(
92    widget: &mut dyn Widget,
93    ctx: &mut dyn DrawCtx,
94    include_overlay: bool,
95    spec: BackbufferSpec,
96) {
97    let b = widget.bounds();
98    let layer_w = (b.width + spec.outsets.left + spec.outsets.right).max(1.0);
99    let layer_h = (b.height + spec.outsets.bottom + spec.outsets.top).max(1.0);
100    let subtree_needs_draw = widget.needs_draw();
101    let theme_epoch = crate::theme::current_visuals_epoch();
102    let typography_epoch = crate::font_settings::current_typography_epoch();
103    let async_state_epoch = crate::animation::async_state_epoch();
104    let (key, needs_draw) = {
105        let Some(state) = widget.backbuffer_state_mut() else {
106            paint_subtree_direct(widget, ctx);
107            return;
108        };
109        let w = layer_w.ceil().max(1.0) as u32;
110        let h = layer_h.ceil().max(1.0) as u32;
111        let changed = state.width != w || state.height != h || state.spec_kind != spec.kind;
112        let style_changed = state.theme_epoch != theme_epoch
113            || state.typography_epoch != typography_epoch
114            || state.async_state_epoch != async_state_epoch;
115        let needs = !spec.cached || state.dirty || changed || style_changed || subtree_needs_draw;
116        if changed {
117            state.width = w;
118            state.height = h;
119            state.spec_kind = spec.kind;
120        }
121        (state.id(), needs)
122    };
123
124    if spec.cached && !needs_draw {
125        ctx.save();
126        ctx.translate(-spec.outsets.left, -spec.outsets.bottom);
127        let composited = ctx.composite_retained_layer(key, layer_w, layer_h, spec.alpha);
128        ctx.restore();
129        if composited {
130            if let Some(state) = widget.backbuffer_state_mut() {
131                state.composite_count = state.composite_count.saturating_add(1);
132            }
133            return;
134        }
135    }
136
137    ctx.save();
138    ctx.translate(-spec.outsets.left, -spec.outsets.bottom);
139    if spec.cached {
140        ctx.push_retained_layer_with_alpha(key, layer_w, layer_h, spec.alpha);
141    } else {
142        ctx.push_layer_with_alpha(layer_w, layer_h, spec.alpha);
143    }
144    ctx.translate(spec.outsets.left, spec.outsets.bottom);
145    paint_subtree_direct_inner(widget, ctx, include_overlay, false);
146    ctx.pop_layer();
147    ctx.restore();
148
149    if let Some(state) = widget.backbuffer_state_mut() {
150        state.dirty = false;
151        state.theme_epoch = theme_epoch;
152        state.typography_epoch = typography_epoch;
153        state.async_state_epoch = async_state_epoch;
154        state.repaint_count = state.repaint_count.saturating_add(1);
155        state.composite_count = state.composite_count.saturating_add(1);
156    }
157}
158
159fn paint_subtree_layer(
160    widget: &mut dyn Widget,
161    ctx: &mut dyn DrawCtx,
162    include_overlay: bool,
163    layer: crate::widget::CompositingLayer,
164) {
165    let b = widget.bounds();
166    let layer_w = (b.width + layer.outset_left + layer.outset_right).max(1.0);
167    let layer_h = (b.height + layer.outset_bottom + layer.outset_top).max(1.0);
168
169    ctx.save();
170    ctx.translate(-layer.outset_left, -layer.outset_bottom);
171    ctx.push_layer_with_alpha(layer_w, layer_h, layer.alpha);
172    ctx.translate(layer.outset_left, layer.outset_bottom);
173    paint_subtree_direct_inner(widget, ctx, include_overlay, false);
174    ctx.pop_layer();
175    ctx.restore();
176}
177
178/// Paint app-level overlays after the whole tree has rendered.
179///
180/// Traverses in paint order while preserving each widget's normal local
181/// transform. Implementors can use `ctx.root_transform()` to submit app-level
182/// overlay geometry without forcing retained parents to repaint.
183pub fn paint_global_overlays(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
184    if !widget.is_visible() {
185        return;
186    }
187    let n = widget.children().len();
188    for i in 0..n {
189        let child = &mut widget.children_mut()[i];
190        let b = child.bounds();
191        ctx.save();
192        ctx.translate(b.x, b.y);
193        paint_global_overlays(child.as_mut(), ctx);
194        ctx.restore();
195    }
196    widget.paint_global_overlay(ctx);
197}
198
199/// Direct (non-cached) paint: widget and its children paint onto `ctx`
200/// at the current CTM.  This is the default path for widgets that don't
201/// opt into backbuffer caching via `Widget::backbuffer_cache_mut`.
202fn paint_subtree_direct(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
203    paint_subtree_direct_inner(widget, ctx, true, true);
204}
205
206/// Cache-building variant: paints body + children into the given ctx
207/// WITHOUT calling `paint_overlay`.  The overlay is what `TextField` uses
208/// for its blinking cursor — if we baked the overlay into the cache bitmap,
209/// the drawn cursor would stay visible forever on blit while a second
210/// (blinking) overlay was being drawn on top of it every frame, producing
211/// two cursors.  Overlay runs only on the outer ctx in
212/// `paint_subtree_backbuffered` after the cache blit.
213fn paint_subtree_direct_no_overlay(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
214    paint_subtree_direct_inner(widget, ctx, false, true);
215}
216
217fn paint_subtree_direct_inner(
218    widget: &mut dyn Widget,
219    ctx: &mut dyn DrawCtx,
220    include_overlay: bool,
221    allow_compositing_layer: bool,
222) {
223    if allow_compositing_layer && ctx.supports_compositing_layers() {
224        if let Some(layer) = widget.compositing_layer() {
225            paint_subtree_layer(widget, ctx, include_overlay, layer);
226            return;
227        }
228    }
229
230    let snap_this = widget.enforce_integer_bounds();
231    if snap_this {
232        ctx.save();
233        ctx.snap_to_pixel();
234    }
235
236    widget.paint(ctx);
237
238    let b = widget.bounds();
239    let (cx, cy, cw, ch) = widget
240        .clip_children_rect()
241        .unwrap_or((0.0, 0.0, b.width, b.height));
242    ctx.save();
243    ctx.clip_rect(cx, cy, cw, ch);
244    let clip = root_rect_from_local(ctx, cx, cy, cw, ch);
245    PAINT_CLIP_STACK.with(|stack| {
246        let mut stack = stack.borrow_mut();
247        let clipped = if let Some(prev) = stack.last().copied() {
248            intersect_rects(prev, clip).unwrap_or_else(|| Rect::new(0.0, 0.0, 0.0, 0.0))
249        } else {
250            clip
251        };
252        stack.push(clipped);
253    });
254
255    let n = widget.children().len();
256    for i in 0..n {
257        let child_bounds = widget.children()[i].bounds();
258        let snap_to_pixel = widget.children()[i].enforce_integer_bounds();
259        ctx.save();
260        if snap_to_pixel {
261            ctx.translate(child_bounds.x.round(), child_bounds.y.round());
262        } else {
263            ctx.translate(child_bounds.x, child_bounds.y);
264        }
265        let child = &mut widget.children_mut()[i];
266        paint_subtree(child.as_mut(), ctx);
267        ctx.restore();
268    }
269
270    PAINT_CLIP_STACK.with(|stack| {
271        stack.borrow_mut().pop();
272    });
273    ctx.restore(); // lifts the children clip before paint_overlay
274    if include_overlay {
275        widget.paint_overlay(ctx);
276    }
277    widget.finish_paint(ctx);
278
279    if snap_this {
280        ctx.restore();
281    }
282}
283
284fn root_rect_from_local(ctx: &dyn DrawCtx, x: f64, y: f64, w: f64, h: f64) -> Rect {
285    let mut points = [(x, y), (x + w, y), (x, y + h), (x + w, y + h)];
286    let transform = ctx.root_transform();
287    for (px, py) in &mut points {
288        transform.transform(px, py);
289    }
290    let min_x = points.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
291    let max_x = points
292        .iter()
293        .map(|(x, _)| *x)
294        .fold(f64::NEG_INFINITY, f64::max);
295    let min_y = points.iter().map(|(_, y)| *y).fold(f64::INFINITY, f64::min);
296    let max_y = points
297        .iter()
298        .map(|(_, y)| *y)
299        .fold(f64::NEG_INFINITY, f64::max);
300    Rect::new(
301        min_x,
302        min_y,
303        (max_x - min_x).max(0.0),
304        (max_y - min_y).max(0.0),
305    )
306}
307
308fn intersect_rects(a: Rect, b: Rect) -> Option<Rect> {
309    let x0 = a.x.max(b.x);
310    let y0 = a.y.max(b.y);
311    let x1 = (a.x + a.width).min(b.x + b.width);
312    let y1 = (a.y + a.height).min(b.y + b.height);
313    (x1 >= x0 && y1 >= y0).then(|| Rect::new(x0, y0, x1 - x0, y1 - y0))
314}
315
316/// Backbuffered paint: re-raster through AGG if dirty, blit the cached
317/// bitmap via `draw_image_rgba_arc` regardless.
318///
319/// # HiDPI
320///
321/// The backing bitmap is allocated at **physical pixel** dimensions
322/// (`bounds × device_scale`) and the sub-ctx running the widget's paint has
323/// a matching `scale(dps, dps)` applied.  This means glyph outlines are
324/// rasterised at the physical grid — "true" HiDPI rendering, not pixel
325/// doubling — and the outer blit then draws the physical-sized image at the
326/// widget's logical rect, which the outer CTM (also scaled by dps) maps 1:1
327/// back to physical pixels.  Net: logical layout, physical rasterisation,
328/// zero upscale blur.
329fn paint_subtree_backbuffered(widget: &mut dyn Widget, ctx: &mut dyn DrawCtx) {
330    // Snap the outer CTM to the pixel grid BEFORE blitting the cached
331    // bitmap.  `draw_image_rgba_arc` uses a NEAREST filter for Arc-keyed
332    // textures (1:1 blit lane), so a fractional CTM translation shifts
333    // every screen pixel by a sub-texel amount — reading back interpolated
334    // near-black/near-white instead of the crisp AGG output.  Snapping
335    // here restores the "AGG rasterised it, show it at the pixel grid"
336    // contract the old pre-refactor code preserved.
337    ctx.save();
338    ctx.snap_to_pixel();
339
340    let b = widget.bounds();
341    let dps = crate::device_scale::device_scale().max(1e-6);
342    // Physical pixel dimensions of the offscreen render target.
343    let w_phys = (b.width * dps).ceil().max(1.0) as u32;
344    let h_phys = (b.height * dps).ceil().max(1.0) as u32;
345    // Logical dimensions used as the blit destination rect.  **Must** be
346    // derived from `w_phys / dps` rather than `b.width` so the quad the
347    // bitmap is drawn into matches the bitmap's actual pixel extent.  If
348    // `b.width` is non-integer (e.g. 19.5 for a sidebar Label), using
349    // it as `dst_w` stretches a 20-pixel bitmap into a 19.5-pixel quad —
350    // sub-pixel shrink that drops partial-coverage rows at the edges,
351    // which reads as a faint fade along the top / bottom of the glyph.
352    // Pre-HiDPI the blit used the bitmap's integer pixel size directly;
353    // this restores that contract for the logical-units pipeline.
354    let w_logical = w_phys as f64 / dps;
355    let h_logical = h_phys as f64 / dps;
356
357    // Decide whether to re-raster.  Size change invalidates; so does a
358    // mode swap — if the cache holds `Rgba` bytes but the widget now
359    // wants `LcdCoverage` (or vice versa) we must re-raster through the
360    // correct pipeline.  Mode membership is recorded implicitly by
361    // `cache.lcd_alpha`: `Some` means LCD cache, `None` means Rgba.
362    let mode = widget.backbuffer_mode();
363    let mode_is_lcd = matches!(mode, BackbufferMode::LcdCoverage);
364    let theme_epoch = crate::theme::current_visuals_epoch();
365    let typography_epoch = crate::font_settings::current_typography_epoch();
366    let async_state_epoch = crate::animation::async_state_epoch();
367    let (needs_raster, has_bitmap) = {
368        let cache = widget
369            .backbuffer_cache_mut()
370            .expect("backbuffered widget must return Some from backbuffer_cache_mut");
371        let cache_is_lcd = cache.lcd_alpha.is_some();
372        let needs = cache.dirty
373            || cache.pixels.is_none()
374            || cache.width != w_phys
375            || cache.height != h_phys
376            || cache_is_lcd != mode_is_lcd
377            || cache.theme_epoch != theme_epoch
378            || cache.typography_epoch != typography_epoch
379            || cache.async_state_epoch != async_state_epoch;
380        (needs, cache.pixels.is_some())
381    };
382
383    if needs_raster {
384        // Allocate a fresh render target whose format matches the
385        // widget's chosen backbuffer mode, paint the subtree into it,
386        // then convert to top-down RGBA for the cache (the blit lane
387        // expects `(R, G, B, A)` rows top-first).
388        //
389        // `LcdCoverage` mode now uses an `LcdGfxCtx` over an `LcdBuffer`
390        // — every primitive (fill, stroke, text, image) flows through
391        // the per-channel LCD pipeline, so child widgets that paint
392        // into this widget's backbuffer compose correctly with
393        // LCD-treated text instead of breaking the per-channel
394        // coverage at the first non-text fill (the alpha bug the
395        // search-box screenshot showed before this change).
396        // Each branch produces `(pixels, lcd_alpha)` top-down:
397        //   - `Rgba`: `pixels` = straight-alpha RGBA8; `lcd_alpha` = None.
398        //   - `LcdCoverage`: `pixels` = premultiplied colour plane (3 B/px);
399        //     `lcd_alpha` = per-channel alpha plane (3 B/px).  The blit
400        //     step below picks a compositor based on which is present.
401        let (pixels_bytes, lcd_alpha_bytes): (Vec<u8>, Option<Vec<u8>>) = match mode {
402            BackbufferMode::Rgba => {
403                let mut fb = Framebuffer::new(w_phys, h_phys);
404                {
405                    let mut sub = GfxCtx::new(&mut fb);
406                    sub.set_lcd_mode(false); // RGBA mode never uses LCD text
407                    if (dps - 1.0).abs() > 1e-6 {
408                        // Widgets paint in logical coords — scale the sub ctx
409                        // so their drawing lands on the physical pixel grid.
410                        sub.scale(dps, dps);
411                    }
412                    paint_subtree_direct_no_overlay(widget, &mut sub);
413                }
414                // Two conversions to make the bitmap directly blittable:
415                //   1. Row order — Framebuffer is Y-up, blit lane is top-down.
416                //   2. Alpha format — AGG writes premultiplied; the blend
417                //      function expects straight alpha so that half-coverage
418                //      AA edges composite without the dark-fringe artifact.
419                let mut pixels = fb.pixels_flipped();
420                crate::framebuffer::unpremultiply_rgba_inplace(&mut pixels);
421                (pixels, None)
422            }
423            BackbufferMode::LcdCoverage => {
424                // The LCD pipeline is strictly WRITE-only.  The buffer
425                // starts at zero coverage everywhere; the widget paints
426                // opaque content covering its full bounds (the contract
427                // for this mode) into it via an `LcdGfxCtx`; then the
428                // two planes (premultiplied colour + per-channel alpha)
429                // are cached and composited onto the destination at
430                // blit time via `draw_lcd_backbuffer_arc` — which
431                // preserves LCD per-channel chroma through the cache.
432                //
433                // We deliberately do NOT read from any destination —
434                // seeding the buffer from the parent's pixels would
435                // tie the cache's validity to the widget's current
436                // screen position (stale on scroll / reparent), stall
437                // the GPU pipeline on GL (glReadPixels is sync), and
438                // break on backends that can't read their own target.
439                // Widgets that can't paint their own opaque bg should
440                // use `Rgba` mode or paint through the parent's ctx
441                // directly instead.
442                let mut buf = LcdBuffer::new(w_phys, h_phys);
443                {
444                    let mut sub = crate::lcd_gfx_ctx::LcdGfxCtx::new(&mut buf);
445                    if (dps - 1.0).abs() > 1e-6 {
446                        // Match the RGBA branch: widgets paint in logical
447                        // coords; the sub ctx's scale transforms them into
448                        // the physical-pixel LCD buffer.
449                        sub.scale(dps, dps);
450                    }
451                    paint_subtree_direct_no_overlay(widget, &mut sub);
452                }
453                (buf.color_plane_flipped(), Some(buf.alpha_plane_flipped()))
454            }
455        };
456        let pixels = Arc::new(pixels_bytes);
457        let lcd_alpha = lcd_alpha_bytes.map(Arc::new);
458
459        let cache = widget.backbuffer_cache_mut().unwrap();
460        cache.pixels = Some(Arc::clone(&pixels));
461        cache.lcd_alpha = lcd_alpha.as_ref().map(Arc::clone);
462        cache.width = w_phys;
463        cache.height = h_phys;
464        cache.dirty = false;
465        cache.theme_epoch = theme_epoch;
466        cache.typography_epoch = typography_epoch;
467        cache.async_state_epoch = async_state_epoch;
468    }
469
470    // Blit the cached bitmap onto the outer ctx.  Two paths:
471    //
472    //   - `Rgba` cache (no `lcd_alpha`): a single RGBA8 texture via the
473    //     standard image-blit lane.  Alpha-aware SrcOver at the blend
474    //     stage handles transparency.
475    //
476    //   - `LcdCoverage` cache (`lcd_alpha` is `Some`): two 3-byte/pixel
477    //     planes — premultiplied colour + per-channel alpha.  The
478    //     backend's `draw_lcd_backbuffer_arc` composites them with
479    //     per-channel src-over, preserving LCD chroma through the
480    //     cache round-trip (grayscale AA on backends that fall back
481    //     to the default trait impl).
482    let cache = widget.backbuffer_cache_mut().unwrap();
483    // Image is physical-sized; dst is logical.  The outer CTM already has
484    // `scale(dps, dps)` active, so logical dst × dps == physical dst ==
485    // bitmap size, giving a 1:1 texel-to-pixel blit (no up/downscale blur).
486    let img_w = cache.width;
487    let img_h = cache.height;
488    match (cache.pixels.as_ref(), cache.lcd_alpha.as_ref()) {
489        (Some(color), Some(alpha)) => {
490            ctx.draw_lcd_backbuffer_arc(color, alpha, img_w, img_h, 0.0, 0.0, w_logical, h_logical);
491        }
492        (Some(bmp), None) => {
493            ctx.draw_image_rgba_arc(bmp, img_w, img_h, 0.0, 0.0, w_logical, h_logical);
494        }
495        _ => {}
496    }
497    let _ = has_bitmap;
498
499    // Overlay paint runs AFTER the cache blit and paints directly onto
500    // the outer ctx.  Widgets use this for content that changes too
501    // often to be worth caching — the canonical case is `TextField`'s
502    // blinking cursor, which flips twice per second and would otherwise
503    // invalidate the cache 2×/s.  With overlay, cursor is drawn fresh
504    // each frame onto the already-blitted bg+text; the cache only
505    // invalidates when the text/focus/selection actually changes.
506    //
507    // `paint_subtree_direct` has the same overlay call after children
508    // (see its own body); this keeps the two paint paths consistent.
509    widget.paint_overlay(ctx);
510
511    ctx.restore(); // pops the snap_to_pixel save above.
512}