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}