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 Sendable fields the handler needs. // Prefer the event's window-local point converted to global AppKit // coordinates; NSEvent.mouseLocation is only a fallback. // keyCode is only valid on key events — reading it on a mouse event raises. let type = event.type let keyCode: UInt16 = (type == .keyDown) ? event.keyCode : 0 let locationInWindow = event.locationInWindow let windowNumber = event.windowNumber let consume = MainActor.assumeIsolated { let location = self.globalAppKitLocation( locationInWindow: locationInWindow, windowNumber: windowNumber ) return self.handle(type: type, keyCode: keyCode, location: location) } return consume ? nil : event } } /// Returns `true` when the event should be swallowed. private func handle(type: NSEvent.EventType, keyCode: UInt16, location: NSPoint?) -> Bool { switch type { case .keyDown: if keyCode == 53 { // kVK_Escape finish(region: nil) return true } return false case .leftMouseDown: handleMouseDown(location: location) return false case .leftMouseDragged: handleMouseDragged(location: location) return false case .leftMouseUp: handleMouseUp(location: location) return false default: return false } } private func handleMouseDown(location: NSPoint?) { let point = location ?? 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(location: NSPoint?) { guard dragStart != nil, startScreen != nil else { return } applyLiveSelection(current: location) } private func handleMouseUp(location: NSPoint?) { guard let startScreen else { dragStart = nil return } guard let rect = currentClampedRect(current: location), 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(current: NSPoint?) { guard let startScreen, let rect = currentClampedRect(current: current) 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(current: NSPoint?) -> CGRect? { guard let dragStart, let startScreen else { return nil } let current = 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) } /// Convert a monitored event's `locationInWindow` into global AppKit coordinates /// (bottom-left origin, matching `NSEvent.mouseLocation` / `NSScreen.frame`). private func globalAppKitLocation(locationInWindow: NSPoint, windowNumber: Int) -> NSPoint? { if let window = windows.first(where: { $0.windowNumber == windowNumber }) { return window.convertPoint(toScreen: locationInWindow) } if windowNumber != 0, let window = NSApp.window(withWindowNumber: windowNumber) { return window.convertPoint(toScreen: locationInWindow) } if windowNumber == 0 { return locationInWindow } return nil } 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) } }