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 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` 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue