Skip to main content

ui/
surface.rs

1//! [`crate::surface`] — the backdrop surface a floating card sits on: wraps a
2//! popover/dialog card so its ENTIRE subtree paints inside one scene layer (a
3//! single draw order).
4//!
5//! The single layer order is the point: with per-primitive bounds-tree
6//! ordering, a hover repaint elsewhere can reassign the card's quads relative
7//! to siblings — inside one layer the card's stacking is structural.
8//!
9//! The blur is painted first, structurally under the content: inside one layer
10//! the order is blur, then shadow, tint, border, rows, text. It needs
11//! `Window::paint_backdrop_blur` from our gpui fork (macOS Metal only);
12//! elsewhere the primitive is ignored and the glass reads as the theme's
13//! translucent tint over the OS window blur.
14//!
15//! Material and glass are different things — a material has thickness, a glass
16//! has a variant — and they meet only at the numbers they resolve to, which is
17//! why one element paints both and [`theme::SurfaceStyle`] names which.
18
19use gpui::{
20    AbsoluteLength, AnyElement, App, Bounds, Corners, Element, GlobalElementId, InspectorElementId,
21    IntoElement, LayoutId, Pixels, Styled, Window, fill, px,
22};
23
24use theme::{SurfaceSpec, SurfaceStyle, Theme};
25
26/// Paint an already-composed card as a surface, at the rounding the caller
27/// states — the shape the popover layers need, where the card arrives erased to
28/// an [`AnyElement`] and cannot be chained onto. The look comes off whichever
29/// theme paints it; a caller wanting its own hands one over through
30/// [`Glass::glass_effect`].
31pub fn of(corner_radius: f32, style: SurfaceStyle, child: impl IntoElement) -> Surface {
32    Surface {
33        corners: uniform(corner_radius),
34        glass: Look::Shipped(style),
35        tint: None,
36        child: child.into_any_element(),
37    }
38}
39
40/// The same, on [`Theme::menu_style`] — what the popover layers mount on, so an
41/// app moves every menu between frost and glass by moving one token.
42pub fn popover(corner_radius: f32, child: impl IntoElement) -> Surface {
43    Surface {
44        corners: uniform(corner_radius),
45        glass: Look::Popover,
46        tint: None,
47        child: child.into_any_element(),
48    }
49}
50
51/// One radius on all four corners.
52fn uniform(radius: f32) -> Corners<AbsoluteLength> {
53    let radius = AbsoluteLength::from(px(radius));
54    Corners {
55        top_left: radius,
56        top_right: radius,
57        bottom_right: radius,
58        bottom_left: radius,
59    }
60}
61
62/// Where a [`Material`]'s numbers come from. The popover layers carry no theme
63/// of their own, so they name a look and it resolves against whichever theme
64/// paints them; a caller tuning its own glass hands the numbers over instead.
65#[derive(Clone, Copy)]
66enum Look {
67    /// Whatever [`Theme::popover_surface`] says — the popover layers' choice.
68    Popover,
69    Shipped(SurfaceStyle),
70    Tuned(Tokens),
71}
72
73impl Look {
74    fn tokens(self, theme: &Theme) -> Tokens {
75        match self {
76            Look::Popover => Tokens::of(theme, theme.popover_surface),
77            Look::Shipped(style) => Tokens::of(theme, style),
78            Look::Tuned(tokens) => tokens,
79        }
80    }
81}
82
83/// The glass numbers, read off the theme the way every other widget reads its
84/// colours. Not parameters: a caller who wants different glass hands over a
85/// different theme.
86#[derive(Clone, Copy)]
87struct Tokens {
88    spec: SurfaceSpec,
89    magnify: f32,
90    dispersion: f32,
91}
92
93impl Tokens {
94    fn of(theme: &Theme, style: SurfaceStyle) -> Self {
95        Self {
96            spec: style.spec(theme),
97            magnify: theme.glass_magnify,
98            dispersion: theme.glass_dispersion,
99        }
100    }
101}
102
103/// The backdrop surfaces, on any element carrying a corner radius.
104pub trait Surfaced: Styled + IntoElement + Sized {
105    /// Paint this card as liquid glass — SwiftUI's `Glass.clear`: a refracting
106    /// bevel at the rim, a lit edge, and [`Theme::glass_clear`]'s dimming.
107    ///
108    /// Blur, bevel and tint all come off [`Theme`], and the shape off the
109    /// card's own rounding: the lens dies if any of the three is wrong.
110    ///
111    /// It clears the card's `bg`, since the lens paints the fill. Where the
112    /// lens cannot run — every renderer but macOS Metal — it paints the frosted
113    /// tint instead, so the surface is never invisible.
114    fn surface(mut self, theme: &Theme, style: SurfaceStyle) -> Surface {
115        let corners = corners_of(&mut self);
116        // The lens paints the fill, so the card's own is dropped here rather
117        // than at the call site: painting both is what buries the lens, and a
118        // caller who has to remember that is a caller who will forget.
119        self.style().background = None;
120        Surface {
121            corners,
122            glass: Look::Tuned(Tokens::of(theme, style)),
123            tint: None,
124            child: self.into_any_element(),
125        }
126    }
127}
128
129/// The rounding an element already carries, as the blur needs it.
130fn corners_of(styled: &mut impl Styled) -> Corners<AbsoluteLength> {
131    let square = AbsoluteLength::from(px(0.0));
132    let radii = &styled.style().corner_radii;
133    Corners {
134        top_left: radii.top_left.unwrap_or(square),
135        top_right: radii.top_right.unwrap_or(square),
136        bottom_right: radii.bottom_right.unwrap_or(square),
137        bottom_left: radii.bottom_left.unwrap_or(square),
138    }
139}
140
141impl<E: Styled + IntoElement> Surfaced for E {}
142
143pub struct Surface {
144    /// The glass's own tint, when the caller wants one. `None` is
145    /// [`Theme::glass_clear`]'s neutral lift.
146    tint: Option<gpui::Hsla>,
147    /// Held unresolved: a rem-rounded card only becomes pixels against the
148    /// window's rem size, which paint is the first place to have.
149    corners: Corners<AbsoluteLength>,
150    /// Which look to paint, and where its numbers come from.
151    glass: Look,
152    child: AnyElement,
153}
154
155/// Whether this build has the backdrop-blur primitive behind it. Metal reads it
156/// off the scene, and so does wgpu now that the fork implements it there —
157/// which is why this tracks the gpui in use rather than the platform alone.
158const LENSED: bool = cfg!(any(target_os = "macos", target_family = "wasm"));
159
160/// Whether [`Glass::glass_effect`] will actually refract here, or fall back to
161/// the flat backdrop tint. Capability and choice both: the primitive is macOS
162/// Metal's and wgpu's, and components with glass off paint no lens.
163pub fn lensed(theme: &Theme) -> bool {
164    LENSED && theme.glass
165}
166
167impl Surface {
168    /// Tint the glass — SwiftUI's `Glass.tint(_:)`, for a control important
169    /// enough to carry colour. Pass the colour at the coverage you want; it
170    /// stands in for [`Theme::glass_clear`]'s neutral lift rather than adding
171    /// to it, so a heavy alpha reads as paint and a light one as glass.
172    ///
173    /// A plain material's fill is still its caller's, and this does not
174    /// reach it.
175    pub fn tint(mut self, color: gpui::Hsla) -> Self {
176        self.tint = Some(color);
177        self
178    }
179
180    /// The card's rounding in pixels, which only a rem size resolves — clamped
181    /// to the box, since gpui reads a radius past half of it as a sharp corner.
182    fn corners(&self, bounds: Bounds<Pixels>, rem: Pixels) -> Corners<Pixels> {
183        Corners {
184            top_left: self.corners.top_left.to_pixels(rem),
185            top_right: self.corners.top_right.to_pixels(rem),
186            bottom_right: self.corners.bottom_right.to_pixels(rem),
187            bottom_left: self.corners.bottom_left.to_pixels(rem),
188        }
189        .clamp_radii_for_quad_size(bounds.size)
190    }
191}
192
193impl Element for Surface {
194    type RequestLayoutState = ();
195    type PrepaintState = ();
196
197    fn id(&self) -> Option<gpui::ElementId> {
198        None
199    }
200
201    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
202        None
203    }
204
205    fn request_layout(
206        &mut self,
207        _id: Option<&GlobalElementId>,
208        _inspector_id: Option<&InspectorElementId>,
209        window: &mut Window,
210        cx: &mut App,
211    ) -> (LayoutId, ()) {
212        (self.child.request_layout(window, cx), ())
213    }
214
215    fn prepaint(
216        &mut self,
217        _id: Option<&GlobalElementId>,
218        _inspector_id: Option<&InspectorElementId>,
219        _bounds: Bounds<Pixels>,
220        _request_layout: &mut Self::RequestLayoutState,
221        window: &mut Window,
222        cx: &mut App,
223    ) {
224        self.child.prepaint(window, cx);
225    }
226
227    fn paint(
228        &mut self,
229        _id: Option<&GlobalElementId>,
230        _inspector_id: Option<&InspectorElementId>,
231        bounds: Bounds<Pixels>,
232        _request_layout: &mut Self::RequestLayoutState,
233        _prepaint: &mut Self::PrepaintState,
234        window: &mut Window,
235        cx: &mut App,
236    ) {
237        let theme = Theme::of(cx);
238        let glass = self.glass.tokens(theme);
239        let corners = self.corners(bounds, window.rem_size());
240        // The backdrop-blur primitive is macOS Metal's and wgpu's alone. The
241        // surface's fill lives inside it, so anywhere it will not run the fill
242        // is painted here — the look's own tint, so the card degrades to a
243        // surface with the page showing through rather than to an opaque slab.
244        if !lensed(theme) {
245            let tint = self.tint.unwrap_or(glass.spec.tint);
246            window.paint_quad(fill(bounds, tint).corner_radii(corners));
247        }
248        if theme.glass {
249            let extent = f32::from(bounds.size.width.min(bounds.size.height));
250            let effect = gpui::GlassEffect {
251                blur_radius: px(glass.spec.blur),
252                // A length, not a share of the box — but two rims cannot meet
253                // in the middle of a small one.
254                lens: px(glass.spec.rim.min(extent / 2.0)),
255                // A length, like the lens: the measured drag is one curve of
256                // distance from the rim, the same on a 96pt box and a 320pt
257                // one. Held under half the box, where two reaches would cross.
258                reach: px(glass.spec.reach.min(extent / 2.0)),
259                gain: glass.spec.gain,
260                saturation: glass.spec.saturation,
261                magnify: glass.magnify,
262                dispersion: glass.dispersion,
263                tint: self.tint.unwrap_or(glass.spec.tint),
264                edge: glass.spec.edge,
265                edge_width: px(glass.spec.edge_width),
266                edge_aa: px(glass.spec.edge_aa),
267            };
268            window.paint_layer(bounds, |window| {
269                window.paint_backdrop_blur(bounds, corners, effect);
270                // After the blur, never before: the blur samples what is
271                // beneath it, so a shadow painted first is one the surface
272                // shows through itself. Cut to outside the shape, so it lands
273                // on the page and nowhere else.
274                if glass.spec.shadow {
275                    window.paint_drop_shadows_outside(bounds, corners, &theme::surface_shadows());
276                }
277                self.child.paint(window, cx);
278            });
279        } else {
280            self.child.paint(window, cx);
281        }
282    }
283}
284
285impl IntoElement for Surface {
286    type Element = Self;
287
288    fn into_element(self) -> Self::Element {
289        self
290    }
291}
292
293/// Paint `child` in its own scene layer, giving it a fresh draw order above
294/// everything painted so far in the enclosing layer.
295///
296/// Needed for overlays INSIDE a material card: the card's single layer means
297/// every primitive shares one draw order, and equal orders render grouped by
298/// primitive kind (quads, then icons, then images) — so a close button's
299/// circle painted "after" a thumbnail still shows up UNDER the image. A
300/// nested layer restores the intended stacking.
301pub fn layered(child: impl IntoElement) -> Layered {
302    Layered {
303        child: child.into_any_element(),
304    }
305}
306
307pub struct Layered {
308    child: AnyElement,
309}
310
311impl Element for Layered {
312    type RequestLayoutState = ();
313    type PrepaintState = ();
314
315    fn id(&self) -> Option<gpui::ElementId> {
316        None
317    }
318
319    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
320        None
321    }
322
323    fn request_layout(
324        &mut self,
325        _id: Option<&GlobalElementId>,
326        _inspector_id: Option<&InspectorElementId>,
327        window: &mut Window,
328        cx: &mut App,
329    ) -> (LayoutId, ()) {
330        (self.child.request_layout(window, cx), ())
331    }
332
333    fn prepaint(
334        &mut self,
335        _id: Option<&GlobalElementId>,
336        _inspector_id: Option<&InspectorElementId>,
337        _bounds: Bounds<Pixels>,
338        _request_layout: &mut Self::RequestLayoutState,
339        window: &mut Window,
340        cx: &mut App,
341    ) {
342        self.child.prepaint(window, cx);
343    }
344
345    fn paint(
346        &mut self,
347        _id: Option<&GlobalElementId>,
348        _inspector_id: Option<&InspectorElementId>,
349        bounds: Bounds<Pixels>,
350        _request_layout: &mut Self::RequestLayoutState,
351        _prepaint: &mut Self::PrepaintState,
352        window: &mut Window,
353        cx: &mut App,
354    ) {
355        window.paint_layer(bounds, |window| self.child.paint(window, cx));
356    }
357}
358
359impl IntoElement for Layered {
360    type Element = Self;
361
362    fn into_element(self) -> Self::Element {
363        self
364    }
365}