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 ) } }