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 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),OwnLoopRendererrenders the UI into an offscreen intermediate, blurs a region of it, and composites a frostedSurfaceover 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 withTint::from_srgb_unmultipliedso 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; theOpacitydial 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.
Re-exports§
pub use glow;
Structs§
- Blur
Strength - Blur radius in logical points.
- Corner
Radius - 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 incrate::ResolvedMask::from_target). Non-negative by construction. - Frame
Input - One frame’s egui output plus where to draw it.
- Grab
Pass Renderer - 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 frostedSurface. - Linear
Rgba - 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 whatSelf::from_srgb_unmultiplieddoes, 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 fromTint’s alpha (which is the film mix, blur vs tint color) and fromBlurStrength(the radius). It scales the composite’s final blend weight:1.0is the surface fully composited (the default — every existing caller and golden is unchanged),0.0leaves 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. - OwnLoop
Renderer - Drives one own-loop frame for an
egui-winit+egui-wgpuhost: 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. - Screen
Descriptor - Information about the screen used for rendering.
- Source
View - The backdrop source: a sampleable view plus the two things a
wgpu::TextureViewcannot 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).
- Wgpu
Blur - 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§
- Repaint
Policy - How often the frosted surface’s backdrop must be re-grabbed and re-blurred. The adapter —
not core — drives the host’s
request_repaintfrom this; core only names the obligation. - Source
Color Space - The color space of the backdrop the host hands in. egui renders gamma-encoded regardless
of texture format (egui#3168), so its intermediate is
GammaSrgband must be decoded before the linear-light convolution. A host that renders linear usesLinear.
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-sRGBUnormtargets. An*Srgbtarget 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:
Livewins, then the shortestBoundedinterval, elseStatic.OwnLoopRenderer::render_frameapplies this to the eguiContextitself; this is exposed for hosts that want to inspect the obligation directly.