diff --git a/Sources/Kapture/OverlayView.swift b/Sources/Kapture/OverlayView.swift index 903ea1d..47dd502 100644 --- a/Sources/Kapture/OverlayView.swift +++ b/Sources/Kapture/OverlayView.swift @@ -97,24 +97,19 @@ final class OverlayView: NSView { 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 + // convertToScreen gives AppKit-global coordinates; convert tracked + // CG frames to the same space and pick the frontmost containing it. + let screenPoint = window.convertToScreen(NSRect(origin: point, size: .zero)).origin for (_, frame) in trackedWindows { - if frame.contains(topLeftPoint) { + if Self.cgToAppKit(rect: frame).contains(screenPoint) { return frame } } return nil } - /// Convert a CGWindowList (top-left origin) frame into AppKit screen - /// coordinates (bottom-left origin) for capture. + /// Convert a CGWindowList (top-left origin, CG global space) 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 @@ -189,12 +184,13 @@ final class OverlayView: NSView { // 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 } + // Convert the CG frame to AppKit-global, then to this view's space. + guard let window = window else { return } let screenFrame = window.convertToScreen(NSRect(origin: .zero, size: bounds.size)) + let appKitFrame = Self.cgToAppKit(rect: frame) let viewFrame = NSRect( - x: frame.origin.x - screenFrame.origin.x, - y: mainScreen.frame.height - frame.origin.y - frame.height - screenFrame.origin.y, + x: appKitFrame.origin.x - screenFrame.origin.x, + y: appKitFrame.origin.y - screenFrame.origin.y, width: frame.width, height: frame.height ) diff --git a/Sources/Kapture/ResultWindow.swift b/Sources/Kapture/ResultWindow.swift index 5639d65..57ce05a 100644 --- a/Sources/Kapture/ResultWindow.swift +++ b/Sources/Kapture/ResultWindow.swift @@ -96,6 +96,8 @@ final class CaptureModel: ObservableObject { @Published var redoStack: [Stroke] = [] /// Whether the pen is active (drag draws instead of selecting) @Published var isDrawing = false + /// Whether drawing redacts (black bar) instead of pen strokes + @Published var isRedacting = false /// Current drawing color @Published var drawColor: NSColor = .systemRed /// Stroke thickness in points (image space) @@ -105,6 +107,7 @@ final class CaptureModel: ObservableObject { let points: [CGPoint] let width: CGFloat let color: NSColor + var isRedaction: Bool = false } enum CaptureMode { @@ -159,14 +162,29 @@ final class CaptureModel: ObservableObject { ctx.setLineJoin(.round) for stroke in strokes { guard stroke.points.count > 1 else { continue } - stroke.color.setStroke() - ctx.setLineWidth(stroke.width) - ctx.beginPath() - ctx.move(to: stroke.points[0]) - for point in stroke.points.dropFirst() { - ctx.addLine(to: point) + if stroke.isRedaction { + // Redaction bar: filled black rect covering the stroke bounds, + // padded so it fully covers text. + let xs = stroke.points.map { $0.x } + let ys = stroke.points.map { $0.y } + let bar = NSRect( + x: xs.min()! - 2, + y: ys.min()! - 2, + width: (xs.max()! - xs.min()!) + 4, + height: max(stroke.width, ys.max()! - ys.min()!) + 4 + ) + NSColor.black.setFill() + NSBezierPath(rect: bar).fill() + } else { + stroke.color.setStroke() + ctx.setLineWidth(stroke.width) + ctx.beginPath() + ctx.move(to: stroke.points[0]) + for point in stroke.points.dropFirst() { + ctx.addLine(to: point) + } + ctx.strokePath() } - ctx.strokePath() } ctx.restoreGState() NSGraphicsContext.restoreGraphicsState() @@ -175,7 +193,13 @@ final class CaptureModel: ObservableObject { func addStroke(_ points: [CGPoint]) { guard points.count > 1 else { return } - strokes.append(Stroke(points: points, width: strokeWidth, color: drawColor)) + if isRedacting { + // Redaction: a filled black bar from start to end, as thick as the + // current stroke width (drawn as a rect for clean edges). + strokes.append(Stroke(points: points, width: max(strokeWidth, 12), color: .black, isRedaction: true)) + } else { + strokes.append(Stroke(points: points, width: strokeWidth, color: drawColor)) + } redoStack = [] } @@ -351,31 +375,37 @@ struct ResultView: View { // Draw toolbar (only when drawing) if model.isDrawing { HStack(spacing: 14) { - Label("Draw", systemImage: "pencil.tip") - .font(.caption) - .foregroundStyle(.secondary) - - Divider().frame(height: 18) - - HStack(spacing: 6) { - Image(systemName: "circle.fill").font(.system(size: 4)) - Image(systemName: "circle.fill").font(.system(size: 7)) - Image(systemName: "circle.fill").font(.system(size: 10)) + Picker("", selection: $model.isRedacting) { + Label("Pen", systemImage: "pencil.tip").tag(false) + Label("Redact", systemImage: "rectangle.fill").tag(true) } - .foregroundStyle(.secondary) - Slider(value: $model.strokeWidth, in: 1...12, step: 0.5) - .frame(width: 120) - .help("Stroke thickness") + .pickerStyle(.segmented) + .fixedSize() + .help("Pen draws strokes; Redact covers text with a black bar") Divider().frame(height: 18) - ColorPicker("", selection: Binding( - get: { Color(nsColor: model.drawColor) }, - set: { model.drawColor = NSColor($0) } - ), supportsOpacity: false) - .labelsHidden() - .frame(width: 28) - .help("Drawing color") + if !model.isRedacting { + HStack(spacing: 6) { + Image(systemName: "circle.fill").font(.system(size: 4)) + Image(systemName: "circle.fill").font(.system(size: 7)) + Image(systemName: "circle.fill").font(.system(size: 10)) + } + .foregroundStyle(.secondary) + Slider(value: $model.strokeWidth, in: 1...12, step: 0.5) + .frame(width: 120) + .help("Stroke thickness") + + Divider().frame(height: 18) + + ColorPicker("", selection: Binding( + get: { Color(nsColor: model.drawColor) }, + set: { model.drawColor = NSColor($0) } + ), supportsOpacity: false) + .labelsHidden() + .frame(width: 28) + .help("Drawing color") + } Spacer() @@ -515,12 +545,24 @@ struct ScreenshotPane: View { Canvas { context, _ in for stroke in model.strokes { guard stroke.points.count > 1 else { continue } - var path = Path() - path.move(to: CGPoint(x: origin.x + stroke.points[0].x * scale, y: origin.y + stroke.points[0].y * scale)) - for point in stroke.points.dropFirst() { - path.addLine(to: CGPoint(x: origin.x + point.x * scale, y: origin.y + point.y * scale)) + if stroke.isRedaction { + let xs = stroke.points.map { $0.x } + let ys = stroke.points.map { $0.y } + let bar = CGRect( + x: origin.x + (xs.min()! - 2) * scale, + y: origin.y + (ys.min()! - 2) * scale, + width: ((xs.max()! - xs.min()!) + 4) * scale, + height: (max(stroke.width, ys.max()! - ys.min()!) + 4) * scale + ) + context.fill(Path(bar), with: .color(.black)) + } else { + var path = Path() + path.move(to: CGPoint(x: origin.x + stroke.points[0].x * scale, y: origin.y + stroke.points[0].y * scale)) + for point in stroke.points.dropFirst() { + path.addLine(to: CGPoint(x: origin.x + point.x * scale, y: origin.y + point.y * scale)) + } + context.stroke(path, with: .color(Color(nsColor: stroke.color)), lineWidth: stroke.width) } - context.stroke(path, with: .color(Color(nsColor: stroke.color)), lineWidth: stroke.width) } } .allowsHitTesting(false) diff --git a/build-app.sh b/build-app.sh index 914c75e..fcb3585 100755 --- a/build-app.sh +++ b/build-app.sh @@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST' CFBundleIconFile Icon CFBundleShortVersionString - 1.5.0 + 1.5.1 CFBundleVersion - 50 + 51 LSUIElement NSHighResolutionCapable