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}