Skip to main content

Crate backdrop_blur_egui

Crate backdrop_blur_egui 

Source
Expand description

backdrop-blur-egui — the egui adapter for frosted glass, over two paths sharing one Surface vocabulary.

  • grab-pass (the mainstream path: eframe-on-glow and the cage Wayland kiosk). The host owns the GL loop; GrabPassRenderer rides an egui paint callback that grabs the live framebuffer behind a surface, blurs it, and composites the frosted surface back. Build it once from eframe::CreationContext::gl, call .frost(ui, surface) per frame, and .destroy(gl) in eframe::App::on_exit. Pulls glow, never wgpu.
  • own-loop (default feature). For a host driving egui-winit + egui-wgpu directly (not eframe), OwnLoopRenderer renders the UI into an offscreen intermediate, blurs a region of it, and composites a frosted Surface over the display target — one encoder, one submit, in the order that does not panic (DESIGN §6). Pulls the wgpu stack.

Pick the path with a feature: a kiosk build is --no-default-features --features grab-pass and compiles neither wgpu nor egui-wgpu; an own-loop build is the default. The backends themselves are the separate backdrop-blur-glow / backdrop-blur-wgpu crates.

The crate owns only a surface’s background. The surface’s content, foreground, and accessibility stay the host’s: a frosted Surface is a post-render composite, never an egui widget, so it adds nothing to the AccessKit tree.

§The three dials: blur, tint, opacity

A frosted Surface mixes three independent knobs — conflating them is the most common “my glass looks wrong”:

  • BlurStrength — the blur radius in logical points. How smeared the backdrop is. 0 = no blur (a plain tinted pane).
  • Tint — the glass film painted over the blur, a linear-light color whose alpha is the film mix (how much tint shows vs. how much blurred backdrop shows through). A colored tint composites in color, not black — author it as sRGB with Tint::from_srgb_unmultiplied so the linear decode is done for you. Alpha 0 = pure blur, no film; alpha 1 = the film is opaque and the blur is invisible under it.
  • Opacity — the surface-global presence in [0, 1], the whole frosted result blended over the destination. This is the fade dial: drive it per frame to dissolve glass in/out. Default 1.0.

Rule of thumb: blur sets the texture, tint-alpha sets the material, opacity sets the presence. A barely-tinted heavy blur is clear vibrancy; a high tint-alpha is frosted/opaque glass; opacity below 1 fades the entire thing.

§Grab-pass contracts (read before calling frost)

The grab-pass path samples the live framebuffer mid-frame, which makes draw order and fade load-bearing in ways the types cannot enforce:

  1. Enqueue the frost before the surface’s foreground. The callback grabs whatever is in the framebuffer at its position — content drawn before it. Call frost(ui, surface) first, then paint the surface’s own content (text, controls) after, so the foreground lands on top of the blur. Enqueue it too late and it grabs — and blurs away — your own content. There is no runtime guard for this; it is a hard ordering contract.
  2. Fade with Opacity, not multiply_opacity. egui’s Ui::multiply_opacity (and the Opacity style) do not reach paint callbacks — the standard fade silently no-ops on the blur. To dissolve frost in/out, drive the surface’s opacity field (Opacity) per frame instead. This is the one egui trap that bites everyone; the Opacity dial is the supported escape hatch.
  3. A dynamically-sized rect needs last frame’s rect. In immediate mode the surface’s rect is only known after its content lays out, but the frost must be enqueued before the content paints (contract 1) — a chicken-and-egg. The worked pattern: stash the rect in egui temp memory keyed by an Id, frost last frame’s rect at the top of this frame, then lay out the content and write back the rect for next frame. It is stable while the surface is open; the only artifact is one frame of staleness on a resize. (A first-class reserved-slot API that returns the callback Shape for painter.set() is planned; until then this is the recommendation.)
  4. GrabPassRenderer::took_effect reports ran, not composited. egui skips a fully-clipped callback, so a frosted surface is not guaranteed to paint; took_effect lets the host observe that the callback fired. It is set even when the region clipped to nothing or the frost errored — it answers “did egui invoke my callback this frame”, not “did pixels change”. Useful to confirm wiring; not a success signal.

Re-exports§

pub use glow;

Structs§

BlurStrength
Blur radius in logical points.
CornerRadius
Corner radius in logical points. Resolves (× the target region’s Scale) to a physical-pixel radius, clamped so it can never overshoot the surface (the clamp lives in crate::ResolvedMask::from_target). Non-negative by construction.
FrameInput
One frame’s egui output plus where to draw it.
GrabPassRenderer
Drives the grab-pass (eframe-on-glow) frosted-glass path: holds the glow backend behind a mutex (the paint callback is Fn + Send + Sync, so it cannot own &mut) and enqueues a paint callback per frosted Surface.
LinearRgba
A straight-alpha color in linear light. RGB are linear (already gamma-decoded) and may exceed 1.0 (HDR over-bright); alpha is coverage in [0, 1] (never gamma-encoded). The blur convolution runs in linear light, so a tint authored in sRGB must be decoded first — that is exactly what Self::from_srgb_unmultiplied does, so callers never hand the backend gamma-encoded tint values (DESIGN §4.2).
Opacity
Surface-global fade coverage in [0, 1] — how present the whole frosted surface is, distinct from Tint’s alpha (which is the film mix, blur vs tint color) and from BlurStrength (the radius). It scales the composite’s final blend weight: 1.0 is the surface fully composited (the default — every existing caller and golden is unchanged), 0.0 leaves the destination untouched (the surface absent), and a fractional value blends the frosted result over the destination by that factor. A consumer animating a surface in/out (a modal scrim fading with its dialog) drives this per frame.
OwnLoopRenderer
Drives one own-loop frame for an egui-winit + egui-wgpu host: it renders the egui UI into the intermediate (the blur source) and the target (the display), then blurs and composites the frosted surfaces over the target — all on one encoder with a single submit.
ScreenDescriptor
Information about the screen used for rendering.
SourceView
The backdrop source: a sampleable view plus the two things a wgpu::TextureView cannot tell the backend on its own — the texture’s pixel size and its color space. The host constructs one per frame from its offscreen intermediate; it owns the view for the call’s duration.
Surface
A frosted surface to composite this frame: an egui-space rectangle (logical points) plus the glass parameters and a liveness policy. v1 treats the backdrop directly behind the rect as the blur source (source_region == target_rect).
Tint
The glass film painted over the blurred backdrop. The wrapped color is linear-light; its alpha is the film opacity (how much of the tint shows over the blur).
WgpuBlur
The wgpu implementation of BackdropBlur. Holds the fixed pipeline machinery (bind-group layout, sampler, Gaussian/downsample/upsample pipelines) and the per-(size) scratch (Gaussian ping-pong + dual-Kawase pyramid) + per-target-format composite caches, so repeated frosted surfaces reuse them.

Enums§

RepaintPolicy
How often the frosted surface’s backdrop must be re-grabbed and re-blurred. The adapter — not core — drives the host’s request_repaint from this; core only names the obligation.
SourceColorSpace
The color space of the backdrop the host hands in. egui renders gamma-encoded regardless of texture format (egui#3168), so its intermediate is GammaSrgb and must be decoded before the linear-light convolution. A host that renders linear uses Linear.

Functions§

is_supported_target
Whether the own-loop adapter supports compositing into format. The adapter renders egui’s gamma-encoded output (egui#3168) into an intermediate of the same format and decodes it in the blur shader; that model is only correct for non-sRGB Unorm targets. An *Srgb target would make the sampler decode once and the shader decode again (washed-out frost), so it is rejected at construction rather than silently mis-rendered.
strongest_repaint
The strongest repaint obligation across a set of surfaces: Live wins, then the shortest Bounded interval, else Static. OwnLoopRenderer::render_frame applies this to the egui Context itself; this is exposed for hosts that want to inspect the obligation directly.