Skip to main content

gpui_kit/overlay/
frost.rs

1//! A surface that shows what is behind it, out of focus.
2//!
3//! [`Frost`] is the backing a popover, a dialog or a rail is placed on when
4//! the window itself is translucent: the pixels underneath are blurred, and
5//! the surface colour is laid over the blur at `effect.glassAlpha` so the
6//! content on top keeps its contrast.
7//!
8//! # One layer, in one order
9//!
10//! The whole subtree paints inside a single scene layer, which is the reason
11//! this is an element and not a styled `div`. Paint order is per-primitive
12//! otherwise, so a repaint elsewhere in the frame can reorder the surface's
13//! own quads underneath the blur — a divider or a border is then snapshotted
14//! and blurred away, intermittently, in a way no test reproduces. Inside one
15//! layer the relationship is structural: blur first, fill and content after.
16//!
17//! # Where blur does not exist
18//!
19//! A backdrop blur is a renderer capability, not a paintable colour. Where
20//! the renderer has none the blur is dropped and the tinted fill is all that
21//! remains, which is a legible surface rather than a broken one — this is why
22//! the fill is painted whether or not the blur was. A theme that declares
23//! itself opaque by setting `effect.glassAlpha` to 1 takes the same path
24//! deliberately: there is nothing to see through, so nothing is blurred.
25
26use gpui::{
27    AnyElement, App, Bounds, Corners, Element, GlobalElementId, InspectorElementId, IntoElement,
28    LayoutId, ParentElement, Pixels, RenderOnce, Styled, Window, div, px,
29};
30use gpui_kit_semantics::{NodeSpec, Role, Semantic};
31use gpui_kit_theme::{ActiveTheme, Radius, Surface};
32
33use crate::foundation::Ident;
34
35/// A frosted-glass surface: blurred backdrop, tinted fill, caller's content.
36#[derive(IntoElement)]
37pub struct Frost {
38    ident: Ident,
39    surface: Surface,
40    radius: Radius,
41    blur: Option<f32>,
42    child: Option<AnyElement>,
43}
44
45impl std::fmt::Debug for Frost {
46    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        formatter
48            .debug_struct("Frost")
49            .field("ident", &self.ident)
50            .field("surface", &self.surface)
51            .field("radius", &self.radius)
52            .field("blur", &self.blur)
53            .field("has_child", &self.child.is_some())
54            .finish()
55    }
56}
57
58impl Frost {
59    pub fn new(ident: impl Into<Ident>) -> Self {
60        Self {
61            ident: ident.into(),
62            surface: Surface::Overlay,
63            radius: Radius::Card,
64            blur: None,
65            child: None,
66        }
67    }
68
69    /// Which surface colour is laid over the blur. The overlay surface is the
70    /// default because that is what a floating thing is made of.
71    pub fn surface(mut self, surface: Surface) -> Self {
72        self.surface = surface;
73        self
74    }
75
76    /// The rounding of the glass. It clips the blur as well as the fill, so a
77    /// caller rounding the card inside must say the same thing here or the
78    /// blur will show past the corners.
79    pub fn radius(mut self, radius: Radius) -> Self {
80        self.radius = radius;
81        self
82    }
83
84    /// How far the backdrop is blurred, in pixels, when `effect.glassBlur` is
85    /// not what this particular surface wants.
86    pub fn blur(mut self, blur: f32) -> Self {
87        self.blur = Some(blur.max(0.0));
88        self
89    }
90
91    pub fn child(mut self, child: impl IntoElement) -> Self {
92        self.child = Some(child.into_any_element());
93        self
94    }
95}
96
97impl RenderOnce for Frost {
98    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
99        let theme = cx.theme();
100        let radius = theme.radius(self.radius);
101        let alpha = theme.effects.glass_alpha.clamp(0.0, 1.0);
102        let fill = theme.surface(self.surface).opacity(alpha);
103        let blur = self.blur.unwrap_or(theme.effects.glass_blur);
104        let translucent = blurs(alpha, blur);
105
106        let glass = div()
107            .rounded(px(radius))
108            .bg(fill)
109            .children(self.child)
110            .semantic_in(cx, NodeSpec::new(self.ident.semantic_id(), Role::Region));
111
112        Glass {
113            radius: px(radius),
114            blur: px(blur),
115            translucent,
116            child: glass.into_any_element(),
117        }
118    }
119}
120
121/// Whether there is anything for a blur to show. Blurring what a fully opaque
122/// fill is about to cover costs a render pass and changes no pixel, and a
123/// radius of zero is a caller saying not to blur at all.
124fn blurs(alpha: f32, blur: f32) -> bool {
125    alpha < 1.0 && blur > 0.0
126}
127
128/// The single scene layer, with the backdrop blur painted first inside it.
129struct Glass {
130    radius: Pixels,
131    blur: Pixels,
132    translucent: bool,
133    child: AnyElement,
134}
135
136impl Element for Glass {
137    type RequestLayoutState = ();
138    type PrepaintState = ();
139
140    fn id(&self) -> Option<gpui::ElementId> {
141        None
142    }
143
144    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
145        None
146    }
147
148    fn request_layout(
149        &mut self,
150        _id: Option<&GlobalElementId>,
151        _inspector_id: Option<&InspectorElementId>,
152        window: &mut Window,
153        cx: &mut App,
154    ) -> (LayoutId, ()) {
155        (self.child.request_layout(window, cx), ())
156    }
157
158    fn prepaint(
159        &mut self,
160        _id: Option<&GlobalElementId>,
161        _inspector_id: Option<&InspectorElementId>,
162        _bounds: Bounds<Pixels>,
163        _request_layout: &mut Self::RequestLayoutState,
164        window: &mut Window,
165        cx: &mut App,
166    ) {
167        self.child.prepaint(window, cx);
168    }
169
170    fn paint(
171        &mut self,
172        _id: Option<&GlobalElementId>,
173        _inspector_id: Option<&InspectorElementId>,
174        bounds: Bounds<Pixels>,
175        _request_layout: &mut Self::RequestLayoutState,
176        _prepaint: &mut Self::PrepaintState,
177        window: &mut Window,
178        cx: &mut App,
179    ) {
180        if !self.translucent {
181            self.child.paint(window, cx);
182            return;
183        }
184        window.paint_layer(bounds, |window| {
185            window.paint_backdrop_blur(bounds, Corners::all(self.radius), self.blur);
186            self.child.paint(window, cx);
187        });
188    }
189}
190
191impl IntoElement for Glass {
192    type Element = Self;
193
194    fn into_element(self) -> Self::Element {
195        self
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn an_opaque_theme_blurs_nothing() {
205        assert!(blurs(0.72, 24.0));
206        assert!(!blurs(1.0, 24.0), "an opaque fill hides what it blurred");
207        assert!(!blurs(0.72, 0.0), "no radius is no blur");
208    }
209}