Files
shotdeck/Sources/Shotdeck/RegionPickerWindow.swift

111 lines
3.9 KiB
Swift

import AppKit
/// Full-screen dim overlay for one attached display. Subclasses `NSPanel` with
/// `.nonactivatingPanel` so it can accept mouse and key events without activating
/// Shotdeck or stealing the frontmost app beyond those events.
@MainActor
final class RegionPickerWindow: NSPanel {
let coveringScreen: NSScreen
private let pickerView: RegionPickerView
init(screen: NSScreen) {
self.coveringScreen = screen
self.pickerView = RegionPickerView(frame: CGRect(origin: .zero, size: screen.frame.size))
super.init(
contentRect: screen.frame,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
setFrame(screen.frame, display: false)
level = .screenSaver
isOpaque = false
backgroundColor = .clear
ignoresMouseEvents = false
hasShadow = false
isMovable = false
hidesOnDeactivate = false
isFloatingPanel = true
becomesKeyOnlyIfNeeded = false
animationBehavior = .none
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
contentView = pickerView
}
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { false }
/// `localRect` is in this window's coordinates (screen.frame origin subtracted).
/// Pass `nil` to restore the full dim with no punch and no size chip.
func updateSelection(localRect: CGRect?, sizeText: String?) {
pickerView.selectionLocalRect = localRect
pickerView.sizeChipText = sizeText
pickerView.needsDisplay = true
}
}
/// Draws ~35% black over the screen with an even-odd punch for the live selection,
/// a thin light border, and the size-readout chip.
@MainActor
private final class RegionPickerView: NSView {
var selectionLocalRect: CGRect?
var sizeChipText: String?
override var isOpaque: Bool { false }
override var acceptsFirstResponder: Bool { true }
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let dim = NSColor.black.withAlphaComponent(0.35)
let path = NSBezierPath(rect: bounds)
if let sel = selectionLocalRect, sel.width > 0, sel.height > 0 {
path.append(NSBezierPath(rect: sel))
path.windingRule = .evenOdd
}
dim.setFill()
path.fill()
guard let sel = selectionLocalRect, sel.width > 0, sel.height > 0 else { return }
NSColor.white.withAlphaComponent(0.85).setStroke()
let border = NSBezierPath(rect: sel)
border.lineWidth = 1
border.stroke()
if let text = sizeChipText {
drawSizeChip(text, above: sel)
}
}
private func drawSizeChip(_ text: String, above sel: CGRect) {
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
.foregroundColor: NSColor.white,
]
let nsText = text as NSString
let textSize = nsText.size(withAttributes: attrs)
let padX: CGFloat = 8
let padY: CGFloat = 4
let chipSize = CGSize(
width: ceil(textSize.width) + padX * 2,
height: ceil(textSize.height) + padY * 2
)
var origin = CGPoint(x: sel.minX, y: sel.maxY + 6)
if origin.y + chipSize.height > bounds.maxY {
origin.y = sel.minY - 6 - chipSize.height
}
origin.x = min(max(origin.x, bounds.minX + 4), bounds.maxX - chipSize.width - 4)
origin.y = min(max(origin.y, bounds.minY + 4), bounds.maxY - chipSize.height - 4)
let chipRect = CGRect(origin: origin, size: chipSize)
NSColor.black.withAlphaComponent(0.75).setFill()
NSBezierPath(roundedRect: chipRect, xRadius: 4, yRadius: 4).fill()
nsText.draw(
at: CGPoint(x: chipRect.minX + padX, y: chipRect.minY + padY),
withAttributes: attrs
)
}
}