diff --git a/src/api-wrappers/HelperExtensions.swift b/src/api-wrappers/HelperExtensions.swift index 295daed9..c57f7e56 100644 --- a/src/api-wrappers/HelperExtensions.swift +++ b/src/api-wrappers/HelperExtensions.swift @@ -198,6 +198,15 @@ extension NSImage { static func initCopy(_ name: String) -> NSImage { return NSImage(named: name)!.copy() as! NSImage } + + func tinted(_ color: NSColor) -> NSImage { + NSImage(size: size, flipped: false) { rect in + color.set() + rect.fill() + self.draw(in: rect, from: NSRect(origin: .zero, size: self.size), operation: .destinationIn, fraction: 1.0) + return true + } + } } extension CGImage { @@ -214,6 +223,23 @@ extension CGImage { func size() -> NSSize { return NSSize(width: width, height: height) } + + func iFullyTransparent() -> Bool { + guard ![.none, .noneSkipFirst, .noneSkipLast].contains(alphaInfo), + let provider = dataProvider, let data = provider.data, let ptr = CFDataGetBytePtr(data) + else { return false } + // Assumes: kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Little + // Layout: [B, G, R, A] + let length = CFDataGetLength(data) + var i = 3 + while i < length { + if ptr[i] != 0 { + return false + } + i += 4 + } + return true + } } extension CVPixelBuffer { diff --git a/src/ui/generic-components/LightImageView.swift b/src/ui/generic-components/LightImageView.swift index 7036fbd3..d45ee5f0 100644 --- a/src/ui/generic-components/LightImageView.swift +++ b/src/ui/generic-components/LightImageView.swift @@ -3,6 +3,9 @@ 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 class LightImageView: NSView { + private var outlineLayer: CAShapeLayer! + private var handRaisedLayer: CALayer! + required init?(coder: NSCoder) { fatalError("Class only supports programmatic initialization") } @@ -25,12 +28,67 @@ class LightImageView: NSView { "contentsScale": NSNull() ] 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) + } + + 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 + fullyTransparent = image.iFullyTransparent() case .pixelBuffer(let pixelBuffer?): layer!.contents = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() default: break @@ -38,6 +96,8 @@ class LightImageView: NSView { if frame.size != size { frame.size = size } + updateOutlineLayer(fullyTransparent) + updateHandRaisedLayer(fullyTransparent) } func releaseImage() {