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, which the Metal and wgpu
12//! renderers carry and the DirectX one does not; see [`theme::LENSED`]. Where
13//! it is missing the card fills with [`theme::SurfaceSpec::flat`] instead.
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 — the DirectX renderer, or [`Theme::glass`] off — it paints
113 /// [`SurfaceSpec::flat`] 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 [`Glass::glass_effect`] will actually refract here, or fall back to
156/// [`SurfaceSpec::flat`]. Capability and choice both: the primitive is macOS
157/// Metal's and wgpu's, and components with glass off paint no lens.
158pub fn lensed(theme: &Theme) -> bool {
159 theme::LENSED && theme.glass
160}
161
162impl Surface {
163 /// Tint the glass — SwiftUI's `Glass.tint(_:)`, for a control important
164 /// enough to carry colour. Pass the colour at the coverage you want; it
165 /// stands in for [`Theme::glass_clear`]'s neutral lift rather than adding
166 /// to it, so a heavy alpha reads as paint and a light one as glass.
167 ///
168 /// A plain material's fill is still its caller's, and this does not
169 /// reach it.
170 pub fn tint(mut self, color: gpui::Hsla) -> Self {
171 self.tint = Some(color);
172 self
173 }
174
175 /// The card's rounding in pixels, which only a rem size resolves — clamped
176 /// to the box, since gpui reads a radius past half of it as a sharp corner.
177 fn corners(&self, bounds: Bounds<Pixels>, rem: Pixels) -> Corners<Pixels> {
178 Corners {
179 top_left: self.corners.top_left.to_pixels(rem),
180 top_right: self.corners.top_right.to_pixels(rem),
181 bottom_right: self.corners.bottom_right.to_pixels(rem),
182 bottom_left: self.corners.bottom_left.to_pixels(rem),
183 }
184 .clamp_radii_for_quad_size(bounds.size)
185 }
186}
187
188impl Element for Surface {
189 type RequestLayoutState = ();
190 type PrepaintState = ();
191
192 fn id(&self) -> Option<gpui::ElementId> {
193 None
194 }
195
196 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
197 None
198 }
199
200 fn request_layout(
201 &mut self,
202 _id: Option<&GlobalElementId>,
203 _inspector_id: Option<&InspectorElementId>,
204 window: &mut Window,
205 cx: &mut App,
206 ) -> (LayoutId, ()) {
207 (self.child.request_layout(window, cx), ())
208 }
209
210 fn prepaint(
211 &mut self,
212 _id: Option<&GlobalElementId>,
213 _inspector_id: Option<&InspectorElementId>,
214 bounds: Bounds<Pixels>,
215 _request_layout: &mut Self::RequestLayoutState,
216 window: &mut Window,
217 cx: &mut App,
218 ) {
219 crate::cover::record(bounds, window, cx);
220 self.child.prepaint(window, cx);
221 }
222
223 fn paint(
224 &mut self,
225 _id: Option<&GlobalElementId>,
226 _inspector_id: Option<&InspectorElementId>,
227 bounds: Bounds<Pixels>,
228 _request_layout: &mut Self::RequestLayoutState,
229 _prepaint: &mut Self::PrepaintState,
230 window: &mut Window,
231 cx: &mut App,
232 ) {
233 let theme = Theme::of(cx);
234 let glass = self.glass.tokens(theme);
235 let corners = self.corners(bounds, window.rem_size());
236 // The backdrop-blur primitive is macOS Metal's and wgpu's alone. The
237 // surface's fill lives inside it, so anywhere it will not run the fill
238 // is painted here — the tone the look settles on, at full coverage.
239 // The tint on its own is a coverage over a blur: painted flat it left
240 // the text behind a composer legible through it (user report).
241 if !lensed(theme) {
242 let tint = self.tint.unwrap_or(glass.spec.tint);
243 window.paint_quad(
244 fill(bounds, glass.spec.flat(tint).unwrap_or(tint)).corner_radii(corners),
245 );
246 }
247 if theme.glass {
248 let extent = f32::from(bounds.size.width.min(bounds.size.height));
249 let effect = gpui::GlassEffect {
250 blur_radius: px(glass.spec.blur),
251 // A length, not a share of the box — but two rims cannot meet
252 // in the middle of a small one.
253 lens: px(glass.spec.rim.min(extent / 2.0)),
254 // A length, like the lens: the measured drag is one curve of
255 // distance from the rim, the same on a 96pt box and a 320pt
256 // one. Held under half the box, where two reaches would cross.
257 reach: px(glass.spec.reach.min(extent / 2.0)),
258 gain: glass.spec.gain,
259 saturation: glass.spec.saturation,
260 magnify: glass.magnify,
261 dispersion: glass.dispersion,
262 tint: self.tint.unwrap_or(glass.spec.tint),
263 edge: glass.spec.edge,
264 edge_width: px(glass.spec.edge_width),
265 edge_aa: px(glass.spec.edge_aa),
266 };
267 window.paint_layer(bounds, |window| {
268 window.paint_backdrop_blur(bounds, corners, effect);
269 // After the blur, never before: the blur samples what is
270 // beneath it, so a shadow painted first is one the surface
271 // shows through itself. Cut to outside the shape, so it lands
272 // on the page and nowhere else.
273 if glass.spec.shadow {
274 window.paint_drop_shadows_outside(bounds, corners, &theme::surface_shadows());
275 }
276 self.child.paint(window, cx);
277 });
278 } else {
279 self.child.paint(window, cx);
280 }
281 }
282}
283
284impl IntoElement for Surface {
285 type Element = Self;
286
287 fn into_element(self) -> Self::Element {
288 self
289 }
290}
291
292/// Paint `child` in its own scene layer, giving it a fresh draw order above
293/// everything painted so far in the enclosing layer.
294///
295/// Needed for overlays INSIDE a material card: the card's single layer means
296/// every primitive shares one draw order, and equal orders render grouped by
297/// primitive kind (quads, then icons, then images) — so a close button's
298/// circle painted "after" a thumbnail still shows up UNDER the image. A
299/// nested layer restores the intended stacking.
300pub fn layered(child: impl IntoElement) -> Layered {
301 Layered {
302 child: child.into_any_element(),
303 }
304}
305
306pub struct Layered {
307 child: AnyElement,
308}
309
310impl Element for Layered {
311 type RequestLayoutState = ();
312 type PrepaintState = ();
313
314 fn id(&self) -> Option<gpui::ElementId> {
315 None
316 }
317
318 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
319 None
320 }
321
322 fn request_layout(
323 &mut self,
324 _id: Option<&GlobalElementId>,
325 _inspector_id: Option<&InspectorElementId>,
326 window: &mut Window,
327 cx: &mut App,
328 ) -> (LayoutId, ()) {
329 (self.child.request_layout(window, cx), ())
330 }
331
332 fn prepaint(
333 &mut self,
334 _id: Option<&GlobalElementId>,
335 _inspector_id: Option<&InspectorElementId>,
336 _bounds: Bounds<Pixels>,
337 _request_layout: &mut Self::RequestLayoutState,
338 window: &mut Window,
339 cx: &mut App,
340 ) {
341 self.child.prepaint(window, cx);
342 }
343
344 fn paint(
345 &mut self,
346 _id: Option<&GlobalElementId>,
347 _inspector_id: Option<&InspectorElementId>,
348 bounds: Bounds<Pixels>,
349 _request_layout: &mut Self::RequestLayoutState,
350 _prepaint: &mut Self::PrepaintState,
351 window: &mut Window,
352 cx: &mut App,
353 ) {
354 window.paint_layer(bounds, |window| self.child.paint(window, cx));
355 }
356}
357
358impl IntoElement for Layered {
359 type Element = Self;
360
361 fn into_element(self) -> Self::Element {
362 self
363 }
364}