backdrop-blur-egui 0.1.0

egui adapter for backdrop-blur: frosted glass over two paths sharing one Surface vocabulary — own-loop (egui-wgpu, render to an intermediate and composite) and grab-pass (eframe-on-glow, grab the live framebuffer in a paint callback, blur, composite).
Documentation

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.