Skip to main content

ui/
material.rs

1//! [`material`] — the material-glass float: wraps a popover/dialog card so its
2//! ENTIRE subtree paints inside one scene layer (a single draw order).
3//!
4//! The single layer order is the point: with per-primitive bounds-tree
5//! ordering, a hover repaint elsewhere can reassign the card's quads relative
6//! to siblings — inside one layer the card's stacking is structural.
7//!
8//! The blur is painted first, structurally under the content: inside one layer
9//! the order is blur, then shadow, tint, border, rows, text. It needs
10//! `Window::paint_backdrop_blur` from our gpui fork (macOS Metal only);
11//! elsewhere the primitive is ignored and the glass reads as the theme's
12//! translucent tint over the OS window blur.
13
14use gpui::{
15    AbsoluteLength, AnyElement, App, Bounds, Corners, Element, GlobalElementId, InspectorElementId,
16    IntoElement, LayoutId, Pixels, Styled, Window, px,
17};
18
19use theme::Theme;
20
21/// Backdrop-blur sigma for floating menu/dialog glass — the reference
22/// `.glass-surface` runs `blur(44px)`, and the [`Theme::glass_overlay`] tint is
23/// thin enough that a 16px blur leaves backdrop detail ghosting through rows.
24pub const MENU_BLUR: f32 = 44.0;
25
26/// Backdrop-blur sigma for a small floating panel — a meter, a HUD. A sigma is
27/// only frost while the box is wide enough to show what it softened; at a
28/// quarter of the box's width the backdrop resolves to one flat tone.
29pub const PANEL_BLUR: f32 = 12.0;
30
31/// Frost a card whose rounding the caller states — the shape the popover layers
32/// need, where the card arrives already erased to an [`AnyElement`] and its
33/// radius belongs to the surface kind. Backdrop-blurred on glass, pass-through
34/// on opaque platforms.
35pub fn material(corner_radius: f32, blur_radius: f32, child: impl IntoElement) -> Material {
36    let radius = AbsoluteLength::from(px(corner_radius));
37    Material {
38        corners: Corners {
39            top_left: radius,
40            top_right: radius,
41            bottom_right: radius,
42            bottom_left: radius,
43        },
44        blur_radius,
45        child: child.into_any_element(),
46    }
47}
48
49/// Frost this card at the corner radius it already carries.
50///
51/// The radius comes off the element's own style, so a caller chaining its own
52/// rounding — `card.rounded(px(4.0)).material(MENU_BLUR)` — gets a blur cut to
53/// the corners it just asked for.
54pub trait Frosted: Styled + IntoElement + Sized {
55    fn material(mut self, blur_radius: f32) -> Material {
56        let square = AbsoluteLength::from(px(0.0));
57        let radii = &self.style().corner_radii;
58        let corners = Corners {
59            top_left: radii.top_left.unwrap_or(square),
60            top_right: radii.top_right.unwrap_or(square),
61            bottom_right: radii.bottom_right.unwrap_or(square),
62            bottom_left: radii.bottom_left.unwrap_or(square),
63        };
64        Material {
65            corners,
66            blur_radius,
67            child: self.into_any_element(),
68        }
69    }
70}
71
72impl<E: Styled + IntoElement> Frosted for E {}
73
74pub struct Material {
75    /// Held unresolved: a rem-rounded card only becomes pixels against the
76    /// window's rem size, which paint is the first place to have.
77    corners: Corners<AbsoluteLength>,
78    blur_radius: f32,
79    child: AnyElement,
80}
81
82impl Element for Material {
83    type RequestLayoutState = ();
84    type PrepaintState = ();
85
86    fn id(&self) -> Option<gpui::ElementId> {
87        None
88    }
89
90    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
91        None
92    }
93
94    fn request_layout(
95        &mut self,
96        _id: Option<&GlobalElementId>,
97        _inspector_id: Option<&InspectorElementId>,
98        window: &mut Window,
99        cx: &mut App,
100    ) -> (LayoutId, ()) {
101        (self.child.request_layout(window, cx), ())
102    }
103
104    fn prepaint(
105        &mut self,
106        _id: Option<&GlobalElementId>,
107        _inspector_id: Option<&InspectorElementId>,
108        _bounds: Bounds<Pixels>,
109        _request_layout: &mut Self::RequestLayoutState,
110        window: &mut Window,
111        cx: &mut App,
112    ) {
113        self.child.prepaint(window, cx);
114    }
115
116    fn paint(
117        &mut self,
118        _id: Option<&GlobalElementId>,
119        _inspector_id: Option<&InspectorElementId>,
120        bounds: Bounds<Pixels>,
121        _request_layout: &mut Self::RequestLayoutState,
122        _prepaint: &mut Self::PrepaintState,
123        window: &mut Window,
124        cx: &mut App,
125    ) {
126        if Theme::of(cx).is_glass() {
127            let rem = window.rem_size();
128            let corners = Corners {
129                top_left: self.corners.top_left.to_pixels(rem),
130                top_right: self.corners.top_right.to_pixels(rem),
131                bottom_right: self.corners.bottom_right.to_pixels(rem),
132                bottom_left: self.corners.bottom_left.to_pixels(rem),
133            };
134            window.paint_layer(bounds, |window| {
135                window.paint_backdrop_blur(bounds, corners, px(self.blur_radius));
136                self.child.paint(window, cx);
137            });
138        } else {
139            self.child.paint(window, cx);
140        }
141    }
142}
143
144impl IntoElement for Material {
145    type Element = Self;
146
147    fn into_element(self) -> Self::Element {
148        self
149    }
150}
151
152/// Paint `child` in its own scene layer, giving it a fresh draw order above
153/// everything painted so far in the enclosing layer.
154///
155/// Needed for overlays INSIDE a material card: the card's single layer means
156/// every primitive shares one draw order, and equal orders render grouped by
157/// primitive kind (quads, then icons, then images) — so a close button's
158/// circle painted "after" a thumbnail still shows up UNDER the image. A
159/// nested layer restores the intended stacking.
160pub fn layered(child: impl IntoElement) -> Layered {
161    Layered {
162        child: child.into_any_element(),
163    }
164}
165
166pub struct Layered {
167    child: AnyElement,
168}
169
170impl Element for Layered {
171    type RequestLayoutState = ();
172    type PrepaintState = ();
173
174    fn id(&self) -> Option<gpui::ElementId> {
175        None
176    }
177
178    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
179        None
180    }
181
182    fn request_layout(
183        &mut self,
184        _id: Option<&GlobalElementId>,
185        _inspector_id: Option<&InspectorElementId>,
186        window: &mut Window,
187        cx: &mut App,
188    ) -> (LayoutId, ()) {
189        (self.child.request_layout(window, cx), ())
190    }
191
192    fn prepaint(
193        &mut self,
194        _id: Option<&GlobalElementId>,
195        _inspector_id: Option<&InspectorElementId>,
196        _bounds: Bounds<Pixels>,
197        _request_layout: &mut Self::RequestLayoutState,
198        window: &mut Window,
199        cx: &mut App,
200    ) {
201        self.child.prepaint(window, cx);
202    }
203
204    fn paint(
205        &mut self,
206        _id: Option<&GlobalElementId>,
207        _inspector_id: Option<&InspectorElementId>,
208        bounds: Bounds<Pixels>,
209        _request_layout: &mut Self::RequestLayoutState,
210        _prepaint: &mut Self::PrepaintState,
211        window: &mut Window,
212        cx: &mut App,
213    ) {
214        window.paint_layer(bounds, |window| self.child.paint(window, cx));
215    }
216}
217
218impl IntoElement for Layered {
219    type Element = Self;
220
221    fn into_element(self) -> Self::Element {
222        self
223    }
224}