Skip to main content

agg_gui/widget/
paint.rs

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