Skip to main content

agg_gui/gfx_ctx/
layers.rs

1//! Offscreen compositing layer support for `GfxCtx`.
2//!
3//! Layers let widgets and SVG groups render into temporary transparent
4//! framebuffers before compositing the result back into the active target.
5
6use super::*;
7
8impl GfxCtx<'_> {
9    // -------------------------------------------------------------------------
10    // Layer compositing
11    // -------------------------------------------------------------------------
12
13    /// Begin an offscreen compositing layer of `width × height` **logical**
14    /// pixels.
15    ///
16    /// All draw calls until the matching `pop_layer` are redirected into a fresh
17    /// transparent `Framebuffer`.  The current CTM's translation records the
18    /// layer's screen-space (physical) origin; the CTM's **scale is preserved**
19    /// so the subtree rasterises at physical resolution inside the layer buffer.
20    pub fn push_layer(&mut self, width: f64, height: f64) {
21        self.push_layer_with_alpha(width, height, 1.0);
22    }
23
24    pub fn push_layer_with_alpha(&mut self, width: f64, height: f64, alpha: f64) {
25        let origin_x = self.state.transform.tx;
26        let origin_y = self.state.transform.ty;
27        // Preserve the CTM's scale across the layer boundary.  Without this the
28        // layer buffer would be sized in logical pixels and drawing would use an
29        // identity transform, so on a HiDPI display (device scale > 1) a
30        // composited subtree rendered at half size into the physical-pixel
31        // target — wrong size and position.  We size the buffer in physical
32        // pixels (logical × scale) and start the layer's local transform at that
33        // pure scale (translation reset to 0, since `origin_x/origin_y` already
34        // records where the layer lands in the parent).  This mirrors the wgpu
35        // backend's `push_layer_with_alpha_impl`.  Rotation/shear in the CTM are
36        // intentionally not carried into the layer — the compositing-layer
37        // callers (opacity groups, widget backbuffers) are always axis-aligned.
38        let (scale_x, scale_y) = self.state.transform.scaling_abs();
39        let scale_x = scale_x.max(1e-6);
40        let scale_y = scale_y.max(1e-6);
41        let phys_w = (width * scale_x).ceil().max(1.0) as u32;
42        let phys_h = (height * scale_y).ceil().max(1.0) as u32;
43        let saved_state = self.state.clone();
44        let saved_stack = std::mem::take(&mut self.state_stack);
45        let layer_fb = Framebuffer::new(phys_w, phys_h);
46        self.layer_stack.push(LayerEntry {
47            fb: layer_fb,
48            saved_state,
49            saved_stack,
50            origin_x,
51            origin_y,
52            alpha: alpha.clamp(0.0, 1.0),
53        });
54        // Reset to local-space origin, keeping the device/UX scale so content
55        // rasterises 1:1 onto the physical-pixel layer buffer.
56        self.state.transform = TransAffine::new_scaling(scale_x, scale_y);
57        self.state.clip = None;
58    }
59
60    /// SrcOver-composite the current layer into the previous render target, then
61    /// restore the graphics state that was active at the matching `push_layer`.
62    pub fn pop_layer(&mut self) {
63        let Some(layer) = self.layer_stack.pop() else {
64            return;
65        };
66        let ox = layer.origin_x as i32;
67        let oy = layer.origin_y as i32;
68        self.state = layer.saved_state;
69        self.state_stack = layer.saved_stack;
70        // Clip the composite to the parent scissor restored above — a layer
71        // sized larger than its clipped content must not overpaint siblings
72        // (e.g. a window's opacity group spilling over the title bar).
73        let parent_clip = self.state.clip;
74        // Composite: src = layer.fb, dst = now-active framebuffer.
75        if let Some(top) = self.layer_stack.last_mut() {
76            composite_framebuffers(&mut top.fb, &layer.fb, ox, oy, layer.alpha, parent_clip);
77        } else {
78            composite_framebuffers(self.base_fb, &layer.fb, ox, oy, layer.alpha, parent_clip);
79        }
80    }
81}