Files
kapture/Sources/Kapture/OverlayView.swift
T
TZGyn e4e20c51d3 Add frozen-screen capture on Ctrl+Shift+Cmd+S
A second hotkey snapshots every display the moment it is pressed and
shows that still image as the overlay, so the pointer and hover states
(tooltips, open menus) are captured. Selection is marquee-only — no
window detection — and the result is cropped from the snapshot rather
than recaptured, keeping the frame exactly as it was.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-25 17:40:12 +08:00

196 lines
7.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
/// Renders the capture overlay. All interaction state lives in
/// CaptureController (driven by the CGEventTap); this view only draws.
final class OverlayWindow: NSPanel {
init(screen: NSScreen, controller: CaptureController) {
let content = OverlayView()
super.init(
// Non-activating: the overlay must NOT steal focus, so the app
// being captured keeps its active appearance (undimmed chrome).
// Input comes from the CGEventTap, not panel events.
contentRect: screen.frame,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
contentView = content
content.controller = controller
level = .screenSaver
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
backgroundColor = .clear
isOpaque = false
hasShadow = false
isReleasedWhenClosed = false
acceptsMouseMovedEvents = true
}
override var canBecomeKey: Bool { true }
}
final class OverlayView: NSView {
weak var controller: CaptureController?
/// In frozen mode: the still image to display for this screen (already
/// full resolution). Nil = live mode (dim the real screen).
var frozenImage: CGImage?
override var acceptsFirstResponder: Bool { true }
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
override func resetCursorRects() {
addCursorRect(bounds, cursor: .crosshair)
}
/// The screen frame this overlay covers, in AppKit global space.
private var screenFrame: NSRect {
guard let window else { return .zero }
return window.convertToScreen(NSRect(origin: .zero, size: bounds.size))
}
/// Convert an AppKit-global rect into this view's coordinates.
private func viewRect(_ global: NSRect) -> NSRect {
let sf = screenFrame
return NSRect(
x: global.origin.x - sf.origin.x,
y: global.origin.y - sf.origin.y,
width: global.width,
height: global.height
)
}
/// Draw the frozen snapshot filling the screen, then replace its centre
/// hole with the live... no: the image IS the content, drawn opaque.
private func drawFrozen(_ image: CGImage) {
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
ctx.saveGState()
ctx.interpolationQuality = .high
// Image is in pixels, view in points — draw into the view bounds.
ctx.draw(image, in: bounds)
ctx.restoreGState()
}
private func drawSizeLabel(for sel: NSRect, in vf: NSRect) {
let label = "\(Int(sel.width)) × \(Int(sel.height))"
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium),
.foregroundColor: NSColor.white,
]
let labelSize = label.size(withAttributes: attrs)
let labelRect = NSRect(
x: vf.maxX - labelSize.width - 8,
y: vf.minY - labelSize.height - 10,
width: labelSize.width + 8,
height: labelSize.height + 6
)
NSColor.black.withAlphaComponent(0.6).setFill()
NSBezierPath(roundedRect: labelRect, xRadius: 4, yRadius: 4).fill()
label.draw(at: NSPoint(x: labelRect.minX + 4, y: labelRect.minY + 3), withAttributes: attrs)
}
override func draw(_ dirtyRect: NSRect) {
if let frozenImage {
drawFrozen(frozenImage)
} else {
NSColor.black.withAlphaComponent(0.35).setFill()
NSBezierPath(rect: bounds).fill()
}
// Esc hint — the only way out once clicks are consumed by the tap.
let hint = "Drag to select · hover + click a window to capture it · Esc to cancel"
let hintAttrs: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedSystemFont(ofSize: 12, weight: .medium),
.foregroundColor: NSColor.white.withAlphaComponent(0.85),
]
let hintSize = hint.size(withAttributes: hintAttrs)
let hintRect = NSRect(
x: (bounds.width - hintSize.width) / 2 - 10,
y: bounds.height - 46,
width: hintSize.width + 20,
height: hintSize.height + 12
)
NSColor.black.withAlphaComponent(0.65).setFill()
NSBezierPath(roundedRect: hintRect, xRadius: 8, yRadius: 8).fill()
hint.draw(at: NSPoint(x: hintRect.minX + 10, y: hintRect.minY + 6), withAttributes: hintAttrs)
guard let controller else { return }
// Frozen mode: no window highlight; the marquee is cut straight out
// of the still image so it shows the frozen pixels.
if frozenImage != nil {
if let sel = controller.marqueeRect, sel.width > 0, sel.height > 0 {
let vf = viewRect(sel)
// Dim everything except the selection (even-odd fill).
let maskPath = CGMutablePath()
maskPath.addRect(bounds)
maskPath.addRect(vf)
NSGraphicsContext.current?.saveGraphicsState()
let ctx = NSGraphicsContext.current?.cgContext
ctx?.addPath(maskPath)
ctx?.fillPath(using: .evenOdd)
NSColor.black.withAlphaComponent(0.35).setFill()
ctx?.addPath(maskPath)
ctx?.fillPath(using: .evenOdd)
NSGraphicsContext.current?.restoreGraphicsState()
let border = NSBezierPath(rect: vf)
border.lineWidth = 1.5
NSColor.controlAccentColor.setStroke()
border.stroke()
drawSizeLabel(for: sel, in: vf)
}
return
}
// Highlight the hovered window (rounded, like real window corners)
if let frame = controller.hoverFrame {
let vf = viewRect(frame)
NSGraphicsContext.current?.saveGraphicsState()
NSGraphicsContext.current?.compositingOperation = .clear
NSColor.black.setFill()
NSBezierPath(roundedRect: vf, xRadius: 10, yRadius: 10).fill()
NSGraphicsContext.current?.restoreGraphicsState()
let border = NSBezierPath(roundedRect: vf, xRadius: 10, yRadius: 10)
border.lineWidth = 2
NSColor.controlAccentColor.setStroke()
border.stroke()
return
}
// Marquee selection
guard let sel = controller.marqueeRect, sel.width > 0, sel.height > 0 else { return }
let vf = viewRect(sel)
NSGraphicsContext.current?.saveGraphicsState()
NSGraphicsContext.current?.compositingOperation = .clear
NSColor.black.setFill()
NSBezierPath(rect: vf).fill()
NSGraphicsContext.current?.restoreGraphicsState()
let border = NSBezierPath(rect: vf)
border.lineWidth = 1.5
NSColor.controlAccentColor.setStroke()
border.stroke()
let label = "\(Int(sel.width)) × \(Int(sel.height))"
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium),
.foregroundColor: NSColor.white,
]
let labelSize = label.size(withAttributes: attrs)
let labelRect = NSRect(
x: vf.maxX - labelSize.width - 8,
y: vf.minY - labelSize.height - 10,
width: labelSize.width + 8,
height: labelSize.height + 6
)
NSColor.black.withAlphaComponent(0.6).setFill()
NSBezierPath(roundedRect: labelRect, xRadius: 4, yRadius: 4).fill()
label.draw(
at: NSPoint(x: labelRect.minX + 4, y: labelRect.minY + 3),
withAttributes: attrs
)
}
}