Add shadow back for blurred/transparent window on macOS (#27403)

Closes #15383
Closes #10993

`NSVisualEffectView` is an official API for implementing blur effects
and, by traversing the layers, we **can remove the background color**
that comes with the view. This avoids using private APIs and aligns
better with macOS’s native design.

Currently, `GPUIView` serves as the content view of the window. To add
the blurred view, `GPUIView` is downgraded to a subview of the content
view, placed at the same level as the blurred view.

Release Notes:

- Fixed the missing shadow for blurred-background windows on macOS.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
This commit is contained in:
alphaArgon 2025-06-28 14:50:54 +08:00 committed by GitHub
parent 97c5c5a6e7
commit 1d684c8890
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,10 +10,12 @@ use crate::{
use block::ConcreteBlock;
use cocoa::{
appkit::{
NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags,
NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility,
NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered,
NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen,
NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial,
NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton,
NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode,
NSWindowStyleMask, NSWindowTitleVisibility,
},
base::{id, nil},
foundation::{
@ -53,6 +55,7 @@ const WINDOW_STATE_IVAR: &str = "windowState";
static mut WINDOW_CLASS: *const Class = ptr::null();
static mut PANEL_CLASS: *const Class = ptr::null();
static mut VIEW_CLASS: *const Class = ptr::null();
static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
@ -241,6 +244,20 @@ unsafe fn build_classes() {
}
decl.register()
};
BLURRED_VIEW_CLASS = {
let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap();
unsafe {
decl.add_method(
sel!(initWithFrame:),
blurred_view_init_with_frame as extern "C" fn(&Object, Sel, NSRect) -> id,
);
decl.add_method(
sel!(updateLayer),
blurred_view_update_layer as extern "C" fn(&Object, Sel),
);
decl.register()
}
};
}
}
@ -335,6 +352,7 @@ struct MacWindowState {
executor: ForegroundExecutor,
native_window: id,
native_view: NonNull<Object>,
blurred_view: Option<id>,
display_link: Option<DisplayLink>,
renderer: renderer::Renderer,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
@ -600,8 +618,9 @@ impl MacWindow {
setReleasedWhenClosed: NO
];
let content_view = native_window.contentView();
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view);
let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view));
assert!(!native_view.is_null());
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
@ -609,6 +628,7 @@ impl MacWindow {
executor,
native_window,
native_view: NonNull::new_unchecked(native_view),
blurred_view: None,
display_link: None,
renderer: renderer::new_renderer(
renderer_context,
@ -683,11 +703,11 @@ impl MacWindow {
// itself and break the association with its context.
native_view.setWantsLayer(YES);
let _: () = msg_send![
native_view,
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
native_view,
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
];
native_window.setContentView_(native_view.autorelease());
content_view.addSubview_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
match kind {
@ -1035,28 +1055,57 @@ impl PlatformWindow for MacWindow {
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut this = self.0.as_ref().lock();
this.renderer
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
80
} else {
0
};
let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc();
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
this.renderer.update_transparency(!opaque);
unsafe {
this.native_window.setOpaque_(opaque);
// Shadows for transparent windows cause artifacts and performance issues
this.native_window.setHasShadow_(opaque);
let clear_color = if opaque == YES {
this.native_window.setOpaque_(opaque as BOOL);
let background_color = if opaque {
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
} else {
NSColor::clearColor(nil)
// Not using `+[NSColor clearColor]` to avoid broken shadow.
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001)
};
this.native_window.setBackgroundColor_(clear_color);
let window_number = this.native_window.windowNumber();
CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
this.native_window.setBackgroundColor_(background_color);
if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 {
// Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`.
// On macOS Catalina/Big Sur `NSVisualEffectView` doesnt own concrete sublayers
// but uses a `CAProxyLayer`. Use the legacy WindowServer API.
let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
80
} else {
0
};
let window_number = this.native_window.windowNumber();
CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
} else {
// On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it
// could have a better performance (it downsamples the backdrop) and more control
// over the effect layer.
if background_appearance != WindowBackgroundAppearance::Blurred {
if let Some(blur_view) = this.blurred_view {
NSView::removeFromSuperview(blur_view);
this.blurred_view = None;
}
} else if this.blurred_view == None {
let content_view = this.native_window.contentView();
let frame = NSView::bounds(content_view);
let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc];
blur_view = NSView::initWithFrame_(blur_view, frame);
blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
let _: () = msg_send![
content_view,
addSubview: blur_view
positioned: NSWindowOrderingMode::NSWindowBelow
relativeTo: nil
];
this.blurred_view = Some(blur_view.autorelease());
}
}
}
}
@ -1763,7 +1812,12 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
let mut lock = window_state.as_ref().lock();
let new_size = Size::<Pixels>::from(size);
if lock.content_size() == new_size {
let old_size = unsafe {
let old_frame: NSRect = msg_send![this, frame];
Size::<Pixels>::from(old_frame.size)
};
if old_size == new_size {
return;
}
@ -2148,3 +2202,75 @@ unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
screen_number as CGDirectDisplayID
}
}
extern "C" fn blurred_view_init_with_frame(this: &Object, _: Sel, frame: NSRect) -> id {
unsafe {
let view = msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame];
// Use a colorless semantic material. The default value `AppearanceBased`, though not
// manually set, is deprecated.
NSVisualEffectView::setMaterial_(view, NSVisualEffectMaterial::Selection);
NSVisualEffectView::setState_(view, NSVisualEffectState::Active);
view
}
}
extern "C" fn blurred_view_update_layer(this: &Object, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer];
let layer: id = msg_send![this, layer];
if !layer.is_null() {
remove_layer_background(layer);
}
}
}
unsafe fn remove_layer_background(layer: id) {
unsafe {
let _: () = msg_send![layer, setBackgroundColor:nil];
let class_name: id = msg_send![layer, className];
if class_name.isEqualToString("CAChameleonLayer") {
// Remove the desktop tinting effect.
let _: () = msg_send![layer, setHidden: YES];
return;
}
let filters: id = msg_send![layer, filters];
if !filters.is_null() {
// Remove the increased saturation.
// The effect of a `CAFilter` or `CIFilter` is determined by its name, and the
// `description` reflects its name and some parameters. Currently `NSVisualEffectView`
// uses a `CAFilter` named "colorSaturate". If one day they switch to `CIFilter`, the
// `description` will still contain "Saturat" ("... inputSaturation = ...").
let test_string: id = NSString::alloc(nil).init_str("Saturat").autorelease();
let count = NSArray::count(filters);
for i in 0..count {
let description: id = msg_send![filters.objectAtIndex(i), description];
let hit: BOOL = msg_send![description, containsString: test_string];
if hit == NO {
continue;
}
let all_indices = NSRange {
location: 0,
length: count,
};
let indices: id = msg_send![class!(NSMutableIndexSet), indexSet];
let _: () = msg_send![indices, addIndexesInRange: all_indices];
let _: () = msg_send![indices, removeIndex:i];
let filtered: id = msg_send![filters, objectsAtIndexes: indices];
let _: () = msg_send![layer, setFilters: filtered];
break;
}
}
let sublayers: id = msg_send![layer, sublayers];
if !sublayers.is_null() {
let count = NSArray::count(sublayers);
for i in 0..count {
let sublayer = sublayers.objectAtIndex(i);
remove_layer_background(sublayer);
}
}
}
}