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 thecageWayland kiosk). The host owns the GL loop;GrabPassRendererrides an egui paint callback that grabs the live framebuffer behind a surface, blurs it, and composites the frosted surface back. Build it once fromeframe::CreationContext::gl, call.frost(ui, surface)per frame, and.destroy(gl)ineframe::App::on_exit. Pulls glow, never wgpu.own-loop(default feature). For a host drivingegui-winit+egui-wgpudirectly (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. Alpha0= pure blur, no film; alpha1= 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. Default1.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:
- 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. - Fade with [
Opacity], notmultiply_opacity. egui'sUi::multiply_opacity(and theOpacitystyle) do not reach paint callbacks — the standard fade silently no-ops on the blur. To dissolve frost in/out, drive the surface'sopacityfield ([Opacity]) per frame instead. This is the one egui trap that bites everyone; the [Opacity] dial is the supported escape hatch. - 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 callbackShapeforpainter.set()is planned; until then this is the recommendation.) GrabPassRenderer::took_effectreports ran, not composited. egui skips a fully-clipped callback, so a frosted surface is not guaranteed to paint;took_effectlets 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.