Created
July 2, 2020 17:23
-
-
Save avaidyam/d3c76df710651edbf4da56bad3fea9d2 to your computer and use it in GitHub Desktop.
DIY NSVisualEffectView using Private API for macOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// TODO: setting window transforms (or mission control) flattens layers... | |
// TODO: Mask image vs path (layer)? | |
/// `NSVisualEffectView`: | |
/// | |
/// A view that adds translucency and vibrancy effects to the views in your interface. | |
/// When you want views to be more prominent in your interface, place them in a | |
/// backdrop view. The backdrop view is partially transparent, allowing some of | |
/// the underlying content to show through. Typically, you use a backdrop view | |
/// to blur background content, instead of obscuring it completely. It can also | |
/// make its contained content more vibrant to ensure that it remains prominent. | |
/// | |
/// A suggested use in designing visual containers is the use of "cards"; apply | |
/// a `.light` or `.dark` effect to the backdrop view, set its `cornerRadius = 4.5`, | |
/// `rimOpacity = 0.25`, and add a `NSView.shadow` (visually similar to `NSWindow`). | |
/// | |
/// Note: if set as the containing `window`'s `contentView`, the window's | |
/// `isOpaque` value will be changed. If the window's `contentView` is changed, | |
/// the original settings are restored. However, unlike `NSVisualEffectView`, | |
/// no desktop bleed blending will occur. In addition, `.behindWindow` blending | |
/// cannot be applied if the view is not the `window`'s `contentView`. | |
/// | |
/// In addition, if the containing `NSWindow` is transformed in any way, including | |
/// through Mission Control/Exposé, the background blending will fail. | |
/// | |
/// Note: A NotificationCenter effect is simulated with `kCAFilterColorBrightness @ 0.5`. | |
public class BackdropView: NSVisualEffectView { | |
/// The `Effect` structure describes the parameters used by the `BackdropView` | |
/// to produce its effects. Note that these effects do not cascade to any | |
/// subviews; that is instead governed by the `BackdropView.effectiveAppearance`. | |
public struct Effect { | |
/// The `backgroundColor` is and autoclosure used to dynamically blend with | |
/// the layers and contents behind the `BackdropView`. | |
public let backgroundColor: () -> (NSColor) | |
/// The `tintColor` is an autoclosure used to dynamically set the tint color. | |
/// This is also the color used when the `BackdropView` is visually inactive. | |
public let tintColor: () -> (NSColor) | |
/// The `tintFilter` can be any object accepted by `CALayer.compositingFilter`. | |
/// `nil`, `darkenBlendMode`, and `lightenBlendMode` are special values. | |
public let tintFilter: Any? | |
/// Create a new `BackdropView.Effect` with the provided parameters. | |
public init(_ backgroundColor: @autoclosure @escaping () -> (NSColor), | |
_ tintColor: @autoclosure @escaping () -> (NSColor), | |
_ tintFilter: Any?) | |
{ | |
self.backgroundColor = backgroundColor | |
self.tintColor = tintColor | |
self.tintFilter = tintFilter | |
} | |
/// A clear effect (only applies blur and saturation); when inactive, | |
/// appears transparent. Not suggested for typical use. | |
public static var clear = Effect(NSColor(calibratedWhite: 1.00, alpha: 0.05), | |
NSColor(calibratedWhite: 1.00, alpha: 0.00), | |
nil) | |
/// A medium light effect. | |
public static var mediumLight = Effect(NSColor(calibratedWhite: 1.00, alpha: 0.30), | |
NSColor(calibratedWhite: 0.94, alpha: 1.00), | |
kCAFilterDarkenBlendMode) | |
/// A light effect. | |
public static var light = Effect(NSColor(calibratedWhite: 0.97, alpha: 0.70), | |
NSColor(calibratedWhite: 0.94, alpha: 1.00), | |
kCAFilterDarkenBlendMode) | |
/// An ultra light effect. | |
public static var ultraLight = Effect(NSColor(calibratedWhite: 0.97, alpha: 0.85), | |
NSColor(calibratedWhite: 0.94, alpha: 1.00), | |
kCAFilterDarkenBlendMode) | |
/// A medium dark effect. | |
public static var mediumDark = Effect(NSColor(calibratedWhite: 1.00, alpha: 0.40), | |
NSColor(calibratedWhite: 0.84, alpha: 1.00), | |
kCAFilterDarkenBlendMode) | |
/// A dark effect. | |
public static var dark = Effect(NSColor(calibratedWhite: 0.12, alpha: 0.45), | |
NSColor(calibratedWhite: 0.16, alpha: 1.00), | |
kCAFilterLightenBlendMode) | |
/// An ultra dark effect. | |
public static var ultraDark = Effect(NSColor(calibratedWhite: 0.12, alpha: 0.80), | |
NSColor(calibratedWhite: 0.01, alpha: 1.00), | |
kCAFilterLightenBlendMode) | |
/// A selection effect that matches the user's current aqua color preference. | |
public static var selection = Effect(NSColor.keyboardFocusIndicatorColor.withAlphaComponent(0.7), | |
NSColor.keyboardFocusIndicatorColor, | |
kCAFilterDestOver) | |
// Note: `keyboardFocusIndicatorColor` was used because it's the only | |
// dynamic color that isn't a pattern image color. | |
} | |
/// If multiple `BackdropView`s within the same layer tree (that is, window) | |
/// share the same `BlendGroup`, they will be composited and blended | |
/// together as a single continuous backdrop. However, setting different | |
/// `effect`s may cause visual disparity; use with caution. | |
public final class BlendGroup { | |
/// The notification posted upon deinit of a `BlendGroup`. | |
fileprivate static let removedNotification = Notification.Name("BackdropView.BlendGroup.deinit") | |
/// The internal value used for `CABackdropLayer.groupName`. | |
fileprivate let value = UUID().uuidString | |
/// Create a new `BlendGroup`. | |
public init() {} | |
deinit { | |
// Alert all `BackdropView`s that we're about to be removed. | |
// The `BackdropView` will figure out if it needs to update itself. | |
NotificationCenter.default.post(name: BlendGroup.removedNotification, | |
object: nil, userInfo: ["value": self.value]) | |
} | |
/// The `global` BlendGroup, if it is desired that all backdrops share | |
/// the same blending group through the layer tree (window). | |
public static let global = BlendGroup() | |
/// The default internal value used for `CABackdropLayer.groupName`. | |
/// This is to be used if no `BlendGroup` is set on the `BackdropView`. | |
fileprivate static func `default`() -> String { | |
return UUID().uuidString | |
} | |
} | |
/// If `state` is set to `.followsWindowActiveState` or `NSWorkspace`'s | |
/// `accessibilityDisplayShouldReduceTransparency` is true, the true visual | |
/// state of the `BackdropView` may actually be `.active` or `.inactive`, | |
/// and may change without notice. If such a state change occurs, this property | |
/// governs whether or not that the visual change is animated. | |
/// | |
/// Note: this property is disregarded if properties are set within an active | |
/// `NSAnimationContext` grouping. | |
public var animatesImplicitStateChanges: Bool = false | |
/// The visual effect to present within the `BackdropView`. See `BackdropView.Effect`. | |
public var effect: BackdropView.Effect = .clear { | |
didSet { | |
self.transaction { | |
self.backdrop?.backgroundColor = self.effect.backgroundColor().cgColor | |
self.tint?.backgroundColor = self.effect.tintColor().cgColor | |
self.tint?.compositingFilter = self.effect.tintFilter | |
} | |
} | |
} | |
/// If multiple `BackdropView`s within the same layer tree (that is, window) | |
/// share the same `BlendGroup`, they will be composited and blended | |
/// together as a single continuous backdrop. However, setting different | |
/// `effect`s may cause visual disparity; use with caution. | |
/// | |
/// Note: you must retain any non-`global` `BlendGroup`s yourself. | |
public weak var blendingGroup: BlendGroup? = nil { | |
didSet { | |
self.transaction { | |
self.backdrop?.groupName = self.blendingGroup?.value ?? BlendGroup.default() | |
} | |
} | |
} | |
/// The gaussian blur radius of the visual effect. Animatable. | |
public var blurRadius: CGFloat { | |
get { return self.backdrop?.value(forKeyPath: "filters.gaussianBlur.inputRadius") as? CGFloat ?? 0 } | |
set { | |
self.transaction { | |
self.backdrop?.setValue(newValue, forKeyPath: "filters.gaussianBlur.inputRadius") | |
} | |
} | |
} | |
/// The background color saturation factor of the visual effect. Animatable. | |
public var saturationFactor: CGFloat { | |
get { return self.backdrop?.value(forKeyPath: "filters.colorSaturate.inputAmount") as? CGFloat ?? 0 } | |
set { | |
self.transaction { | |
self.backdrop?.setValue(newValue, forKeyPath: "filters.colorSaturate.inputAmount") | |
} | |
} | |
} | |
/// The corner radius of the view. | |
public var cornerRadius: CGFloat = 0.0 { | |
didSet { | |
self.transaction { | |
self.container?.cornerRadius = self.cornerRadius | |
self.rim?.cornerRadius = self.cornerRadius | |
} | |
} | |
} | |
/// The `BackdropView`'s rim serves to provide a visual contrast at its edges. | |
/// If `rimOpacity > 0.0`, a slight hairline border is rendered around the view. | |
/// | |
/// Note: this property, along with `shadow` requires the superview to be | |
/// layer-backed. The rim is contained 0.5px outside of the view. | |
public var rimOpacity: CGFloat = 0.0 { | |
didSet { | |
self.transaction { | |
self.rim!.opacity = Float(self.rimOpacity) | |
} | |
} | |
} | |
/// Automatically `.behindWindow` if set as the contentView of the window. | |
/// Otherwise, the view is ALWAYS `.withinWindow` blended. Thus, it is not | |
/// possible to "punch out" the window in specific regions. | |
public override var blendingMode: NSVisualEffectView.BlendingMode { | |
get { return self.window?.contentView == self ? .behindWindow : .withinWindow } | |
set { } | |
} | |
/// Always `.appearanceBased`; use `effect` instead. | |
public override var material: NSVisualEffectView.Material { | |
get { return .appearanceBased } | |
set { } | |
} | |
/// Specify how the `effect` should reflect window activity or accessibility state. | |
public override var state: NSVisualEffectView.State { | |
get { return self._state } | |
set { self._state = newValue } | |
} | |
/// We handle the state differently from our superview, which requires `.active`. | |
/// Bounce the `state.didSet` onto `reduceTransparencyChanged`. | |
private var _state: NSVisualEffectView.State = .active { | |
didSet { | |
// Don't be called when `commonInit` hasn't finished. | |
guard let _ = self.backdrop else { return } | |
self.reduceTransparencyChanged(nil) | |
} | |
} | |
private var backdrop: CABackdropLayer? = nil | |
private var tint: CALayer? = nil | |
private var container: CALayer? = nil | |
private var rim: CALayer? = nil | |
//private var fallback: CAProxyLayer? = nil | |
public override init(frame frameRect: NSRect) { | |
super.init(frame: frameRect) | |
self.commonInit() | |
} | |
public required init?(coder decoder: NSCoder) { | |
super.init(coder: decoder) | |
self.commonInit() | |
} | |
private func commonInit() { | |
self.wantsLayer = true | |
self.layerContentsRedrawPolicy = .onSetNeedsDisplay | |
self.layer?.masksToBounds = false | |
self.layer?.name = "view" | |
// Essentially, tell the `NSVisualEffectView` to not do its job: | |
super.state = .active | |
super.blendingMode = .withinWindow | |
super.material = .appearanceBased | |
self.setValue(true, forKey: "clear") // internal material | |
// Set up our backdrop view: | |
self.backdrop = CABackdropLayer() | |
self.backdrop!.name = "backdrop" | |
self.backdrop!.allowsGroupBlending = true | |
self.backdrop!.allowsGroupOpacity = true | |
self.backdrop!.allowsEdgeAntialiasing = false | |
self.backdrop!.disablesOccludedBackdropBlurs = true | |
self.backdrop!.ignoresOffscreenGroups = true | |
self.backdrop!.allowsInPlaceFiltering = false // blendgroups don't work otherwise | |
self.backdrop!.scale = 1.0 // 0.25 typically | |
self.backdrop!.bleedAmount = 0.0 | |
// Set up the backdrop filters: | |
let blur = CAFilter(type: kCAFilterGaussianBlur)! | |
let saturate = CAFilter(type: kCAFilterColorSaturate)! | |
blur.setValue(true, forKey: "inputNormalizeEdges") | |
self.backdrop!.filters = [blur, saturate] | |
// Set up the fallback layer used when the window is transformed: | |
/* | |
self.fallback = CAProxyLayer() | |
self.fallback!.name = "fallback" | |
self.fallback!.proxyProperties = [ | |
kCAProxyLayerLevel: 1, | |
kCAProxyLayerActive: true, | |
kCAProxyLayerBlendMode: "PlusD", | |
kCAProxyLayerMaterial: "L" | |
] | |
*/ | |
// Set up the tint and container view: | |
self.tint = CALayer() | |
self.tint!.name = "tint" | |
self.container = CALayer() | |
self.container!.name = "container" | |
self.container!.masksToBounds = true | |
self.container!.allowsGroupBlending = true | |
self.container!.allowsEdgeAntialiasing = false | |
self.container!.sublayers = [self.backdrop!, self.tint!] | |
self.layer?.insertSublayer(self.container!, at: 0) | |
// Set up rim: | |
self.rim = CALayer() | |
self.rim!.name = "rim" | |
self.rim!.borderWidth = 0.5 | |
self.rim!.opacity = 0.0 | |
self.layer?.addSublayer(self.rim!) | |
// Set our effect-related properties: | |
self._state = .followsWindowActiveState | |
self.blendingGroup = nil | |
self.blurRadius = 30.0 | |
self.saturationFactor = 2.5 | |
self.effect = .dark | |
// [Note] macOS 11+: no longer necessary to call `removeObserver` upon `deinit`. | |
NotificationCenter.default.addObserver(self, selector: #selector(self.reduceTransparencyChanged(_:)), | |
name: NSWorkspace.accessibilityDisplayOptionsDidChangeNotification, | |
object: NSWorkspace.shared) | |
NotificationCenter.default.addObserver(self, selector: #selector(self.colorVariantsChanged(_:)), | |
name: NSColor.systemColorsDidChangeNotification, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(self.blendGroupsChanged(_:)), | |
name: BlendGroup.removedNotification, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(self.layerSurfaceChanged(_:)), | |
name: BackdropView.layerSurfaceFlattenedNotification, object: nil) | |
NotificationCenter.default.addObserver(self, selector: #selector(self.layerSurfaceChanged(_:)), | |
name: BackdropView.layerSurfaceFlushedNotification, object: nil) | |
} | |
/// Update sublayer `frame`. | |
public override func layout() { | |
super.layout() | |
self.transaction(false) { | |
self.container!.frame = self.layer?.bounds ?? .zero | |
self.backdrop!.frame = self.layer?.bounds ?? .zero | |
self.tint!.frame = self.layer?.bounds ?? .zero | |
self.rim!.frame = self.layer?.bounds.insetBy(dx: -0.5, dy: -0.5) ?? .zero | |
//self.fallback?.frame = self.layer?.bounds ?? .zero | |
} | |
} | |
/// Update sublayer `contentsScale`. | |
public override func viewDidChangeBackingProperties() { | |
super.viewDidChangeBackingProperties() | |
let scale = self.window?.backingScaleFactor ?? 1.0 | |
self.transaction(false) { | |
self.layer?.contentsScale = scale | |
self.container!.contentsScale = scale | |
self.backdrop!.contentsScale = scale | |
self.tint!.contentsScale = scale | |
self.rim!.contentsScale = scale | |
//self.fallback?.contentsScale = scale | |
} | |
} | |
/// Toggle `CAProxyLayer` visibility if our layers were flattened. | |
@objc private func layerSurfaceChanged(_ note: NSNotification!) { | |
guard let win = note.userInfo?["window"] as? NSWindow, win.contentView == self else { return } | |
//let proxyVisible = note.userInfo?["proxy"] as? Bool ?? false | |
/* | |
// Update the `material` based on our `effectiveAppearance`. | |
var props = self.fallback!.proxyProperties! | |
props[kCAProxyLayerMaterial] = "L"//self.effectiveAppearance.name == .vibrantDark ? "D" : "L" | |
self.fallback!.proxyProperties = props | |
// Toggle visibility. | |
CATransaction.begin() | |
if proxyVisible { | |
self.container!.insertSublayer(self.fallback!, at: 1) | |
} else { | |
self.fallback!.removeFromSuperlayer() | |
} | |
CATransaction.commit() | |
CATransaction.flush() | |
*/ | |
} | |
/// Adjust our `BlendGroup` information if we need to. | |
@objc private func blendGroupsChanged(_ note: NSNotification!) { | |
guard let removed = note.userInfo?["value"] as? String else { return } | |
guard let backdrop = self.backdrop, backdrop.groupName == removed else { return } | |
self.transaction(self.animatesImplicitStateChanges) { | |
backdrop.groupName = BlendGroup.default() // was nil'd out | |
} | |
} | |
/// Allow dynamic/system colors update themselves. | |
@objc private func colorVariantsChanged(_ note: NSNotification!) { | |
guard let _ = self.backdrop else { return } | |
DispatchQueue.main.async { | |
self.transaction(self.animatesImplicitStateChanges) { | |
self.backdrop!.backgroundColor = self.effect.backgroundColor().cgColor | |
self.tint!.backgroundColor = self.effect.tintColor().cgColor | |
} | |
} | |
} | |
/// Modifies sublayers if the dynamic property `reduceTransparency` has changed. | |
@objc private func reduceTransparencyChanged(_ note: NSNotification!) { | |
// If `note` is `nil`, it is considered that we invoked this method from | |
// `BackdropView.state.didSet` - if so, allow animation of the visual state | |
// if called within an `NSAnimationContext` grouping. | |
let actions = ( | |
self.animatesImplicitStateChanges || | |
(note == nil && (CATransaction.value(forKey: "NSAnimationContextBeganGroup") as? Bool ?? false)) | |
) | |
let reduceTransparency = ( | |
NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency || | |
self._state == .inactive || | |
(self._state == .followsWindowActiveState && !(self.window?.isMainWindow ?? false)) | |
) | |
// Enable/disable the backdrop layer and tint layer's `compositingFilter`. | |
self.transaction(actions) { | |
self.backdrop!.isEnabled = !reduceTransparency | |
self.tint!.compositingFilter = !reduceTransparency ? self.effect.tintFilter : nil | |
// Allows the actual animation to work; `-setEnabled:` isn't animated. | |
if reduceTransparency { | |
self.backdrop!.removeFromSuperlayer() | |
} else { | |
self.container!.insertSublayer(self.backdrop!, at: 0) | |
} | |
} | |
} | |
/// Creates a nested transaction whose actions are only enabled by default if | |
/// called within an active `NSAnimationContext` grouping. | |
/// | |
/// Note: also sets the current NSAppearance for drawing purposes. | |
private func transaction(_ actions: Bool? = nil, _ handler: () -> ()) { | |
let actions = actions ?? CATransaction.value(forKey: "NSAnimationContextBeganGroup") as? Bool ?? false | |
// NSAnimationContext handles per-thread activation of CATransaction for us. | |
NSAnimationContext.beginGrouping() | |
CATransaction.setDisableActions(!actions) | |
let saved = NSAppearance.current | |
NSAppearance.current = self.effectiveAppearance | |
handler() | |
NSAppearance.current = saved | |
NSAnimationContext.endGrouping() | |
} | |
public override func viewWillMove(toWindow newWindow: NSWindow?) { | |
super.viewWillMove(toWindow: newWindow) | |
// Restore our window's settings, if we were the `contentView` only. | |
if let oldWindow = self.window, oldWindow.contentView == self { | |
self.configurator.unapply(from: oldWindow) | |
} | |
// Unregister window main-ness changes: | |
guard let _ = self.window else { return } | |
NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeMainNotification, | |
object: self.window!) | |
NotificationCenter.default.removeObserver(self, name: NSWindow.didResignMainNotification, | |
object: self.window!) | |
} | |
public override func viewDidMoveToWindow() { | |
super.viewDidMoveToWindow() | |
self.state = .active | |
// Adjust the backdrop layer's WindowServer awareness. | |
self.backdrop?.windowServerAware = (self.window?.contentView == self) | |
// Set parent window configuration, if we're the `contentView` only. | |
if let newWindow = self.window, newWindow.contentView == self { | |
self.configurator.apply(to: newWindow) | |
} | |
self.cornerRadius = 4.5 | |
self.rimOpacity = 0.25 | |
let s = NSShadow() | |
s.shadowColor = NSColor.black.withAlphaComponent(0.8) | |
s.shadowBlurRadius = 20 | |
self.shadow = s | |
// Register for window main-ness changes: | |
guard let _ = self.window else { return } | |
NotificationCenter.default.addObserver(self, selector: #selector(self.reduceTransparencyChanged(_:)), | |
name: NSWindow.didBecomeMainNotification, object: self.window!) | |
NotificationCenter.default.addObserver(self, selector: #selector(self.reduceTransparencyChanged(_:)), | |
name: NSWindow.didResignMainNotification, object: self.window!) | |
self.reduceTransparencyChanged(NSNotification(name: NSWindow.didBecomeMainNotification, object: nil)) | |
} | |
// | |
// [Private SPI] HERE LIE DRAGONS! | |
// | |
private var configurator = WindowConfigurator() | |
/// Declared for NSVisualEffectView; affects non-contentView backdrops. | |
@objc private func _shouldAutoFlattenLayerTree() -> Bool { | |
return false | |
} | |
/// Controls key `NSWindow` operations if the `BackdropView` is its `contentView`. | |
private struct WindowConfigurator { | |
private var observer: Any? = nil | |
private var shouldAutoFlattenLayerTree = true | |
private var canHostLayersInWindowServer = true | |
private var isOpaque = false | |
private var backgroundColor: NSColor? = nil | |
/// Call upon migration to a new window. | |
mutating func apply(to newWindow: NSWindow) { | |
let cid = NSApp.value(forKey: "contextID") as! Int32 | |
self.shouldAutoFlattenLayerTree = newWindow.value(forKey: "shouldAutoFlattenLayerTree") as? Bool ?? true | |
self.canHostLayersInWindowServer = newWindow.value(forKey: "canHostLayersInWindowServer") as? Bool ?? true | |
self.isOpaque = newWindow.isOpaque | |
self.backgroundColor = newWindow.backgroundColor | |
// The WindowServer automatically flattens the render layer tree on its | |
// end after a delayed duration (currently 1.05s). This is likely to | |
// allow higher performance in windows that don't require effects. | |
newWindow.setValue(false, forKey: "shouldAutoFlattenLayerTree") | |
// `CGSSetSurfaceLayerBackingOptions` needs to be set to prevent layer | |
// tree flattening, and `NSWindow` doesn't inform its `NSViewLayerSurface`s | |
// to do this, EXCEPT upon initial surface creation, which happens | |
// during the first call to `-[NSWindow displayIfNeeded]`, where the layer | |
// tree is set up to match the `NSView` tree. | |
// | |
// A possible fix would be to grab "borderView.layerSurface.surface.surfaceID" | |
// and call `CGSSetSurfaceLayerBackingOptions` ourselves, but we don't | |
// consider currently set AppKit defaults. | |
// | |
// Instead, a simple workaround is to toggle `canHostLayersInWindowServer` | |
// off and back on again, as this recreates the layer tree immediately | |
// in both cases. This is, however, an "expensive" operation, but we | |
// don't expect to be swapping `contentView` in and out rapidly anyway. | |
newWindow.setValue(false, forKey: "canHostLayersInWindowServer") | |
newWindow.setValue(true, forKey: "canHostLayersInWindowServer") | |
// If the window is not opaque, the `CABackdropLayer` cannot sample behind it. | |
newWindow.isOpaque = false | |
// If the window's `backgroundColor` is `.clear`, the theme frame/`borderView` | |
// will unfortunately turn off corner masking, which then causes terrible | |
// window resize lag. This is likely because without a mask, WindowServer | |
// recomputes the "real shape" for any non-opaque windows. | |
newWindow.backgroundColor = NSColor.white.withAlphaComponent(0.001) | |
// The kCGSNeverFlattenSurfacesDuringSwipesTagBit tells WindowServer to | |
// not flatten the layer tree on its end, during Spaces swipes. | |
let fixSurfaces: () -> () = { [weak newWindow] in | |
guard let newWindow = newWindow else { return } | |
var x: [Int32] = [0x0, (1 << 23)/*kCGSNeverFlattenSurfacesDuringSwipesTagBit?*/] | |
_ = CGSSetWindowTags(cid, Int32(newWindow.windowNumber), | |
&x, 0x40/*kCGSRealMaximumTagSize*/) | |
} | |
// Since `_startLiveResize` and the balanced `_endLiveResize` calls made | |
// to `NSWindow` add and then reset this tag, respectively, we want to | |
// make sure we restore it ourselves upon `_endLiveResize` using this note. | |
DispatchQueue.main.async(execute: fixSurfaces) | |
self.observer = NotificationCenter.default.addObserver(forName: NSWindow.didEndLiveResizeNotification, object: newWindow, queue: nil) { _ in | |
DispatchQueue.main.async(execute: fixSurfaces) | |
} | |
var transformed = false | |
let flushSurfaces: () -> () = { [weak newWindow] in | |
guard let newWindow = newWindow else { return } | |
let wid = Int32(newWindow.windowNumber) | |
var q = CGAffineTransform.identity, p = CGAffineTransform.identity | |
CGSGetCatenatedWindowTransform(cid, wid, &q) | |
let _transformed = !(q.a == p.a && q.b == p.b && q.c == p.c && q.d == p.d) | |
if (transformed != _transformed) && _transformed /* transformed */ { | |
//DispatchQueue.main.async { | |
NotificationCenter.default.post(name: BackdropView.layerSurfaceFlattenedNotification, | |
object: nil, userInfo: ["window": newWindow, "proxy": true]) | |
//} | |
} else if (transformed != _transformed) && !_transformed /* untransformed */ { | |
// Flush the layer surface, as we were just un-transformed, and | |
// WindowServer doesn't do this for us. | |
if let sid = newWindow.value(forKeyPath: "borderView.layerSurface.surface.surfaceID") as? Int32 { | |
CGSFlushSurface(cid, wid, sid, 0) | |
} | |
//DispatchQueue.main.async { | |
NotificationCenter.default.post(name: BackdropView.layerSurfaceFlushedNotification, | |
object: nil, userInfo: ["window": newWindow, "proxy": false]) | |
//} | |
} | |
transformed = _transformed | |
} | |
// Wrap into a "timer" of some sort. | |
func follow() { | |
flushSurfaces() | |
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500), execute: follow) | |
} | |
DispatchQueue.global().async(execute: follow) | |
} | |
/// Call upon migration away from an existing window. | |
mutating func unapply(from oldWindow: NSWindow) { | |
// See the above notes for the particular order of operations. | |
oldWindow.setValue(self.shouldAutoFlattenLayerTree, forKey: "shouldAutoFlattenLayerTree") | |
oldWindow.setValue(false, forKey: "canHostLayersInWindowServer") | |
oldWindow.setValue(self.canHostLayersInWindowServer, forKey: "canHostLayersInWindowServer") | |
oldWindow.isOpaque = self.isOpaque | |
oldWindow.backgroundColor = self.backgroundColor | |
// There's no need to clear the kCGSNeverFlattenSurfacesDuringSwipesTagBit | |
// window tag, as the window will manage that itself upon resize. | |
NotificationCenter.default.removeObserver(self.observer!) | |
} | |
} | |
/// Emitted when we detect that our containing window was transformed. | |
/// As of macOS 13, all layer surfaces are forcibly flattened when a window is transformed. | |
/// | |
/// `userInfo` keys: | |
/// - `window`: the window. | |
/// - `proxy`: boolean indicating CAProxyLayer usage. | |
private static let layerSurfaceFlattenedNotification = Notification.Name("BackdropView.layerSurfaceFlattenedNotification") | |
/// Emitted when we flush the layer surface after our containing window was un-transformed. | |
/// | |
/// `userInfo` keys: | |
/// - `window`: the window. | |
/// - `proxy`: boolean indicating CAProxyLayer usage. | |
private static let layerSurfaceFlushedNotification = Notification.Name("BackdropView.layerSurfaceFlushedNotification") | |
} | |
@_silgen_name("CGSSetWindowTags") | |
func CGSSetWindowTags(_ cid: Int32, _ wid: Int32, _ tags: UnsafePointer<Int32>!, _ maxTagSize: Int) -> CGError |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What should be imported? I am getting so many errors.