Add shadow back for blurred/transparent window on macOS
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.
We require contributors to sign our Contributor License Agreement, and we don't have @alphaArgon on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.
@cla-bot check
The cla-bot has been summoned, and re-checked this pull request!
Does this actually work across macOS versions? I remember coming across this approach when investigating blurred windows initially and it was mentioned that it only worked on specific versions of macOS (well, Cocoa) because the layers/names changed.
@jansol
Does this actually work across macOS versions
I’m sorry for being late with the full-version testing, and I found my implementation doesn’t work on macOS Catalina. Prior to macOS Big Sur, NSVisualEffectView doesn’t own concrete sublayers; instead, it transfers all the rendering work to a CAProxyLayer.
I remember coming across this approach when investigating blurred windows initially and it was mentioned that it only worked on specific versions of macOS (well, Cocoa) because the layers/names changed.
As long as the proxy layer is not used, checking filters by name is robust: unlike layers, filter types (CAFilter, CIFilter) use name to identify what effects they should do, which is a relatively stable property.
Another thing is setting the window background color to a fully transparent color causes the shadow being drawn differently. A bitmap of the content is taken, made monotone and blurred, and then attached as the shadow. If the opacity of the content changes, the shadow won’t get updated unless an -[NSWindow invalidateShadow] is called. Therefore, for clear background colors we must disable the shadow to avoid visual inconsistency.
However, as long as the window is titled and the background color is partially opaque, say having an alpha of 0.01, the shadow behaves as if it’s in normal windows. Maybe I made things complicated that all we need is a very-low-alpha color?
As long as the proxy layer is not used, checking filters by name is robust: unlike layers, filter types (CAFilter, CIFilter) use name to identify what effects they should do, which is a relatively stable property.
Thanks for the clarification. Can you add this explanation as a comment in the code as well? As for macOS support... Zed does only support some number of versions going back but I'm not sure what the official policy for the cutoff is. Maybe a good approach would be to fall back to the current implementation for versions older than Big Sur?
Another thing is setting the window background color to a fully transparent color causes the shadow being drawn differently. A bitmap of the content is taken, made monotone and blurred, and then attached as the shadow. If the opacity of the content changes, the shadow won’t get updated unless an -[NSWindow invalidateShadow] is called. Therefore, for clear background colors we must disable the shadow to avoid visual inconsistency.
Hmm.. I do remember getting "phantom text" even with a background alpha of like 0.5. But that was on Catalina, maybe they changed the shadow rendering logic after that.
However, as long as the window is
titledand the background color is partially opaque, say having an alpha of 0.01, the shadow behaves as if it’s in normal windows. Maybe I made things complicated that all we need is a very-low-alpha color?
I'm hesitant to limit background opacity, some people did deliberately set it to exactly zero for some specific scenarios. But disabling shadows when background alpha is zero would probably be an acceptable compromise. Still an improvement over status quo and if someone really wants a fully transparent background the shadow would look awkward anyway. But that is ultimately something the designer types of the Zed team have to decide.
@jansol
Hmm.. I do remember getting "phantom text" even with a background alpha of like 0.5. But that was on Catalina, maybe they changed the shadow rendering logic after that.
Friends and I tested setting the background to -[NSColor colorWithWhite:0 alpha:0.0001]. This color is invisible to the human eye while not technically being clear, and here’s† how it works on macOS High Sierra and Ventura. The shadow works fine and breaks not on Mission Control. Since GPUIWindow is a standard titled window, “phantom text” won’t appear with a non-zero-alpha background color.
I'm hesitant to limit background opacity, some people did deliberately set it to exactly zero
There are two kinds of background colors: the settings’ background (rendered by GPUIView), and the background color of CGUIWindow; they are independent. Currently the CGUIWindow’s background color is set to -[NSColor clearColors] when the settings’ background.appearance is not opaque — that’s the problem. This also makes the backdrop blur disappear if the setting’s background is set to to #xxxxxx00, which I don’t think is by design (no logic found in the code) and appears to be a bug of the Quartz Compositor.
but I'm not sure what the official policy for the cutoff is.
Currently supports macOS Catalina. Sadly. Using two implementations could be hard to maintain. If we want the shadow back, replacing NSColor::clearColor(nil) to somecolor like NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001) could be the best approach. I’m opening an issue about whether and where the shadow should be added back.
† The screenshots:
Mmm okay, maybe a black background with 0.0001 alpha is the way to go then. I don't have access to any macs these days so I can't check myself. Thanks for doing the in depth research though! :pray:
Mmm okay, maybe a black background with 0.0001 alpha is the way to go then. I don't have access to any macs these days so I can't check myself. Thanks for doing the in depth research though! 🙏
Well it seems that we still need to use an NSVisualEffectView... The maximum effective radius for CGSSetWindowBackgroundBlurRadius is about 15; setting it to the current value (80) causes a clamp. It’s less blurry than the NSVisualEffectView (30):
@jansol Thanks for the review!
I’ve removed the use of deprecated materials. It now hides the CAChameleonLayer which shows the desktop tinting effect. The reason why I originally chose a deprecated material was that the default material of an NSVisualEffectView is AppearanceBased, which is also deprecated but widely used.
And I tested on macOS Catalina, Monterey, Ventura, and Sequoia. They work just like before with the same performance. Here’re the screenshots of Zed built with these commits on Catalina and Sequoia:
Could someone else with a Mac take a look of it?
i can confirm fixed on sequoia 15.5!
I have tried to test this but have not succeeded due to 0 knowledge on how to test it.
The easiest way is to check the corner case(s):
- install the Glazier extension
- switch to the Moss Fett theme
- add
"background": "#00000000"totheme_overrides - toggle macOS back and forth between light and dark mode
If everything works perfectly, there should be no tinting/lightening/darkening in the window background with the exception of the shadow behind the window, and more importantly it should not change when switching between light and dark mode. The latter should be easy to verify with a screenshot or color picker tool.
I'm not entirely sure what you'd expect from that paragraph in the docs? Assuming everything works as described here, the only takeaway should really be that transparent windows will still always have a shadow affecting what is seen through the window.
Yeah this just fixes this bug for me, here's main for comparison:
The easiest way is to check the corner case(s):
https://github.com/user-attachments/assets/42614795-2f0b-4866-8b4a-38b6748282f9
lgtm
Perfect!
Can you also post a comparison with "background.appearance": "transparent"? The blurred background looks like it ignores the shadow behind the window. (not relevant to this PR but I'd appreciate a comparison point for #21341)
yep:
video: https://github.com/user-attachments/assets/f5d43723-1a0e-45af-ac96-6927ba53df73