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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user