perf: use calayer instead of nsview for lightimageview
This commit is contained in:
1 parent
3a8c0f3bee
commit
7dec9da401
5 files changed
+148
-99
No files matched your search
@@ -154,6 +154,7 @@
|
||||
BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */; };
|
||||
BF0C898511686611E4D7D81E /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B2C057099EDABE46779 /* Button.swift */; };
|
||||
BF0C8991399A3277A9A02FFA /* LightImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8E63BE7394A6591E74E0 /* LightImageView.swift */; };
|
||||
BF0C8992399A3277A9A02FFB /* LightImageLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8E64BE7394A6591E74E1 /* LightImageLayer.swift */; };
|
||||
BF0C89B4B17D7843C32A102E /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8353097840D7DB1E0E2E /* Markdown.swift */; };
|
||||
BF0C89C49CFBAAB4646BB8CB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C82A8848224E76B880A83 /* InfoPlist.strings */; };
|
||||
BF0C89F0632E0A2EC52D9168 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C87749F1A64E2D3305181 /* InfoPlist.strings */; };
|
||||
@@ -547,6 +548,7 @@
|
||||
BF0C8E54658B4DAEA0528200 /* menubar-2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-2@2x.png"; sourceTree = "<group>"; };
|
||||
BF0C8E5D2BD5F7F93DF77DF5 /* sk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sk; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C8E63BE7394A6591E74E0 /* LightImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightImageView.swift; sourceTree = "<group>"; };
|
||||
BF0C8E64BE7394A6591E74E1 /* LightImageLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightImageLayer.swift; sourceTree = "<group>"; };
|
||||
BF0C8E7B087DCC97A454C0AE /* uk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = uk; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C8E94CDFB672663333546 /* lt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = lt; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C8E9D0B36A8872D5B7653 /* nb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nb; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@@ -1503,6 +1505,7 @@
|
||||
1C9617C5839903D752070CF2 /* ImageTextButtonView.swift */,
|
||||
BF0C896FD4907CD85D62C124 /* WindowlessAppIndicator.swift */,
|
||||
BF0C8E63BE7394A6591E74E0 /* LightImageView.swift */,
|
||||
BF0C8E64BE7394A6591E74E1 /* LightImageLayer.swift */,
|
||||
BF0C8DC2A355E29516DD2FAF /* CustomRecorderControlTestable.swift */,
|
||||
);
|
||||
path = "generic-components";
|
||||
@@ -2540,6 +2543,7 @@
|
||||
BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */,
|
||||
BF0C8521DA3078516615C414 /* HelperExtensionsTestable.swift in Sources */,
|
||||
BF0C8991399A3277A9A02FFA /* LightImageView.swift in Sources */,
|
||||
BF0C8992399A3277A9A02FFB /* LightImageLayer.swift in Sources */,
|
||||
BF0C8F40C2D0FF7601D926D3 /* LightweightTimer.swift in Sources */,
|
||||
BF0C88A9AB286CB7CDC1019E /* MainMenu.swift in Sources */,
|
||||
BF0C826C7F8C5F68732FD75B /* DebugMenu.swift in Sources */,
|
||||
|
||||
@@ -89,6 +89,22 @@ extension NSColor {
|
||||
}
|
||||
}
|
||||
|
||||
extension CALayer {
|
||||
func centerInSuperlayer(x: Bool = false, y: Bool = false) {
|
||||
guard let superlayer else { return }
|
||||
if x { frame.origin.x = ((superlayer.bounds.width - frame.width) / 2).rounded() }
|
||||
if y { frame.origin.y = ((superlayer.bounds.height - frame.height) / 2).rounded() }
|
||||
}
|
||||
|
||||
func applyShadow(_ shadow: NSShadow?) {
|
||||
guard let shadow else { shadowOpacity = 0; return }
|
||||
shadowColor = shadow.shadowColor?.cgColor
|
||||
shadowOffset = shadow.shadowOffset
|
||||
shadowRadius = shadow.shadowBlurRadius
|
||||
shadowOpacity = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
extension NSView {
|
||||
// constrain size to fittingSize
|
||||
func fit() {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import Cocoa
|
||||
|
||||
/// this is a lightweight CALayer which displays an image
|
||||
/// it is an alternative to NSView-based image display, avoiding AppKit overhead (layout recursion, responder chain, drag-and-drop)
|
||||
class LightImageLayer: CALayer {
|
||||
private var outlineLayer: CAShapeLayer!
|
||||
private var handRaisedLayer: CALayer!
|
||||
private let withTransparencyChecks: Bool
|
||||
|
||||
init(withTransparencyChecks: Bool = false) {
|
||||
self.withTransparencyChecks = withTransparencyChecks
|
||||
super.init()
|
||||
contentsGravity = .resize
|
||||
magnificationFilter = .trilinear
|
||||
minificationFilter = .trilinear
|
||||
minificationFilterBias = 0.0
|
||||
shouldRasterize = false
|
||||
delegate = NoAnimationDelegate.shared
|
||||
setupOutlineLayer()
|
||||
setupHandRaisedLayer()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Class only supports programmatic initialization")
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
let other = layer as? LightImageLayer
|
||||
self.withTransparencyChecks = other?.withTransparencyChecks ?? false
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
private func setupOutlineLayer() {
|
||||
outlineLayer = CAShapeLayer()
|
||||
outlineLayer.name = "outline"
|
||||
outlineLayer.fillColor = nil
|
||||
outlineLayer.lineWidth = 1.0
|
||||
outlineLayer.lineDashPattern = [4, 3]
|
||||
outlineLayer.lineCap = .round
|
||||
outlineLayer.lineJoin = .round
|
||||
outlineLayer.isHidden = true
|
||||
addSublayer(outlineLayer)
|
||||
}
|
||||
|
||||
private func setupHandRaisedLayer() {
|
||||
guard #available(macOS 11.0, *) else { return }
|
||||
handRaisedLayer = CALayer()
|
||||
handRaisedLayer.contentsGravity = .resizeAspect
|
||||
handRaisedLayer.isHidden = true
|
||||
addSublayer(handRaisedLayer)
|
||||
}
|
||||
|
||||
private func updateOutlineLayer(_ fullyTransparent: Bool) {
|
||||
outlineLayer.isHidden = !fullyTransparent
|
||||
guard fullyTransparent else { return }
|
||||
let inset: CGFloat = 0.5
|
||||
let radius: CGFloat = 8.0
|
||||
outlineLayer.frame = bounds
|
||||
let rect = bounds.insetBy(dx: inset, dy: inset)
|
||||
let path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
|
||||
outlineLayer.path = path
|
||||
outlineLayer.strokeColor = Appearance.fontColor.cgColor
|
||||
}
|
||||
|
||||
private func updateHandRaisedLayer(_ fullyTransparent: Bool) {
|
||||
guard #available(macOS 11.0, *) else { return }
|
||||
handRaisedLayer.isHidden = !fullyTransparent
|
||||
guard fullyTransparent else { return }
|
||||
let minSize = min(Appearance.iconSize, bounds.width * 0.9, bounds.height * 0.9)
|
||||
let maxSize = Appearance.iconSize * 3
|
||||
let ratio = min(bounds.width / ThumbnailView.maxThumbnailWidth(), bounds.height / ThumbnailView.maxThumbnailHeight())
|
||||
let pointSize = max(minSize, maxSize * ratio)
|
||||
let config = NSImage.SymbolConfiguration(pointSize: pointSize, weight: .regular)
|
||||
guard let image = NSImage(systemSymbolName: "hand.raised.fill", accessibilityDescription: nil)?.withSymbolConfiguration(config) else { return }
|
||||
handRaisedLayer.contents = image.tinted(Appearance.fontColor)
|
||||
handRaisedLayer.bounds = CGRect(x: 0, y: 0, width: pointSize, height: pointSize)
|
||||
handRaisedLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
}
|
||||
|
||||
func updateContents(_ caLayerContents: CALayerContents, _ size: NSSize) {
|
||||
var fullyTransparent = false
|
||||
switch caLayerContents {
|
||||
case .cgImage(let image?):
|
||||
contents = image
|
||||
if withTransparencyChecks {
|
||||
fullyTransparent = image.isFullyTransparent()
|
||||
}
|
||||
case .pixelBuffer(let pixelBuffer?):
|
||||
contents = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue()
|
||||
default: break
|
||||
}
|
||||
if frame.size != size {
|
||||
frame.size = size
|
||||
}
|
||||
if withTransparencyChecks {
|
||||
updateOutlineLayer(fullyTransparent)
|
||||
updateHandRaisedLayer(fullyTransparent)
|
||||
}
|
||||
}
|
||||
|
||||
func releaseImage() {
|
||||
contents = nil
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1,35 @@
|
||||
import Cocoa
|
||||
|
||||
/// this is a lightweight view which displays an image using its CALayer
|
||||
/// it is an alternative to NSImageView, which doesn't have internal complexities and performance costs
|
||||
/// thin NSView wrapper around LightImageLayer, for use where AppKit requires an NSView (contentView, Auto Layout, NSStackView)
|
||||
class LightImageView: NSView {
|
||||
private var outlineLayer: CAShapeLayer!
|
||||
private var handRaisedLayer: CALayer!
|
||||
private let withTransparencyChecks: Bool
|
||||
let imageLayer: LightImageLayer
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Class only supports programmatic initialization")
|
||||
}
|
||||
|
||||
init(frame frameRect: NSRect = .zero, withTransparencyChecks: Bool = false) {
|
||||
self.withTransparencyChecks = withTransparencyChecks
|
||||
imageLayer = LightImageLayer(withTransparencyChecks: withTransparencyChecks)
|
||||
super.init(frame: frameRect)
|
||||
wantsLayer = true
|
||||
// configure the layer for efficient GPU-scaling
|
||||
layer!.contentsGravity = .resize
|
||||
layer!.magnificationFilter = .trilinear
|
||||
layer!.minificationFilter = .trilinear
|
||||
layer!.minificationFilterBias = 0.0
|
||||
layer!.shouldRasterize = false
|
||||
layer!.delegate = NoAnimationDelegate.shared
|
||||
layer!.addSublayer(imageLayer)
|
||||
layerContentsRedrawPolicy = .never
|
||||
setupOutlineLayer()
|
||||
setupHandRaisedLayer()
|
||||
}
|
||||
|
||||
private func setupOutlineLayer() {
|
||||
outlineLayer = CAShapeLayer()
|
||||
outlineLayer.name = "outline"
|
||||
outlineLayer.fillColor = nil
|
||||
outlineLayer.lineWidth = 1.0
|
||||
outlineLayer.lineDashPattern = [4, 3]
|
||||
outlineLayer.lineCap = .round
|
||||
outlineLayer.lineJoin = .round
|
||||
outlineLayer.isHidden = true
|
||||
layer!.addSublayer(outlineLayer)
|
||||
override func layout() {
|
||||
super.layout()
|
||||
imageLayer.frame = bounds
|
||||
}
|
||||
|
||||
private func setupHandRaisedLayer() {
|
||||
guard #available(macOS 11.0, *) else { return }
|
||||
handRaisedLayer = CALayer()
|
||||
handRaisedLayer.contentsGravity = .resizeAspect
|
||||
handRaisedLayer.isHidden = true
|
||||
layer!.addSublayer(handRaisedLayer)
|
||||
}
|
||||
|
||||
|
||||
private func updateOutlineLayer(_ fullyTransparent: Bool) {
|
||||
outlineLayer.isHidden = !fullyTransparent
|
||||
guard fullyTransparent else { return }
|
||||
let inset: CGFloat = 0.5
|
||||
let radius: CGFloat = 8.0
|
||||
let bounds = outlineLayer.superlayer!.bounds
|
||||
outlineLayer.frame = bounds
|
||||
let rect = bounds.insetBy(dx: inset, dy: inset)
|
||||
let path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil)
|
||||
outlineLayer.path = path
|
||||
outlineLayer.strokeColor = Appearance.fontColor.cgColor
|
||||
}
|
||||
|
||||
private func updateHandRaisedLayer(_ fullyTransparent: Bool) {
|
||||
guard #available(macOS 11.0, *) else { return }
|
||||
handRaisedLayer.isHidden = !fullyTransparent
|
||||
guard fullyTransparent else { return }
|
||||
// we want the icon to be large but not too large, and readable at small sizes
|
||||
let minSize = min(Appearance.iconSize, bounds.width * 0.9, bounds.height * 0.9)
|
||||
let maxSize = Appearance.iconSize * 3
|
||||
let ratio = min(bounds.width / ThumbnailView.maxThumbnailWidth(), bounds.height / ThumbnailView.maxThumbnailHeight())
|
||||
let pointSize = max(minSize, maxSize * ratio)
|
||||
let config = NSImage.SymbolConfiguration(pointSize: pointSize, weight: .regular)
|
||||
guard let image = NSImage(systemSymbolName: "hand.raised.fill", accessibilityDescription: nil)?.withSymbolConfiguration(config) else { return }
|
||||
handRaisedLayer.contents = image.tinted(Appearance.fontColor)
|
||||
handRaisedLayer.bounds = CGRect(x: 0, y: 0, width: pointSize, height: pointSize)
|
||||
handRaisedLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
}
|
||||
|
||||
|
||||
func updateContents(_ caLayerContents: CALayerContents, _ size: NSSize) {
|
||||
var fullyTransparent = false
|
||||
switch caLayerContents {
|
||||
case .cgImage(let image?):
|
||||
layer!.contents = image
|
||||
if withTransparencyChecks {
|
||||
fullyTransparent = image.isFullyTransparent()
|
||||
}
|
||||
case .pixelBuffer(let pixelBuffer?):
|
||||
layer!.contents = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue()
|
||||
default: break
|
||||
}
|
||||
imageLayer.updateContents(caLayerContents, size)
|
||||
if frame.size != size {
|
||||
frame.size = size
|
||||
}
|
||||
if withTransparencyChecks {
|
||||
updateOutlineLayer(fullyTransparent)
|
||||
updateHandRaisedLayer(fullyTransparent)
|
||||
}
|
||||
}
|
||||
|
||||
func releaseImage() {
|
||||
layer!.contents = nil
|
||||
imageLayer.releaseImage()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ class ThumbnailView: FlippedView {
|
||||
static let extraTextForPadding = "lmnopqrstuvw"
|
||||
|
||||
var window_: Window?
|
||||
var thumbnail = LightImageView(withTransparencyChecks: true)
|
||||
var appIcon = LightImageView()
|
||||
var thumbnail = LightImageLayer(withTransparencyChecks: true)
|
||||
var appIcon = LightImageLayer()
|
||||
var label = ThumbnailTitleView(font: Appearance.font)
|
||||
var statusIcons = StatusIconsView()
|
||||
var dockLabelIcon = ThumbnailFilledFontIconView(
|
||||
@@ -165,13 +165,13 @@ class ThumbnailView: FlippedView {
|
||||
|
||||
private func setupSharedSubviews() {
|
||||
let shadow = ThumbnailView.makeShadow(Appearance.imagesShadowColor)
|
||||
thumbnail.layer!.masksToBounds = false // let thumbnail shadows show
|
||||
thumbnail.shadow = shadow
|
||||
appIcon.shadow = shadow
|
||||
thumbnail.masksToBounds = false // let thumbnail shadows show
|
||||
thumbnail.applyShadow(shadow)
|
||||
appIcon.applyShadow(shadow)
|
||||
dockLabelIcon.shadow = shadow
|
||||
appIcon.setSubviewAbove(dockLabelIcon)
|
||||
layer!.addSublayer(appIcon)
|
||||
addSubview(dockLabelIcon)
|
||||
label.fixHeight()
|
||||
setSubviews([appIcon])
|
||||
}
|
||||
|
||||
private func setupStyleSpecificSubviews() {
|
||||
@@ -181,8 +181,9 @@ class ThumbnailView: FlippedView {
|
||||
label.alignment = .center
|
||||
label.isHidden = true
|
||||
} else if Preferences.appearanceStyle == .thumbnails {
|
||||
addSubviews([thumbnail, label, statusIcons])
|
||||
thumbnail.setSubviewAbove(windowlessAppIndicator)
|
||||
layer!.addSublayer(thumbnail)
|
||||
addSubviews([label, statusIcons])
|
||||
setSubviewAbove(windowlessAppIndicator)
|
||||
} else {
|
||||
setSubviewAbove(windowlessAppIndicator)
|
||||
addSubviews([label, statusIcons])
|
||||
@@ -260,7 +261,6 @@ class ThumbnailView: FlippedView {
|
||||
private func updateAppIcon(_ element: Window, _ title: String) {
|
||||
let appIconSize = ThumbnailView.iconSize()
|
||||
appIcon.updateContents(.cgImage(element.icon), appIconSize)
|
||||
appIcon.setAccessibilityLabel(title)
|
||||
}
|
||||
|
||||
private func updateValues(_ element: Window, _ index: Int, _ newHeight: CGFloat) {
|
||||
@@ -275,7 +275,6 @@ class ThumbnailView: FlippedView {
|
||||
)
|
||||
))
|
||||
)
|
||||
thumbnail.toolTip = element.isWindowlessApp ? ThumbnailView.noOpenWindowToolTip : nil
|
||||
if !thumbnail.isHidden {
|
||||
if let screenshot = element.thumbnail {
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(element.size, false)
|
||||
@@ -285,8 +284,6 @@ class ThumbnailView: FlippedView {
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(element.icon?.size(), true)
|
||||
thumbnail.updateContents(.cgImage(element.icon), thumbnailSize)
|
||||
}
|
||||
// for Accessibility > "speak items under the pointer"
|
||||
thumbnail.setAccessibilityLabel(element.title)
|
||||
}
|
||||
let title = getAppOrAndWindowTitle()
|
||||
let labelChanged = label.stringValue != title
|
||||
@@ -342,10 +339,12 @@ class ThumbnailView: FlippedView {
|
||||
if Preferences.appearanceStyle == .thumbnails {
|
||||
let hHeight = max(appIcon.frame.height, ThumbnailsView.layoutCache.labelHeight)
|
||||
assignIfDifferent(&thumbnail.frame.origin, NSPoint(x: edgeInsets, y: edgeInsets + hHeight + Appearance.intraCellPadding))
|
||||
thumbnail.centerFrameInParent(x: true)
|
||||
thumbnail.centerInSuperlayer(x: true)
|
||||
}
|
||||
if !windowlessAppIndicator.isHidden {
|
||||
if Preferences.appearanceStyle != .titles {
|
||||
if Preferences.appearanceStyle == .thumbnails {
|
||||
windowlessAppIndicator.frame.origin.x = thumbnail.frame.origin.x + ((thumbnail.frame.width - windowlessAppIndicator.frame.width) / 2).rounded()
|
||||
} else if Preferences.appearanceStyle != .titles {
|
||||
windowlessAppIndicator.centerFrameInParent(x: true)
|
||||
} else {
|
||||
windowlessAppIndicator.frame.origin.x = ((appIcon.frame.width / 2) - (windowlessAppIndicator.frame.width / 2)).rounded()
|
||||
@@ -354,7 +353,7 @@ class ThumbnailView: FlippedView {
|
||||
if Preferences.appearanceStyle != .thumbnails {
|
||||
windowlessAppIndicator.frame.origin.y = windowlessAppIndicator.superview!.frame.height - windowlessAppIndicator.frame.height + 5
|
||||
} else {
|
||||
windowlessAppIndicator.frame.origin.y = -5
|
||||
windowlessAppIndicator.frame.origin.y = thumbnail.frame.origin.y - 5
|
||||
}
|
||||
}
|
||||
// we set dockLabelIcon origin, without checking if .isHidden
|
||||
@@ -432,9 +431,6 @@ class ThumbnailView: FlippedView {
|
||||
}
|
||||
|
||||
private func observeDragAndDrop() {
|
||||
// NSImageView instances are registered to drag-and-drop by default
|
||||
thumbnail.unregisterDraggedTypes()
|
||||
appIcon.unregisterDraggedTypes()
|
||||
// we only handle URLs (i.e. not text, image, or other draggable things)
|
||||
registerForDraggedTypes([NSPasteboard.PasteboardType(kUTTypeURL as String)])
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user