Skip to main content

theme/theme/
glass.rs

1//! Glass recipes: translucent chrome over the blurred window background, and
2//! the modal scrim.
3
4use gpui::{Hsla, WindowBackgroundAppearance, hsla};
5
6use crate::{Appearance, color, paint, theme::Theme};
7
8impl Theme {
9    /// The frost tint painted over the blurred window background (macOS glass):
10    /// [`Self::vibrancy_tone`] at [`Brand::vibrancy_alpha`](crate::Brand::vibrancy_alpha).
11    /// Opaque, this IS the surface tone.
12    pub fn vibrancy_tint(&self) -> Hsla {
13        let alpha = self.vibrancy_alpha;
14        if alpha >= 1.0 {
15            return self.surface;
16        }
17        self.vibrancy_tone.opacity(alpha)
18    }
19
20    /// The app's root fill: the frost where glass is on, the opaque panel
21    /// where it is not. What a root element paints instead of
22    /// [`Self::bg`](Self#structfield.bg), so a window that opens blurred is not
23    /// then covered over by the paint that made the blur pointless.
24    pub fn window_bg(&self) -> Hsla {
25        if self.vibrancy {
26            self.vibrancy_tint()
27        } else {
28            self.bg
29        }
30    }
31
32    /// What a content card paints over [`Self::sidebar_bg`]:
33    /// [`Self::content_background`] at [`Self::content_opacity`] where
34    /// [`Self::vibrancy`] is on, and opaque [`Self::bg`](Self#structfield.bg)
35    /// where it is not.
36    pub fn content_bg(&self) -> Hsla {
37        match self.vibrancy {
38            true => self.content_background.opacity(self.content_opacity),
39            false => self.bg,
40        }
41    }
42
43    /// A sidebar's fill: the frost where glass is on, [`Self::surface`] where
44    /// it is not.
45    pub fn sidebar_bg(&self) -> Hsla {
46        if self.vibrancy {
47            self.vibrancy_tint()
48        } else {
49            self.surface
50        }
51    }
52
53    /// The translucent tint floating cards paint over their backdrop blur
54    /// (see `bezel::material`). Dark: the reference `.glass-surface` menu tint
55    /// verbatim — `oklch(0.33 0 0 / 34%)`. Light: heavier, since white over a
56    /// bright backdrop separates a row from it by less.
57    ///
58    /// Some callers paint this *over* a [`Surface`](crate::SurfaceStyle) —
59    /// `control_bar` does — so coverage here hides the lens under it.
60    pub fn glass_overlay(&self) -> Hsla {
61        match self.appearance {
62            Appearance::Dark => color::oklch(0.33, 0.0, 0.0).opacity(0.34),
63            Appearance::Light => self.surface_overlay.opacity(0.55),
64        }
65    }
66
67    /// The composer pill / question panel fill. Light's `input_bg` is opaque
68    /// white (the elevation ladder on an opaque page) — over glass it read as
69    /// a solid slab in front of the blurred backdrop, so it thins to a
70    /// translucent tint there (0.6 and then 0.45 both still read too bright
71    /// over the 0.80 frost — lowered on user request). Dark's 3% white wash
72    /// is already glass-native.
73    pub fn input_glass_bg(&self) -> Hsla {
74        if self.glass && matches!(self.appearance, Appearance::Light) {
75            self.input_bg.opacity(0.30)
76        } else {
77            self.input_bg
78        }
79    }
80
81    /// Section-card fill — the group box, and the in-panel cards built like it.
82    ///
83    /// Each appearance plates in the direction it has room in: dark lifts on a
84    /// white wash (`../desktop`'s `--color-card`), light lands a white card
85    /// over whatever is beneath it. An opaque platform has no frost beneath the
86    /// card, so it takes the grey below its white page.
87    pub fn card_glass_bg(&self) -> Hsla {
88        if !self.glass {
89            return self.surface;
90        }
91        match self.appearance {
92            Appearance::Dark => hsla(0.0, 0.0, 1.0, 0.06),
93            Appearance::Light => self.surface_card.opacity(0.60),
94        }
95    }
96
97    /// The standard modal backdrop — see [`scrim`](crate::paint::scrim).
98    pub fn scrim(&self) -> Hsla {
99        paint::scrim_for(self.appearance, paint::SCRIM_ALPHA_DARK)
100    }
101
102    /// How the platform should composite the window behind our paint.
103    ///
104    /// Windows takes Mica, which is DWM's own backdrop; `Blurred` there is the
105    /// undocumented accent API's acrylic at a zero tint, which reads as plain
106    /// transparency. Mica tints from the desktop wallpaper rather than from
107    /// the windows behind, so what it carries is not what vibrancy carries.
108    ///
109    /// This is a method rather than a constant because it has to be *re-applied* after
110    /// every theme swap: gpui's macOS backend tears the `NSVisualEffectView`
111    /// out of the hierarchy whenever the value is anything but `Blurred`, and
112    /// the re-apply in `appearance::apply` is what restores vibrancy when the
113    /// user switches back to dark. See zed's `crates/zed/src/main.rs`, which
114    /// runs the same loop on every settings change.
115    pub fn window_background_appearance(&self) -> WindowBackgroundAppearance {
116        match (self.vibrancy, cfg!(target_os = "windows")) {
117            (false, _) => WindowBackgroundAppearance::Opaque,
118            (true, true) => WindowBackgroundAppearance::MicaBackdrop,
119            (true, false) => WindowBackgroundAppearance::Blurred,
120        }
121    }
122}