WP-3b: ScreenCaptureKit capturer + region picker overlay per SPEC-A2a
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user