Skip to main content

backdrop_blur_egui/
lib.rs

1//! `backdrop-blur-egui` — the egui adapter for frosted glass, over **two paths** sharing one
2//! [`Surface`] vocabulary.
3//!
4//! - **`grab-pass`** (the mainstream path: `eframe`-on-glow and the `cage` Wayland kiosk). The host
5//!   owns the GL loop; `GrabPassRenderer` rides an egui **paint callback** that grabs the live
6//!   framebuffer behind a surface, blurs it, and composites the frosted surface back. Build it once
7//!   from `eframe::CreationContext::gl`, call `.frost(ui, surface)` per frame, and `.destroy(gl)` in
8//!   `eframe::App::on_exit`. Pulls glow, never wgpu.
9//! - **`own-loop`** (default feature). For a host driving `egui-winit` + `egui-wgpu` directly (not
10//!   eframe), [`OwnLoopRenderer`] renders the UI into an offscreen intermediate, blurs a region of
11//!   it, and composites a frosted [`Surface`] over the display target — one encoder, one submit, in
12//!   the order that does not panic (DESIGN §6). Pulls the wgpu stack.
13//!
14//! Pick the path with a feature: a kiosk build is `--no-default-features --features grab-pass` and
15//! compiles neither wgpu nor egui-wgpu; an own-loop build is the default. The backends themselves are
16//! the separate `backdrop-blur-glow` / `backdrop-blur-wgpu` crates.
17//!
18//! The crate owns only a surface's *background*. The surface's content, foreground, and
19//! accessibility stay the host's: a frosted [`Surface`] is a post-render composite, never an egui
20//! widget, so it adds nothing to the AccessKit tree.
21//!
22//! # The three dials: blur, tint, opacity
23//!
24//! A frosted [`Surface`] mixes three **independent** knobs — conflating them is the most common
25//! "my glass looks wrong":
26//!
27//! - **[`BlurStrength`]** — the blur *radius* in logical points. How smeared the backdrop is.
28//!   `0` = no blur (a plain tinted pane).
29//! - **[`Tint`]** — the glass *film* painted over the blur, a linear-light color whose **alpha is
30//!   the film mix** (how much tint shows vs. how much blurred backdrop shows through). A *colored*
31//!   tint composites in color, not black — author it as sRGB with [`Tint::from_srgb_unmultiplied`]
32//!   so the linear decode is done for you. Alpha `0` = pure blur, no film; alpha `1` = the film is
33//!   opaque and the blur is invisible under it.
34//! - **[`Opacity`]** — the surface-global *presence* in `[0, 1]`, the whole frosted result blended
35//!   over the destination. This is the **fade dial**: drive it per frame to dissolve glass in/out.
36//!   Default `1.0`.
37//!
38//! Rule of thumb: blur sets the *texture*, tint-alpha sets the *material*, opacity sets the
39//! *presence*. A barely-tinted heavy blur is clear vibrancy; a high tint-alpha is frosted/opaque
40//! glass; opacity below `1` fades the entire thing.
41//!
42//! # Grab-pass contracts (read before calling `frost`)
43//!
44//! The grab-pass path samples the **live framebuffer** mid-frame, which makes draw order and fade
45//! load-bearing in ways the types cannot enforce:
46//!
47//! 1. **Enqueue the frost *before* the surface's foreground.** The callback grabs whatever is in the
48//!    framebuffer at its position — content drawn *before* it. Call `frost(ui, surface)` first, then
49//!    paint the surface's own content (text, controls) **after**, so the foreground lands on top of
50//!    the blur. Enqueue it too late and it grabs — and blurs away — your own content. There is no
51//!    runtime guard for this; it is a hard ordering contract.
52//! 2. **Fade with [`Opacity`], not `multiply_opacity`.** egui's `Ui::multiply_opacity` (and the
53//!    `Opacity` style) **do not reach paint callbacks** — the standard fade silently no-ops on the
54//!    blur. To dissolve frost in/out, drive the surface's `opacity` field ([`Opacity`]) per frame
55//!    instead. This is the one egui trap that bites everyone; the [`Opacity`] dial is the supported
56//!    escape hatch.
57//! 3. **A dynamically-sized rect needs *last frame's* rect.** In immediate mode the surface's rect
58//!    is only known *after* its content lays out, but the frost must be enqueued *before* the content
59//!    paints (contract 1) — a chicken-and-egg. The worked pattern: stash the rect in egui temp memory
60//!    keyed by an `Id`, frost **last frame's** rect at the top of this frame, then lay out the content
61//!    and write back the rect for next frame. It is stable while the surface is open; the only
62//!    artifact is one frame of staleness on a resize. (A first-class reserved-slot API that returns
63//!    the callback `Shape` for `painter.set()` is planned; until then this is the recommendation.)
64//! 4. **`GrabPassRenderer::took_effect` reports *ran*, not *composited*.** egui skips a
65//!    fully-clipped callback, so a frosted surface is not guaranteed to paint; `took_effect` lets the
66//!    host observe that the callback **fired**. It is set even when the region clipped to nothing or
67//!    the frost errored — it answers "did egui invoke my callback this frame", not "did pixels
68//!    change". Useful to confirm wiring; not a success signal.
69#![forbid(unsafe_code)]
70
71mod surface;
72
73#[cfg(feature = "own-loop")]
74mod own_loop;
75
76#[cfg(feature = "grab-pass")]
77mod grab_pass;
78
79// Neutral spine — available on both paths: the glass material vocabulary (used in `Surface`) and
80// the shared `Surface` type itself.
81pub use backdrop_blur_core::{
82    BlurStrength, CornerRadius, LinearRgba, Opacity, RepaintPolicy, Tint,
83};
84pub use surface::Surface;
85
86// Own-loop path re-exports: the wgpu backend (`render_frame` drives it), the egui-wgpu screen
87// descriptor (`FrameInput` carries it), and the renderer. Gated so a grab-pass-only build pulls
88// none of the wgpu stack.
89#[cfg(feature = "own-loop")]
90pub use backdrop_blur_wgpu::{SourceColorSpace, SourceView, WgpuBlur};
91#[cfg(feature = "own-loop")]
92pub use egui_wgpu::ScreenDescriptor;
93#[cfg(feature = "own-loop")]
94pub use own_loop::{FrameInput, OwnLoopRenderer, is_supported_target, strongest_repaint};
95
96// Grab-pass path: the eframe-on-glow adapter. Gated so an own-loop-only build pulls no glow/egui_glow.
97#[cfg(feature = "grab-pass")]
98pub use grab_pass::GrabPassRenderer;
99
100// Re-export the exact `glow` this crate's public API ([`GrabPassRenderer::new`]/`destroy`) is typed
101// against, so a consumer writes `backdrop_blur_egui::glow::Context` and is structurally pinned to the
102// same `glow` as the adapter. Without this a consumer picks its own `glow` version; a skew from the
103// one eframe hands back at `new` surfaces as a baffling "expected `glow::Context`, found
104// `glow::Context`" with no breadcrumb. Re-exporting the crate (the eframe-ecosystem norm) turns the
105// footgun into a compile-time guarantee.
106#[cfg(feature = "grab-pass")]
107pub use glow;