Add window detection capture with rounded corners

Hovering during selection now highlights the window under the cursor;
clicking captures it with a rounded-corner alpha mask matching macOS
window chrome. Dragging still switches to manual marquee selection.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
This commit is contained in:
2026-09-09 10:29:24 +08:00
parent de642ad692
commit 969cda6f14
3 changed files with 176 additions and 15 deletions
+130 -6
View File
@@ -1,8 +1,8 @@
import AppKit
final class OverlayWindow: NSPanel {
init(screen: NSScreen, onComplete: @escaping (NSRect) -> Void, onCancel: @escaping () -> Void) {
let content = OverlayView(onComplete: onComplete, onCancel: onCancel)
init(screen: NSScreen, onComplete: @escaping (NSRect) -> Void, onCancel: @escaping () -> Void, onWindowPick: @escaping (NSRect) -> Void) {
let content = OverlayView(onComplete: onComplete, onCancel: onCancel, onWindowPick: onWindowPick)
super.init(
contentRect: screen.frame,
styleMask: [.borderless, .nonactivatingPanel],
@@ -25,13 +25,21 @@ final class OverlayWindow: NSPanel {
final class OverlayView: NSView {
private let onComplete: (NSRect) -> Void
private let onCancel: () -> Void
private let onWindowPick: (NSRect) -> Void
private var startPoint: NSPoint?
private(set) var selection: NSRect?
private var isDragging = false
init(onComplete: @escaping (NSRect) -> Void, onCancel: @escaping () -> Void) {
// Window detection
private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = []
private var hoveredWindowFrame: NSRect?
private var excludedIDs: [Int] = []
init(onComplete: @escaping (NSRect) -> Void, onCancel: @escaping () -> Void, onWindowPick: @escaping (NSRect) -> Void) {
self.onComplete = onComplete
self.onCancel = onCancel
self.onWindowPick = onWindowPick
super.init(frame: .zero)
}
@@ -47,6 +55,85 @@ final class OverlayView: NSView {
addCursorRect(bounds, cursor: .crosshair)
}
// MARK: - Window detection
/// Rebuild the list of capturable windows intersecting this screen.
/// Called lazily on mouse moves; cheap enough for interactive hover.
private func refreshTrackedWindows() {
guard let window = window else { return }
let screenFrame = window.convertToScreen(NSRect(origin: .zero, size: bounds.size))
// Exclude this overlay's own window (the other overlays don't intersect
// this screen, so the on-screen filter handles them).
let ownID = window.windowNumber
guard let list = CGWindowListCopyWindowInfo(
[ .optionOnScreenOnly, .excludeDesktopElements ], kCGNullWindowID
) as? [[String: Any]] else { trackedWindows = []; return }
var found: [(CGWindowID, NSRect)] = []
for info in list {
guard let cid = info[kCGWindowNumber as String] as? CGWindowID,
let bounds = info[kCGWindowBounds as String] as? [String: CGFloat],
let layer = info[kCGWindowLayer as String] as? Int,
layer == 0
else { continue }
// Skip our own overlays
if Int(cid) == ownID { continue }
let rect = NSRect(
x: bounds["X"] ?? 0,
y: bounds["Y"] ?? 0,
width: bounds["Width"] ?? 0,
height: bounds["Height"] ?? 0
)
guard rect.width >= 80, rect.height >= 60 else { continue }
// Skip tiny/utility windows
guard rect.width < screenFrame.width || rect.height < screenFrame.height else { continue }
// Only windows intersecting this screen
guard rect.intersects(screenFrame) else { continue }
found.append((cid, rect))
}
trackedWindows = found
}
private func windowFrame(at point: NSPoint) -> NSRect? {
guard let window = window else { return nil }
let screenPoint = window.convertToScreen(NSRect(origin: point, size: .zero))
// CGWindowList bounds use a TOP-LEFT origin; convert the cursor point.
guard let mainScreen = NSScreen.screens.first else { return nil }
let topLeftPoint = NSPoint(
x: screenPoint.origin.x,
y: mainScreen.frame.height - screenPoint.origin.y
)
// Pick the frontmost (list order) window containing the point
for (_, frame) in trackedWindows {
if frame.contains(topLeftPoint) {
return frame
}
}
return nil
}
/// Convert a CGWindowList (top-left origin) frame into AppKit screen
/// coordinates (bottom-left origin) for capture.
private static func cgToAppKit(rect: NSRect) -> NSRect {
guard let mainScreen = NSScreen.screens.first else { return rect }
let y = mainScreen.frame.height - rect.origin.y - rect.height
return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height)
}
// MARK: - Events
override func mouseMoved(with event: NSEvent) {
guard !isDragging else { return }
refreshTrackedWindows()
let p = convert(event.locationInWindow, from: nil)
let frame = windowFrame(at: p)
if frame != hoveredWindowFrame {
hoveredWindowFrame = frame
needsDisplay = true
}
}
override func mouseDown(with event: NSEvent) {
let p = convert(event.locationInWindow, from: nil)
startPoint = p
@@ -56,6 +143,8 @@ final class OverlayView: NSView {
override func mouseDragged(with event: NSEvent) {
guard let start = startPoint else { return }
isDragging = true
hoveredWindowFrame = nil
let p = convert(event.locationInWindow, from: nil)
selection = NSRect(
x: min(start.x, p.x),
@@ -67,9 +156,20 @@ final class OverlayView: NSView {
}
override func mouseUp(with event: NSEvent) {
guard let sel = selection else { return }
if sel.width >= 2, sel.height >= 2 {
onComplete(window!.convertToScreen(sel))
if isDragging {
guard let sel = selection else { return }
if sel.width >= 2, sel.height >= 2 {
onComplete(window!.convertToScreen(sel))
} else {
onCancel()
}
return
}
// No drag: it was a click. If a window is hovered, capture it with
// rounded corners; otherwise treat as an empty marquee (cancel).
if let frame = hoveredWindowFrame {
onWindowPick(Self.cgToAppKit(rect: frame))
} else {
onCancel()
}
@@ -87,6 +187,30 @@ final class OverlayView: NSView {
NSColor.black.withAlphaComponent(0.35).setFill()
NSBezierPath(rect: bounds).fill()
// Highlight the hovered window (rounded, like the real window corners)
if let frame = hoveredWindowFrame {
// Convert CGWindowList (top-left) frame to view (bottom-left) space
guard let window = window, let mainScreen = NSScreen.screens.first else { return }
let screenFrame = window.convertToScreen(NSRect(origin: .zero, size: bounds.size))
let viewFrame = NSRect(
x: frame.origin.x - screenFrame.origin.x,
y: mainScreen.frame.height - frame.origin.y - frame.height - screenFrame.origin.y,
width: frame.width,
height: frame.height
)
NSGraphicsContext.current?.saveGraphicsState()
NSGraphicsContext.current?.compositingOperation = .clear
NSColor.black.setFill()
NSBezierPath(roundedRect: viewFrame, xRadius: 10, yRadius: 10).fill()
NSGraphicsContext.current?.restoreGraphicsState()
let border = NSBezierPath(roundedRect: viewFrame, xRadius: 10, yRadius: 10)
border.lineWidth = 2
NSColor.controlAccentColor.setStroke()
border.stroke()
return
}
guard let sel = selection, sel.width > 0, sel.height > 0 else { return }
NSGraphicsContext.current?.saveGraphicsState()