Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

234 lines
7.3 KiB
Swift

import AppKit
import SwiftUI
@MainActor
final class ScreenshotOverlaySession: ScreenshotOverlaySelecting {
private var windows: [any ScreenshotOverlayWindowManaging] = []
private var closingWindows: [any ScreenshotOverlayWindowManaging] = []
private var continuation: CheckedContinuation<ScreenshotOverlaySelectionResult, Never>?
private let hasAvailableScreens: () -> Bool
private let activateApplication: () -> Void
private let overlayWindowPresenter: (@escaping (ScreenshotOverlaySelectionResult) -> Void) -> [any ScreenshotOverlayWindowManaging]
init(
hasAvailableScreens: @escaping () -> Bool = { !NSScreen.screens.isEmpty },
activateApplication: @escaping () -> Void = { NSApp.activate(ignoringOtherApps: true) },
overlayWindowPresenter: (
(@escaping (ScreenshotOverlaySelectionResult) -> Void) -> [any ScreenshotOverlayWindowManaging]
)? = nil
) {
self.hasAvailableScreens = hasAvailableScreens
self.activateApplication = activateApplication
self.overlayWindowPresenter = overlayWindowPresenter ?? { completion in
Self.makeOverlayWindows(on: NSScreen.screens, completion: completion)
}
}
func selectRegion() async -> ScreenshotOverlaySelectionResult {
guard continuation == nil else {
return .failure(.cancelled)
}
guard hasAvailableScreens() else {
return .failure(.noScreen)
}
return await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
self.continuation = continuation
if Task.isCancelled {
self.complete(.failure(.cancelled))
return
}
self.showOverlayWindows()
}
} onCancel: {
Task { @MainActor [weak self] in
self?.complete(.failure(.cancelled))
}
}
}
private func showOverlayWindows() {
activateApplication()
windows = overlayWindowPresenter { [weak self] result in
self?.complete(result)
}
guard !windows.isEmpty else {
complete(.failure(.noScreen))
return
}
windows.forEach { $0.showOverlayWindow() }
windows.first?.makeOverlayKeyWindow()
}
private static func makeOverlayWindows(
on screens: [NSScreen],
completion: @escaping (ScreenshotOverlaySelectionResult) -> Void
) -> [any ScreenshotOverlayWindowManaging] {
screens.map { screen in
let window = ScreenshotOverlayWindow(
contentRect: screen.frame,
styleMask: [.borderless],
backing: .buffered,
defer: false,
screen: screen
)
window.level = .screenSaver
window.animationBehavior = .none
window.backgroundColor = .clear
window.isOpaque = false
window.isReleasedWhenClosed = false
window.hasShadow = false
window.ignoresMouseEvents = false
window.acceptsMouseMovedEvents = true
window.collectionBehavior = [
.canJoinAllSpaces,
.fullScreenAuxiliary,
.ignoresCycle,
.stationary,
]
window.onCancel = {
completion(.failure(.cancelled))
}
window.contentView = NSHostingView(
rootView: ScreenshotOverlayView(
screen: screen,
onComplete: completion
)
)
return window
}
}
private func complete(_ result: ScreenshotOverlaySelectionResult) {
guard let continuation else { return }
self.continuation = nil
let currentWindows = windows
windows = []
closingWindows.append(contentsOf: currentWindows)
currentWindows.forEach { window in
window.dismissOverlayWindow()
}
continuation.resume(returning: result)
DispatchQueue.main.async { [weak self] in
self?.closingWindows.removeAll()
}
}
}
@MainActor
protocol ScreenshotOverlayWindowManaging: AnyObject {
func showOverlayWindow()
func makeOverlayKeyWindow()
func dismissOverlayWindow()
}
private final class ScreenshotOverlayWindow: NSWindow, ScreenshotOverlayWindowManaging {
var onCancel: (() -> Void)?
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { true }
override func keyDown(with event: NSEvent) {
if event.keyCode == 53 {
onCancel?()
return
}
super.keyDown(with: event)
}
override func rightMouseDown(with event: NSEvent) {
onCancel?()
}
func showOverlayWindow() {
orderFrontRegardless()
}
func makeOverlayKeyWindow() {
makeKeyAndOrderFront(nil)
}
func dismissOverlayWindow() {
onCancel = nil
contentView = nil
orderOut(nil)
}
}
private struct ScreenshotOverlayView: View {
let screen: NSScreen
let onComplete: (ScreenshotOverlaySelectionResult) -> Void
@State private var dragStart: CGPoint?
@State private var dragCurrent: CGPoint?
var body: some View {
ZStack(alignment: .topLeading) {
Color.black.opacity(0.28)
.ignoresSafeArea()
if let selectionRect {
Rectangle()
.fill(Color.clear)
.background(.clear)
.overlay(
Rectangle()
.stroke(Color.white, lineWidth: 1.5)
)
.background(Color.white.opacity(0.12))
.frame(width: selectionRect.width, height: selectionRect.height)
.offset(x: selectionRect.minX, y: selectionRect.minY)
}
}
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { value in
if dragStart == nil {
dragStart = value.startLocation
}
dragCurrent = value.location
}
.onEnded { value in
let rect = normalizedRect(from: dragStart ?? value.startLocation, to: value.location)
dragStart = nil
dragCurrent = nil
guard rect.width >= 10, rect.height >= 10 else {
onComplete(.failure(.regionTooSmall))
return
}
onComplete(.success(ScreenshotOverlaySelection(screen: screen, selectedRect: rect)))
}
)
.onAppear {
NSCursor.crosshair.push()
}
.onDisappear {
NSCursor.pop()
}
}
private var selectionRect: CGRect? {
guard let dragStart, let dragCurrent else { return nil }
return normalizedRect(from: dragStart, to: dragCurrent)
}
private func normalizedRect(from start: CGPoint, to end: CGPoint) -> CGRect {
CGRect(
x: min(start.x, end.x),
y: min(start.y, end.y),
width: abs(start.x - end.x),
height: abs(start.y - end.y)
)
}
}