gpui: Add support for window transparency & blur on macOS (#9610)

jansol , Luiz Marcondes , and Marshall Bowers created

This PR adds support for transparent and blurred window backgrounds on
macOS.

Release Notes:

- Added support for transparent and blurred window backgrounds on macOS
([#5040](https://github.com/zed-industries/zed/issues/5040)).
- This requires themes to specify a new `background.appearance` key
("opaque", "transparent" or "blurred") and to include an alpha value in
colors that should be transparent.

<img width="913" alt="image"
src="https://github.com/zed-industries/zed/assets/2588851/7547ee2a-e376-4d55-9114-e6fc2f5110bc">
<img width="994" alt="image"
src="https://github.com/zed-industries/zed/assets/2588851/b36fbc14-6e4d-4140-9448-69cad803c45a">
<img width="1020" alt="image"
src="https://github.com/zed-industries/zed/assets/2588851/d70e2005-54fd-4991-a211-ed484ccf26ef">

---------

Co-authored-by: Luiz Marcondes <luizgustavodevergennes@gmail.com>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

crates/collab_ui/src/collab_ui.rs                |  5 +
crates/gpui/examples/window_positioning.rs       |  1 
crates/gpui/src/platform.rs                      | 28 +++++++++++
crates/gpui/src/platform/linux/wayland/window.rs |  6 ++
crates/gpui/src/platform/linux/x11/window.rs     |  6 ++
crates/gpui/src/platform/mac/metal_renderer.rs   |  2 
crates/gpui/src/platform/mac/window.rs           | 45 ++++++++++++++++-
crates/gpui/src/platform/test/window.rs          |  6 ++
crates/gpui/src/platform/windows/window.rs       |  4 +
crates/gpui/src/window.rs                        | 15 ++++-
crates/theme/src/default_theme.rs                |  4 +
crates/theme/src/one_themes.rs                   |  4 
crates/theme/src/registry.rs                     |  8 +++
crates/theme/src/schema.rs                       | 24 +++++++++
crates/theme/src/settings.rs                     |  6 ++
crates/theme/src/styles/colors.rs                |  4 +
crates/theme/src/theme.rs                        | 10 +++
crates/theme_importer/src/vscode/converter.rs    |  1 
crates/zed/src/main.rs                           |  9 +++
crates/zed/src/zed.rs                            |  2 
20 files changed, 173 insertions(+), 17 deletions(-)

Detailed changes

crates/collab_ui/src/collab_ui.rs 🔗

@@ -13,8 +13,8 @@ use call::{report_call_event_for_room, ActiveCall};
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
 use gpui::{
-    actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task, WindowContext,
-    WindowKind, WindowOptions,
+    actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task,
+    WindowBackgroundAppearance, WindowContext, WindowKind, WindowOptions,
 };
 use panel_settings::MessageEditorSettings;
 pub use panel_settings::{
@@ -121,5 +121,6 @@ fn notification_window_options(
         is_movable: false,
         display_id: Some(screen.id()),
         fullscreen: false,
+        window_background: WindowBackgroundAppearance::default(),
     }
 }

crates/gpui/examples/window_positioning.rs 🔗

@@ -48,6 +48,7 @@ fn main() {
                     display_id: Some(screen.id()),
 
                     titlebar: None,
+                    window_background: WindowBackgroundAppearance::default(),
                     focus: false,
                     show: true,
                     kind: WindowKind::PopUp,

crates/gpui/src/platform.rs 🔗

@@ -190,6 +190,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn activate(&self);
     fn is_active(&self) -> bool;
     fn set_title(&mut self, title: &str);
+    fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
     fn set_edited(&mut self, edited: bool);
     fn show_character_palette(&self);
     fn minimize(&self);
@@ -533,6 +534,9 @@ pub struct WindowOptions {
     /// The display to create the window on, if this is None,
     /// the window will be created on the main display
     pub display_id: Option<DisplayId>,
+
+    /// The appearance of the window background.
+    pub window_background: WindowBackgroundAppearance,
 }
 
 /// The variables that can be configured when creating a new window
@@ -555,6 +559,8 @@ pub(crate) struct WindowParams {
     pub show: bool,
 
     pub display_id: Option<DisplayId>,
+
+    pub window_background: WindowBackgroundAppearance,
 }
 
 impl Default for WindowOptions {
@@ -572,6 +578,7 @@ impl Default for WindowOptions {
             is_movable: true,
             display_id: None,
             fullscreen: false,
+            window_background: WindowBackgroundAppearance::default(),
         }
     }
 }
@@ -633,6 +640,27 @@ impl Default for WindowAppearance {
     }
 }
 
+/// The appearance of the background of the window itself, when there is
+/// no content or the content is transparent.
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
+pub enum WindowBackgroundAppearance {
+    /// Opaque.
+    ///
+    /// This lets the window manager know that content behind this
+    /// window does not need to be drawn.
+    ///
+    /// Actual color depends on the system and themes should define a fully
+    /// opaque background color instead.
+    #[default]
+    Opaque,
+    /// Plain alpha transparency.
+    Transparent,
+    /// Transparency, but the contents behind the window are blurred.
+    ///
+    /// Not always supported.
+    Blurred,
+}
+
 /// The options that can be configured for a file dialog prompt
 #[derive(Copy, Clone, Debug)]
 pub struct PathPromptOptions {

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -23,7 +23,7 @@ use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
 use crate::scene::Scene;
 use crate::{
     px, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformDisplay, PlatformInput, Point,
-    PromptLevel, Size, WindowAppearance, WindowParams,
+    PromptLevel, Size, WindowAppearance, WindowBackgroundAppearance, WindowParams,
 };
 
 #[derive(Default)]
@@ -355,6 +355,10 @@ impl PlatformWindow for WaylandWindow {
         self.0.toplevel.set_title(title.to_string());
     }
 
+    fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
+        // todo(linux)
+    }
+
     fn set_edited(&mut self, edited: bool) {
         // todo(linux)
     }

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -4,7 +4,7 @@
 use crate::{
     platform::blade::BladeRenderer, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformAtlas,
     PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
-    Scene, Size, WindowAppearance, WindowOptions, WindowParams,
+    Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams,
 };
 use blade_graphics as gpu;
 use parking_lot::Mutex;
@@ -423,6 +423,10 @@ impl PlatformWindow for X11Window {
     // todo(linux)
     fn set_edited(&mut self, edited: bool) {}
 
+    fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
+        // todo(linux)
+    }
+
     // todo(linux), this corresponds to `orderFrontCharacterPalette` on macOS,
     // but it looks like the equivalent for Linux is GTK specific:
     //

crates/gpui/src/platform/mac/metal_renderer.rs 🔗

@@ -73,7 +73,7 @@ impl MetalRenderer {
         let layer = metal::MetalLayer::new();
         layer.set_device(&device);
         layer.set_pixel_format(MTLPixelFormat::RGBA8Unorm);
-        layer.set_opaque(true);
+        layer.set_opaque(false);
         layer.set_maximum_drawable_count(3);
         unsafe {
             let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];

crates/gpui/src/platform/mac/window.rs 🔗

@@ -4,12 +4,12 @@ use crate::{
     DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke,
     Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
     Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel,
-    Size, Timer, WindowAppearance, WindowKind, WindowParams,
+    Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowKind, WindowParams,
 };
 use block::ConcreteBlock;
 use cocoa::{
     appkit::{
-        CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
+        CGPoint, NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags,
         NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
         NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
         NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility,
@@ -83,6 +83,17 @@ const NSDragOperationNone: NSDragOperation = 0;
 #[allow(non_upper_case_globals)]
 const NSDragOperationCopy: NSDragOperation = 1;
 
+#[link(name = "CoreGraphics", kind = "framework")]
+extern "C" {
+    // Widely used private APIs; Apple uses them for their Terminal.app.
+    fn CGSMainConnectionID() -> id;
+    fn CGSSetWindowBackgroundBlurRadius(
+        connection_id: id,
+        window_id: NSInteger,
+        radius: i64,
+    ) -> i32;
+}
+
 #[ctor]
 unsafe fn build_classes() {
     WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
@@ -509,6 +520,7 @@ impl MacWindow {
     pub fn open(
         handle: AnyWindowHandle,
         WindowParams {
+            window_background,
             bounds,
             titlebar,
             kind,
@@ -606,7 +618,7 @@ impl MacWindow {
                 )
             };
 
-            let window = Self(Arc::new(Mutex::new(MacWindowState {
+            let mut window = Self(Arc::new(Mutex::new(MacWindowState {
                 handle,
                 executor,
                 native_window,
@@ -685,6 +697,8 @@ impl MacWindow {
             native_window.setContentView_(native_view.autorelease());
             native_window.makeFirstResponder_(native_view);
 
+            window.set_background_appearance(window_background);
+
             match kind {
                 WindowKind::Normal => {
                     native_window.setLevel_(NSNormalWindowLevel);
@@ -967,6 +981,31 @@ impl PlatformWindow for MacWindow {
         }
     }
 
+    fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
+        let this = self.0.as_ref().lock();
+        let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
+            80
+        } else {
+            0
+        };
+        let opaque = if background_appearance == WindowBackgroundAppearance::Opaque {
+            YES
+        } else {
+            NO
+        };
+        unsafe {
+            this.native_window.setOpaque_(opaque);
+            let clear_color = if opaque == YES {
+                NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
+            } else {
+                NSColor::clearColor(nil)
+            };
+            this.native_window.setBackgroundColor_(clear_color);
+            let window_number = this.native_window.windowNumber();
+            CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
+        }
+    }
+
     fn set_edited(&mut self, edited: bool) {
         unsafe {
             let window = self.0.lock().native_window;

crates/gpui/src/platform/test/window.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels,
     DispatchEventResult, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
     PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance,
-    WindowParams,
+    WindowBackgroundAppearance, WindowParams,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -190,6 +190,10 @@ impl PlatformWindow for TestWindow {
         self.0.lock().title = Some(title.to_owned());
     }
 
+    fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) {
+        unimplemented!()
+    }
+
     fn set_edited(&mut self, edited: bool) {
         self.0.lock().edited = edited;
     }

crates/gpui/src/platform/windows/window.rs 🔗

@@ -1468,6 +1468,10 @@ impl PlatformWindow for WindowsWindow {
             .ok();
     }
 
+    fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
+        // todo(windows)
+    }
+
     // todo(windows)
     fn set_edited(&mut self, _edited: bool) {}
 

crates/gpui/src/window.rs 🔗

@@ -7,8 +7,8 @@ use crate::{
     Modifiers, ModifiersChangedEvent, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels,
     PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Render,
     ScaledPixels, SharedString, Size, SubscriberSet, Subscription, TaffyLayoutEngine, Task,
-    TextStyle, TextStyleRefinement, View, VisualContext, WeakView, WindowAppearance, WindowOptions,
-    WindowParams, WindowTextSystem,
+    TextStyle, TextStyleRefinement, View, VisualContext, WeakView, WindowAppearance,
+    WindowBackgroundAppearance, WindowOptions, WindowParams, WindowTextSystem,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::FxHashSet;
@@ -398,6 +398,7 @@ impl Window {
             is_movable,
             display_id,
             fullscreen,
+            window_background,
         } = options;
 
         let bounds = bounds.unwrap_or_else(|| default_bounds(display_id, cx));
@@ -411,6 +412,7 @@ impl Window {
                 focus,
                 show,
                 display_id,
+                window_background,
             },
         );
         let display_id = platform_window.display().id();
@@ -872,7 +874,7 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.bounds()
     }
 
-    /// Retusn whether or not the window is currently fullscreen
+    /// Returns whether or not the window is currently fullscreen
     pub fn is_fullscreen(&self) -> bool {
         self.window.platform_window.is_fullscreen()
     }
@@ -911,6 +913,13 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.set_title(title);
     }
 
+    /// Sets the window background appearance.
+    pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
+        self.window
+            .platform_window
+            .set_background_appearance(background_appearance);
+    }
+
     /// Mark the window as dirty at the platform level.
     pub fn set_window_edited(&mut self, edited: bool) {
         self.window.platform_window.set_edited(edited);

crates/theme/src/default_theme.rs 🔗

@@ -1,5 +1,7 @@
 use std::sync::Arc;
 
+use gpui::WindowBackgroundAppearance;
+
 use crate::prelude::*;
 
 use crate::{
@@ -15,6 +17,7 @@ fn zed_pro_daylight() -> Theme {
         name: "Zed Pro Daylight".into(),
         appearance: Appearance::Light,
         styles: ThemeStyles {
+            window_background_appearance: WindowBackgroundAppearance::Opaque,
             system: SystemColors::default(),
             colors: ThemeColors::light(),
             status: StatusColors::light(),
@@ -45,6 +48,7 @@ pub(crate) fn zed_pro_moonlight() -> Theme {
         name: "Zed Pro Moonlight".into(),
         appearance: Appearance::Dark,
         styles: ThemeStyles {
+            window_background_appearance: WindowBackgroundAppearance::Opaque,
             system: SystemColors::default(),
             colors: ThemeColors::dark(),
             status: StatusColors::dark(),

crates/theme/src/one_themes.rs 🔗

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use gpui::{hsla, FontStyle, FontWeight, HighlightStyle};
+use gpui::{hsla, FontStyle, FontWeight, HighlightStyle, WindowBackgroundAppearance};
 
 use crate::{
     default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme,
@@ -39,8 +39,8 @@ pub(crate) fn one_dark() -> Theme {
         id: "one_dark".to_string(),
         name: "One Dark".into(),
         appearance: Appearance::Dark,
-
         styles: ThemeStyles {
+            window_background_appearance: WindowBackgroundAppearance::Opaque,
             system: SystemColors::default(),
             colors: ThemeColors {
                 border: hsla(225. / 360., 13. / 100., 12. / 100., 1.),

crates/theme/src/registry.rs 🔗

@@ -122,6 +122,13 @@ impl ThemeRegistry {
                 AppearanceContent::Light => SyntaxTheme::light(),
                 AppearanceContent::Dark => SyntaxTheme::dark(),
             };
+
+            let window_background_appearance = user_theme
+                .style
+                .window_background_appearance
+                .map(Into::into)
+                .unwrap_or_default();
+
             if !user_theme.style.syntax.is_empty() {
                 syntax_colors.highlights = user_theme
                     .style
@@ -153,6 +160,7 @@ impl ThemeRegistry {
                 },
                 styles: ThemeStyles {
                     system: SystemColors::default(),
+                    window_background_appearance,
                     colors: theme_colors,
                     status: status_colors,
                     player: player_colors,

crates/theme/src/schema.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::Result;
-use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla};
+use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance};
 use indexmap::IndexMap;
 use palette::FromColor;
 use schemars::gen::SchemaGenerator;
@@ -33,6 +33,25 @@ pub enum AppearanceContent {
     Dark,
 }
 
+/// The background appearance of the window.
+#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WindowBackgroundContent {
+    Opaque,
+    Transparent,
+    Blurred,
+}
+
+impl From<WindowBackgroundContent> for WindowBackgroundAppearance {
+    fn from(value: WindowBackgroundContent) -> Self {
+        match value {
+            WindowBackgroundContent::Opaque => WindowBackgroundAppearance::Opaque,
+            WindowBackgroundContent::Transparent => WindowBackgroundAppearance::Transparent,
+            WindowBackgroundContent::Blurred => WindowBackgroundAppearance::Blurred,
+        }
+    }
+}
+
 /// The content of a serialized theme family.
 #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 pub struct ThemeFamilyContent {
@@ -53,6 +72,9 @@ pub struct ThemeContent {
 #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(default)]
 pub struct ThemeStyleContent {
+    #[serde(default, rename = "background.appearance")]
+    pub window_background_appearance: Option<WindowBackgroundContent>,
+
     #[serde(flatten, default)]
     pub colors: ThemeColorsContent,
 

crates/theme/src/settings.rs 🔗

@@ -244,6 +244,12 @@ impl ThemeSettings {
         if let Some(theme_overrides) = &self.theme_overrides {
             let mut base_theme = (*self.active_theme).clone();
 
+            if let Some(window_background_appearance) = theme_overrides.window_background_appearance
+            {
+                base_theme.styles.window_background_appearance =
+                    window_background_appearance.into();
+            }
+
             base_theme
                 .styles
                 .colors

crates/theme/src/styles/colors.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::Hsla;
+use gpui::{Hsla, WindowBackgroundAppearance};
 use refineable::Refineable;
 use std::sync::Arc;
 
@@ -235,6 +235,8 @@ pub struct ThemeColors {
 
 #[derive(Refineable, Clone)]
 pub struct ThemeStyles {
+    /// The background appearance of the window.
+    pub window_background_appearance: WindowBackgroundAppearance,
     pub system: SystemColors,
     /// An array of colors used for theme elements that iterate through a series of colors.
     ///

crates/theme/src/theme.rs 🔗

@@ -27,7 +27,9 @@ pub use schema::*;
 pub use settings::*;
 pub use styles::*;
 
-use gpui::{AppContext, AssetSource, Hsla, SharedString, WindowAppearance};
+use gpui::{
+    AppContext, AssetSource, Hsla, SharedString, WindowAppearance, WindowBackgroundAppearance,
+};
 use serde::Deserialize;
 
 #[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
@@ -158,6 +160,12 @@ impl Theme {
     pub fn appearance(&self) -> Appearance {
         self.appearance
     }
+
+    /// Returns the [`WindowBackgroundAppearance`] for the theme.
+    #[inline(always)]
+    pub fn window_background_appearance(&self) -> WindowBackgroundAppearance {
+        self.styles.window_background_appearance
+    }
 }
 
 pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla {

crates/theme_importer/src/vscode/converter.rs 🔗

@@ -56,6 +56,7 @@ impl VsCodeThemeConverter {
             name: self.theme_metadata.name,
             appearance,
             style: ThemeStyleContent {
+                window_background_appearance: Some(theme::WindowBackgroundContent::Opaque),
                 colors: theme_colors,
                 status: status_colors,
                 players: Vec::new(),

crates/zed/src/main.rs 🔗

@@ -208,12 +208,21 @@ fn main() {
         watch_file_types(fs.clone(), cx);
 
         languages.set_theme(cx.theme().clone());
+
         cx.observe_global::<SettingsStore>({
             let languages = languages.clone();
             let http = http.clone();
             let client = client.clone();
 
             move |cx| {
+                for &mut window in cx.windows().iter_mut() {
+                    let background_appearance = cx.theme().window_background_appearance();
+                    window
+                        .update(cx, |_, cx| {
+                            cx.set_background_appearance(background_appearance)
+                        })
+                        .ok();
+                }
                 languages.set_theme(cx.theme().clone());
                 let new_host = &client::ClientSettings::get_global(cx).server_url;
                 if &http.base_url() != new_host {

crates/zed/src/zed.rs 🔗

@@ -34,6 +34,7 @@ use task::{
     oneshot_source::OneshotSource,
     static_source::{StaticSource, TrackedFile},
 };
+use theme::ActiveTheme;
 
 use terminal_view::terminal_panel::{self, TerminalPanel};
 use util::{
@@ -104,6 +105,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
         is_movable: true,
         display_id: display.map(|display| display.id()),
         fullscreen: false,
+        window_background: cx.theme().window_background_appearance(),
     }
 }