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:
parent
97c5c5a6e7
commit
1d684c8890
1 changed files with 151 additions and 25 deletions
|
@ -10,10 +10,12 @@ use crate::{
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{
|
appkit::{
|
||||||
NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags,
|
NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered,
|
||||||
NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
|
NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen,
|
||||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
|
NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial,
|
||||||
NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility,
|
NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton,
|
||||||
|
NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode,
|
||||||
|
NSWindowStyleMask, NSWindowTitleVisibility,
|
||||||
},
|
},
|
||||||
base::{id, nil},
|
base::{id, nil},
|
||||||
foundation::{
|
foundation::{
|
||||||
|
@ -53,6 +55,7 @@ const WINDOW_STATE_IVAR: &str = "windowState";
|
||||||
static mut WINDOW_CLASS: *const Class = ptr::null();
|
static mut WINDOW_CLASS: *const Class = ptr::null();
|
||||||
static mut PANEL_CLASS: *const Class = ptr::null();
|
static mut PANEL_CLASS: *const Class = ptr::null();
|
||||||
static mut VIEW_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)]
|
#[allow(non_upper_case_globals)]
|
||||||
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
||||||
|
@ -241,6 +244,20 @@ unsafe fn build_classes() {
|
||||||
}
|
}
|
||||||
decl.register()
|
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,
|
executor: ForegroundExecutor,
|
||||||
native_window: id,
|
native_window: id,
|
||||||
native_view: NonNull<Object>,
|
native_view: NonNull<Object>,
|
||||||
|
blurred_view: Option<id>,
|
||||||
display_link: Option<DisplayLink>,
|
display_link: Option<DisplayLink>,
|
||||||
renderer: renderer::Renderer,
|
renderer: renderer::Renderer,
|
||||||
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
|
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
|
||||||
|
@ -600,8 +618,9 @@ impl MacWindow {
|
||||||
setReleasedWhenClosed: NO
|
setReleasedWhenClosed: NO
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let content_view = native_window.contentView();
|
||||||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
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());
|
assert!(!native_view.is_null());
|
||||||
|
|
||||||
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
|
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
|
||||||
|
@ -609,6 +628,7 @@ impl MacWindow {
|
||||||
executor,
|
executor,
|
||||||
native_window,
|
native_window,
|
||||||
native_view: NonNull::new_unchecked(native_view),
|
native_view: NonNull::new_unchecked(native_view),
|
||||||
|
blurred_view: None,
|
||||||
display_link: None,
|
display_link: None,
|
||||||
renderer: renderer::new_renderer(
|
renderer: renderer::new_renderer(
|
||||||
renderer_context,
|
renderer_context,
|
||||||
|
@ -683,11 +703,11 @@ impl MacWindow {
|
||||||
// itself and break the association with its context.
|
// itself and break the association with its context.
|
||||||
native_view.setWantsLayer(YES);
|
native_view.setWantsLayer(YES);
|
||||||
let _: () = msg_send![
|
let _: () = msg_send![
|
||||||
native_view,
|
native_view,
|
||||||
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
|
setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
|
||||||
];
|
];
|
||||||
|
|
||||||
native_window.setContentView_(native_view.autorelease());
|
content_view.addSubview_(native_view.autorelease());
|
||||||
native_window.makeFirstResponder_(native_view);
|
native_window.makeFirstResponder_(native_view);
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
|
@ -1035,28 +1055,57 @@ impl PlatformWindow for MacWindow {
|
||||||
|
|
||||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||||
let mut this = self.0.as_ref().lock();
|
let mut this = self.0.as_ref().lock();
|
||||||
this.renderer
|
|
||||||
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
|
|
||||||
|
|
||||||
let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
|
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
|
||||||
80
|
this.renderer.update_transparency(!opaque);
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc();
|
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
this.native_window.setOpaque_(opaque);
|
this.native_window.setOpaque_(opaque as BOOL);
|
||||||
// Shadows for transparent windows cause artifacts and performance issues
|
let background_color = if opaque {
|
||||||
this.native_window.setHasShadow_(opaque);
|
|
||||||
let clear_color = if opaque == YES {
|
|
||||||
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
|
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
|
||||||
} else {
|
} 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);
|
this.native_window.setBackgroundColor_(background_color);
|
||||||
let window_number = this.native_window.windowNumber();
|
|
||||||
CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
|
if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 {
|
||||||
|
// Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`.
|
||||||
|
// On macOS Catalina/Big Sur `NSVisualEffectView` doesn’t 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 mut lock = window_state.as_ref().lock();
|
||||||
|
|
||||||
let new_size = Size::<Pixels>::from(size);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2148,3 +2202,75 @@ unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
|
||||||
screen_number as 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue