Initial commit

This commit is contained in:
Alex E. Tafoya
2026-09-17 15:15:45 -05:00
commit 87fe7927c4
36 changed files with 5113 additions and 0 deletions
+380
View File
@@ -0,0 +1,380 @@
// CaptureOverlay.swift
// GreenshotMac
//
// The equivalent of Greenshot's CaptureForm: a frozen, dimmed copy of the desktop
// on which the user drags a region or clicks a window.
import AppKit
import ScreenCaptureKit
enum OverlayResult {
case region(DisplaySnapshot, CGRect) // rect in CoreGraphics global coordinates
case window(WindowInfo)
case cancelled
}
enum OverlayMode {
case region
case window
}
@MainActor
final class CaptureOverlayController {
private var windows: [OverlayWindow] = []
private let snapshots: [DisplaySnapshot]
private let windowInfos: [WindowInfo]
private var mode: OverlayMode
private var completion: ((OverlayResult) -> Void)?
private var finished = false
init(snapshots: [DisplaySnapshot], windows windowInfos: [WindowInfo], mode: OverlayMode) {
self.snapshots = snapshots
self.windowInfos = windowInfos
self.mode = mode
}
func present(completion: @escaping (OverlayResult) -> Void) {
self.completion = completion
for snapshot in snapshots {
let window = OverlayWindow(snapshot: snapshot, windows: windowInfos, mode: mode, controller: self)
windows.append(window)
window.orderFrontRegardless()
}
windows.first?.makeKey()
NSApp.activate(ignoringOtherApps: true)
}
/// Keeps every display's overlay in the same mode.
func setMode(_ newMode: OverlayMode) {
mode = newMode
for window in windows { window.overlayView.mode = newMode }
}
func toggleMode() {
setMode(mode == .region ? .window : .region)
}
func finish(_ result: OverlayResult) {
guard !finished else { return }
finished = true
for window in windows { window.orderOut(nil) }
windows.removeAll()
completion?(result)
completion = nil
}
}
// MARK: - Window
@MainActor
final class OverlayWindow: NSWindow {
let overlayView: OverlayView
init(snapshot: DisplaySnapshot, windows: [WindowInfo], mode: OverlayMode, controller: CaptureOverlayController) {
overlayView = OverlayView(snapshot: snapshot, windows: windows, mode: mode, controller: controller)
super.init(contentRect: snapshot.screen.frame,
styleMask: .borderless,
backing: .buffered,
defer: false)
isOpaque = false
backgroundColor = .clear
level = .screenSaver
hasShadow = false
ignoresMouseEvents = false
collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
acceptsMouseMovedEvents = true
contentView = overlayView
setFrame(snapshot.screen.frame, display: true)
}
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { true }
}
// MARK: - View
@MainActor
final class OverlayView: NSView {
private let snapshot: DisplaySnapshot
private let windowInfos: [WindowInfo]
private weak var controller: CaptureOverlayController?
private var backdrop: NSImage
private var anchor: CGPoint?
private var current: CGPoint?
private var mouseLocation: CGPoint = .zero
private var hoveredWindow: WindowInfo?
private var trackingArea: NSTrackingArea?
var mode: OverlayMode {
didSet {
hoveredWindow = nil
anchor = nil
current = nil
needsDisplay = true
}
}
init(snapshot: DisplaySnapshot, windows: [WindowInfo], mode: OverlayMode, controller: CaptureOverlayController) {
self.snapshot = snapshot
self.windowInfos = windows
self.mode = mode
self.controller = controller
self.backdrop = NSImage(cgImage: snapshot.image,
size: CGSize(width: snapshot.cgFrame.width, height: snapshot.cgFrame.height))
super.init(frame: CGRect(origin: .zero, size: snapshot.screen.frame.size))
wantsLayer = true
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
// MARK: Tracking and cursor
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea { removeTrackingArea(trackingArea) }
let area = NSTrackingArea(rect: bounds,
options: [.activeAlways, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect],
owner: self,
userInfo: nil)
addTrackingArea(area)
trackingArea = area
}
override func resetCursorRects() {
addCursorRect(bounds, cursor: .crosshair)
}
override var acceptsFirstResponder: Bool { true }
// MARK: Coordinate helpers
/// View point (bottom-left origin) to CoreGraphics global point (top-left origin).
private func cgPoint(from viewPoint: CGPoint) -> CGPoint {
CGPoint(x: snapshot.cgFrame.origin.x + viewPoint.x,
y: snapshot.cgFrame.origin.y + (bounds.height - viewPoint.y))
}
/// CoreGraphics global rect to a rect in this view.
private func viewRect(fromCG rect: CGRect) -> CGRect {
CGRect(x: rect.origin.x - snapshot.cgFrame.origin.x,
y: bounds.height - (rect.origin.y - snapshot.cgFrame.origin.y) - rect.height,
width: rect.width,
height: rect.height)
}
private var selectionRect: CGRect? {
guard let anchor, let current else { return nil }
let rect = CGRect(x: min(anchor.x, current.x),
y: min(anchor.y, current.y),
width: abs(current.x - anchor.x),
height: abs(current.y - anchor.y))
return rect.width >= 1 && rect.height >= 1 ? rect : nil
}
private var highlightRect: CGRect? {
switch mode {
case .region:
return selectionRect
case .window:
guard let hoveredWindow else { return nil }
return viewRect(fromCG: hoveredWindow.cgFrame).intersection(bounds)
}
}
// MARK: Mouse
override func mouseMoved(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
if mode == .window {
updateHoveredWindow()
}
needsDisplay = true
}
override func mouseDown(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
if mode == .window {
updateHoveredWindow()
if let hoveredWindow {
controller?.finish(.window(hoveredWindow))
}
return
}
anchor = mouseLocation
current = mouseLocation
needsDisplay = true
}
override func mouseDragged(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
if mode == .region {
if anchor == nil { anchor = mouseLocation }
current = mouseLocation
}
needsDisplay = true
}
override func mouseUp(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
guard mode == .region else { return }
current = mouseLocation
guard let rect = selectionRect, rect.width >= 3, rect.height >= 3 else {
anchor = nil
current = nil
needsDisplay = true
return
}
let topLeft = cgPoint(from: CGPoint(x: rect.minX, y: rect.maxY))
let cgRect = CGRect(x: topLeft.x.rounded(), y: topLeft.y.rounded(),
width: rect.width.rounded(), height: rect.height.rounded())
controller?.finish(.region(snapshot, cgRect))
}
override func keyDown(with event: NSEvent) {
switch event.keyCode {
case 53: // Escape
controller?.finish(.cancelled)
case 49: // Space
controller?.toggleMode()
default:
super.keyDown(with: event)
}
}
private func updateHoveredWindow() {
let point = cgPoint(from: mouseLocation)
hoveredWindow = windowInfos.first { $0.cgFrame.contains(point) }
}
// MARK: Drawing
override func draw(_ dirtyRect: CGRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
backdrop.draw(in: bounds, from: .zero, operation: .copy, fraction: 1.0)
let highlight = highlightRect
NSColor(calibratedWhite: 0, alpha: 0.42).setFill()
if let highlight {
// Dim everything but the highlighted area.
let path = NSBezierPath(rect: bounds)
path.append(NSBezierPath(rect: highlight))
path.windingRule = .evenOdd
path.fill()
NSColor.systemGreen.setStroke()
let border = NSBezierPath(rect: highlight.insetBy(dx: -0.5, dy: -0.5))
border.lineWidth = 1.5
border.stroke()
} else {
bounds.fill()
drawCrosshair(in: context)
}
if Configuration.shared.showSelectionSize, let highlight {
drawSizeLabel(for: highlight)
}
if Configuration.shared.showMagnifier, highlight == nil || mode == .region {
drawMagnifier()
}
if highlight == nil {
drawHint()
}
}
private func drawCrosshair(in context: CGContext) {
context.saveGState()
context.setStrokeColor(NSColor.systemGreen.withAlphaComponent(0.8).cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: 0, y: mouseLocation.y))
context.addLine(to: CGPoint(x: bounds.width, y: mouseLocation.y))
context.move(to: CGPoint(x: mouseLocation.x, y: 0))
context.addLine(to: CGPoint(x: mouseLocation.x, y: bounds.height))
context.strokePath()
context.restoreGState()
}
private func drawSizeLabel(for rect: CGRect) {
let scale = snapshot.scale
let text: String
switch mode {
case .region:
text = "\(Int(rect.width * scale)) × \(Int(rect.height * scale)) px"
case .window:
let title = hoveredWindow.map { $0.applicationName.isEmpty ? $0.title : $0.applicationName } ?? ""
text = "\(title) \(Int(rect.width * scale)) × \(Int(rect.height * scale)) px"
}
draw(label: text, near: CGPoint(x: rect.minX, y: rect.maxY + 6))
}
private func drawHint() {
let text = mode == .region
? "Drag to select a region · Space for window mode · Esc to cancel"
: "Click a window to capture it · Space for region mode · Esc to cancel"
draw(label: text, near: CGPoint(x: mouseLocation.x + 18, y: mouseLocation.y - 34))
}
private func draw(label text: String, near point: CGPoint) {
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 12, weight: .medium),
.foregroundColor: NSColor.white
]
let size = (text as NSString).size(withAttributes: attributes)
var box = CGRect(x: point.x, y: point.y, width: size.width + 12, height: size.height + 8)
box.origin.x = min(max(4, box.origin.x), bounds.width - box.width - 4)
box.origin.y = min(max(4, box.origin.y), bounds.height - box.height - 4)
NSColor(calibratedWhite: 0, alpha: 0.75).setFill()
NSBezierPath(roundedRect: box, xRadius: 5, yRadius: 5).fill()
(text as NSString).draw(at: CGPoint(x: box.minX + 6, y: box.minY + 4), withAttributes: attributes)
}
private func drawMagnifier() {
let magnification: CGFloat = 8
let side: CGFloat = 120
let sourceSide = side / magnification
let cgCursor = cgPoint(from: mouseLocation)
let localPoint = CGPoint(x: cgCursor.x - snapshot.cgFrame.origin.x,
y: cgCursor.y - snapshot.cgFrame.origin.y)
let sourceRect = CGRect(x: localPoint.x - sourceSide / 2,
y: localPoint.y - sourceSide / 2,
width: sourceSide,
height: sourceSide)
guard let zoomed = ScreenGeometry.crop(snapshot.image, toPointRect: sourceRect, scale: snapshot.scale) else {
return
}
var frame = CGRect(x: mouseLocation.x + 20, y: mouseLocation.y + 20, width: side, height: side)
if frame.maxX > bounds.width - 8 { frame.origin.x = mouseLocation.x - side - 20 }
if frame.maxY > bounds.height - 8 { frame.origin.y = mouseLocation.y - side - 20 }
guard let context = NSGraphicsContext.current?.cgContext else { return }
context.saveGState()
context.interpolationQuality = .none
let clip = NSBezierPath(roundedRect: frame, xRadius: 8, yRadius: 8)
clip.addClip()
context.translateBy(x: frame.minX, y: frame.minY + frame.height)
context.scaleBy(x: 1, y: -1)
context.draw(zoomed, in: CGRect(origin: .zero, size: frame.size))
context.restoreGState()
NSColor.white.withAlphaComponent(0.9).setStroke()
clip.lineWidth = 1
clip.stroke()
// Centre marker showing the exact pixel under the cursor.
NSColor.systemGreen.setStroke()
let marker = NSBezierPath(rect: CGRect(x: frame.midX - magnification / 2,
y: frame.midY - magnification / 2,
width: magnification,
height: magnification))
marker.lineWidth = 1
marker.stroke()
}
}