diff --git a/Sources/Kapture/CaptureController.swift b/Sources/Kapture/CaptureController.swift index 5a889f6..fa8557f 100644 --- a/Sources/Kapture/CaptureController.swift +++ b/Sources/Kapture/CaptureController.swift @@ -14,7 +14,8 @@ final class CaptureController: NSObject { // Selection state (driven by the event tap; AppKit global coordinates) private var marqueeStart: NSPoint? private(set) var marqueeRect: NSRect? - private(set) var hoverFrame: NSRect? + private(set) var hoverWindow: (id: CGWindowID, frame: NSRect)? + var hoverFrame: NSRect? { hoverWindow?.frame } private var trackedWindows: [(id: CGWindowID, frame: NSRect)] = [] private var lastWindowScan = Date.distantPast private var eventTap: CFMachPort? @@ -102,14 +103,15 @@ final class CaptureController: NSObject { } } - /// A window was picked from hover — capture it with rounded corners. - private func finishWindowPick(_ rect: NSRect) { + /// A window was picked from hover — capture it directly (real rounded + /// corners come from the window's own alpha). + private func finishWindowPick(_ windowID: CGWindowID, _ rect: NSRect) { let ids = overlayIDs hideOverlays() isSelecting = false restorePreviousApp() DispatchQueue.main.async { [self] in - capture(rect, excludingWindowIDs: ids, roundedCorners: true) + capture(rect, excludingWindowIDs: ids, windowID: windowID) } } @@ -136,7 +138,7 @@ final class CaptureController: NSObject { overlays.forEach { $0.orderOut(nil) } overlays = [] marqueeRect = nil - hoverFrame = nil + hoverWindow = nil marqueeStart = nil } @@ -246,10 +248,10 @@ final class CaptureController: NSObject { return NSRect(x: rect.origin.x, y: y, width: rect.width, height: rect.height) } - private func topmostWindowContaining(_ appKitPoint: NSPoint) -> NSRect? { - for (_, frame) in trackedWindows { - if frame.contains(appKitPoint) { - return frame + private func topmostWindowContaining(_ appKitPoint: NSPoint) -> (id: CGWindowID, frame: NSRect)? { + for entry in trackedWindows { + if entry.frame.contains(appKitPoint) { + return entry } } return nil @@ -282,9 +284,9 @@ final class CaptureController: NSObject { } let appKitPoint = cgPointToAppKit(NSPoint(x: event.location.x, y: event.location.y)) if type == .mouseMoved { - let frame = topmostWindowContaining(appKitPoint) - if frame != hoverFrame { - hoverFrame = frame + let window = topmostWindowContaining(appKitPoint) + if window?.frame != hoverFrame { + hoverWindow = window renderOverlays() } return event @@ -298,7 +300,7 @@ final class CaptureController: NSObject { height: abs(appKitPoint.y - start.y) ) marqueeRect = rect - hoverFrame = nil + hoverWindow = nil renderOverlays() return nil case .leftMouseDown: @@ -313,9 +315,9 @@ final class CaptureController: NSObject { if let rect = marqueeRect, dragDistance >= 4, rect.width >= 2, rect.height >= 2 { CaptureController.log("tap: marquee complete \(rect)") finishSelection(rect) - } else if let hover = hoverFrame { - CaptureController.log("tap: window pick \(hover)") - finishWindowPick(hover) + } else if let hover = hoverWindow { + CaptureController.log("tap: window pick id=\(hover.id) \(hover.frame)") + finishWindowPick(hover.id, hover.frame) } else { CaptureController.log("tap: click with no hover/marquee — cancel") cancelSelection() @@ -328,8 +330,8 @@ final class CaptureController: NSObject { // MARK: - Capture - private func capture(_ rect: NSRect, excludingWindowIDs ids: [Int], roundedCorners: Bool = false) { - ScreenCapture.shared.capture(rect, excludingWindowIDs: ids, roundedCorners: roundedCorners) { [weak self] image in + private func capture(_ rect: NSRect, excludingWindowIDs ids: [Int], roundedCorners: Bool = false, windowID: CGWindowID? = nil) { + ScreenCapture.shared.capture(rect, excludingWindowIDs: ids, roundedCorners: roundedCorners, windowID: windowID) { [weak self] image in DispatchQueue.main.async { self?.isSelecting = false self?.restorePreviousApp() @@ -414,7 +416,7 @@ final class ScreenCapture { return content.displays.first(where: { $0.displayID == cgID }) ?? content.displays.first } - func capture(_ rect: NSRect, excludingWindowIDs excludedIDs: [Int], roundedCorners: Bool = false, completion: @escaping (CGImage?) -> Void) { + func capture(_ rect: NSRect, excludingWindowIDs excludedIDs: [Int], roundedCorners: Bool = false, windowID: CGWindowID? = nil, completion: @escaping (CGImage?) -> Void) { if #available(macOS 14.0, *) { Task { guard let content = try? await SCShareableContent.current, @@ -449,6 +451,25 @@ final class ScreenCapture { height: rect.height ) + // Window pick: capture the window DIRECTLY — the image comes + // with the app's real rounded-corner alpha (no mask guessing). + if let windowID, + let scWindow = content.windows.first(where: { $0.windowID == windowID }) { + var windowConfig = SCStreamConfiguration() + windowConfig.width = Int(scWindow.frame.width * backingScale) + windowConfig.height = Int(scWindow.frame.height * backingScale) + windowConfig.showsCursor = false + if let windowImage = try? await SCScreenshotManager.captureImage( + contentFilter: SCContentFilter(desktopIndependentWindow: scWindow), + configuration: windowConfig + ) { + CaptureController.log("capture: WINDOW id=\(windowID) image=\(windowImage.width)x\(windowImage.height) (native alpha corners)") + completion(windowImage) + return + } + CaptureController.log("capture: window filter failed for id=\(windowID) — falling back to crop+mask") + } + guard let image = try? await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) else { completion(nil) return diff --git a/build-app.sh b/build-app.sh index 3a38527..a108ddd 100755 --- a/build-app.sh +++ b/build-app.sh @@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' CFBundleIconFile Icon CFBundleShortVersionString - 1.7.4 + 1.8.0 CFBundleVersion - 74 + 80 LSUIElement NSHighResolutionCapable