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: String if controller?.isRecordingMode == true { hint = "Drag to select · hover + click a window to record · Esc to cancel" } else if frozenImage != nil { hint = "Frozen screen · drag to select · Esc to cancel" } else { 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: same look as live mode — a dark veil over everything, // with the selection showing the frozen pixels undimmed. if let frozenImage { NSColor.black.withAlphaComponent(0.35).setFill() NSBezierPath(rect: bounds).fill() if let sel = controller.marqueeRect, sel.width > 0, sel.height > 0 { let vf = viewRect(sel) NSGraphicsContext.current?.saveGraphicsState() NSBezierPath(rect: vf).addClip() drawFrozen(frozenImage) 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 ) } }