111 lines
3.9 KiB
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
|
|
)
|
|
}
|
|
}
|