import AppKit import ShotdeckCore @MainActor public final class RegionPickerController { private var windows: [RegionPickerWindow] = [] private var monitor: Any? private var completion: (@MainActor (CaptureRegion?) -> Void)? private var dragStart: NSPoint? private var startScreen: NSScreen? private var cursorPushed = false public init() {} /// Presents one overlay per attached screen. A pick already in progress is cancelled /// (its completion called with nil) before the new one starts — never leaves a caller /// waiting forever on a dropped re-entrant call. public func pick(completion: @escaping @MainActor (CaptureRegion?) -> Void) { if self.completion != nil { finish(region: nil) } self.completion = completion presentOverlays() } private func presentOverlays() { let screens = NSScreen.screens guard !screens.isEmpty else { finish(region: nil) return } NSCursor.crosshair.push() cursorPushed = true windows = screens.map { RegionPickerWindow(screen: $0) } for window in windows { window.orderFrontRegardless() } // Nonactivating panel can become key without activating the app; needed so // Escape reaches the local monitor immediately, before any mouse click. let mouse = NSEvent.mouseLocation let keyWindow = windows.first(where: { $0.coveringScreen.frame.contains(mouse) }) ?? windows[0] keyWindow.makeKey() monitor = NSEvent.addLocalMonitorForEvents( matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp, .keyDown] ) { [weak self] event in guard let self else { return event } // NSEvent is not Sendable; lift the two Sendable fields the handler // needs. Mouse geometry is read from NSEvent.mouseLocation on the // main thread inside the isolated methods, per spec. let type = event.type let keyCode = event.keyCode let consume = MainActor.assumeIsolated { self.handle(type: type, keyCode: keyCode) } return consume ? nil : event } } /// Returns `true` when the event should be swallowed. private func handle(type: NSEvent.EventType, keyCode: UInt16) -> Bool { switch type { case .keyDown: if keyCode == 53 { // kVK_Escape finish(region: nil) return true } return false case .leftMouseDown: handleMouseDown() return false case .leftMouseDragged: handleMouseDragged() return false case .leftMouseUp: handleMouseUp() return false default: return false } } private func handleMouseDown() { let point = NSEvent.mouseLocation dragStart = point startScreen = NSScreen.screens.first(where: { $0.frame.contains(point) }) ?? NSScreen.screens.first guard let startScreen else { return } let local = Self.localRect( from: CGRect(origin: point, size: .zero).intersection(startScreen.frame), on: startScreen ) window(for: startScreen)?.updateSelection(localRect: local, sizeText: "0 x 0") for window in windows where window.coveringScreen !== startScreen { window.updateSelection(localRect: nil, sizeText: nil) } } private func handleMouseDragged() { guard dragStart != nil, startScreen != nil else { return } applyLiveSelection() } private func handleMouseUp() { guard let startScreen else { dragStart = nil return } guard let rect = currentClampedRect(), rect.width >= 8, rect.height >= 8 else { // Mis-click: reset drag state, leave every window open and fully dimmed. dragStart = nil self.startScreen = nil for window in windows { window.updateSelection(localRect: nil, sizeText: nil) } return } let region = CaptureRegion.fromAppKit(rect: rect, on: startScreen) finish(region: region) } private func applyLiveSelection() { guard let startScreen, let rect = currentClampedRect() else { return } let local = Self.localRect(from: rect, on: startScreen) let text = "\(Int(rect.width)) x \(Int(rect.height))" window(for: startScreen)?.updateSelection(localRect: local, sizeText: text) for window in windows where window.coveringScreen !== startScreen { window.updateSelection(localRect: nil, sizeText: nil) } } /// Normalize the drag (so bottom-right → up-left is not misjudged) then clamp /// to the screen the gesture started on — never selects across displays. private func currentClampedRect() -> CGRect? { guard let dragStart, let startScreen else { return nil } let current = NSEvent.mouseLocation let normalized = CGRect( x: min(dragStart.x, current.x), y: min(dragStart.y, current.y), width: abs(current.x - dragStart.x), height: abs(current.y - dragStart.y) ) return normalized.intersection(startScreen.frame) } private func window(for screen: NSScreen) -> RegionPickerWindow? { windows.first { $0.coveringScreen === screen } } private static func localRect(from rect: CGRect, on screen: NSScreen) -> CGRect { CGRect( x: rect.minX - screen.frame.minX, y: rect.minY - screen.frame.minY, width: rect.width, height: rect.height ) } private func finish(region: CaptureRegion?) { if let monitor { NSEvent.removeMonitor(monitor) self.monitor = nil } if cursorPushed { NSCursor.pop() cursorPushed = false } for window in windows { window.orderOut(nil) } windows.removeAll() dragStart = nil startScreen = nil let done = completion completion = nil done?(region) } }