diff --git a/Sources/Kapture/ResultWindow.swift b/Sources/Kapture/ResultWindow.swift
index bf2ab01..9524bda 100644
--- a/Sources/Kapture/ResultWindow.swift
+++ b/Sources/Kapture/ResultWindow.swift
@@ -92,10 +92,14 @@ final class CaptureModel: ObservableObject {
@Published var selection: CGRect?
/// Freehand drawing strokes in IMAGE point space
@Published var strokes: [[CGPoint]] = []
+ /// Redo stack of removed strokes
+ @Published var redoStack: [[CGPoint]] = []
/// Whether the pen is active (drag draws instead of selecting)
@Published var isDrawing = false
/// Current drawing color
@Published var drawColor: NSColor = .systemRed
+ /// Stroke thickness in points (image space)
+ @Published var strokeWidth: CGFloat = 3.0
enum CaptureMode {
case screenshot
@@ -107,6 +111,23 @@ final class CaptureModel: ObservableObject {
}
var hasSelection: Bool { selection != nil && selection!.width >= 2 && selection!.height >= 2 }
+ var canUndo: Bool { !strokes.isEmpty }
+ var canRedo: Bool { !redoStack.isEmpty }
+
+ func undoStroke() {
+ guard let last = strokes.popLast() else { return }
+ redoStack.append(last)
+ }
+
+ func redoStroke() {
+ guard let last = redoStack.popLast() else { return }
+ strokes.append(last)
+ }
+
+ func clearAnnotations() {
+ strokes = []
+ redoStack = []
+ }
/// Image with drawn strokes flattened in, for copy/save/export.
var annotatedImage: CGImage {
@@ -123,7 +144,7 @@ final class CaptureModel: ObservableObject {
// CGImage draws with a flipped Y in NSGraphicsContext
ctx.draw(image, in: NSRect(origin: .zero, size: size))
drawColor.setStroke()
- ctx.setLineWidth(3)
+ ctx.setLineWidth(strokeWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
for stroke in strokes {
@@ -142,10 +163,7 @@ final class CaptureModel: ObservableObject {
func addStroke(_ points: [CGPoint]) {
guard points.count > 1 else { return }
strokes.append(points)
- }
-
- func clearAnnotations() {
- strokes = []
+ redoStack = []
}
var displayText: String {
@@ -297,15 +315,37 @@ struct ResultView: View {
.help("Drawing color")
}
- if !model.strokes.isEmpty {
- Button {
- model.clearAnnotations()
- } label: {
- Label("Clear", systemImage: "eraser")
+ if model.isDrawing || !model.strokes.isEmpty {
+ HStack(spacing: 6) {
+ Button {
+ model.undoStroke()
+ } label: {
+ Image(systemName: "arrow.uturn.backward")
+ }
+ .buttonStyle(.bordered)
+ .disabled(!model.canUndo)
+ .keyboardShortcut("z", modifiers: .command)
+ .help("Undo last stroke (⌘Z)")
+
+ Button {
+ model.redoStroke()
+ } label: {
+ Image(systemName: "arrow.uturn.forward")
+ }
+ .buttonStyle(.bordered)
+ .disabled(!model.canRedo)
+ .keyboardShortcut("z", modifiers: [.command, .shift])
+ .help("Redo stroke (⌘⇧Z)")
+
+ Button {
+ model.clearAnnotations()
+ } label: {
+ Image(systemName: "eraser")
+ }
+ .buttonStyle(.bordered)
+ .disabled(model.strokes.isEmpty)
+ .help("Remove all drawn annotations")
}
- .buttonStyle(.bordered)
- .foregroundStyle(.secondary)
- .help("Remove all drawn annotations")
}
Button {
@@ -338,6 +378,64 @@ struct ResultView: View {
Divider()
+ // 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))
+ }
+ .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()
+
+ Button {
+ model.undoStroke()
+ } label: {
+ Label("Undo", systemImage: "arrow.uturn.backward")
+ }
+ .buttonStyle(.bordered)
+ .controlSize(.small)
+ .disabled(!model.canUndo)
+ .help("Undo last stroke (⌘Z)")
+
+ Button {
+ model.redoStroke()
+ } label: {
+ Label("Redo", systemImage: "arrow.uturn.forward")
+ }
+ .buttonStyle(.bordered)
+ .controlSize(.small)
+ .disabled(!model.canRedo)
+ .help("Redo stroke (⌘⇧Z)")
+ }
+ .padding(.horizontal, 14)
+ .padding(.vertical, 8)
+ .background(.bar)
+
+ Divider()
+ }
+
// Content
if model.mode == .screenshot {
ScreenshotPane(model: model)
@@ -441,7 +539,7 @@ struct ScreenshotPane: View {
path.addLine(to: CGPoint(x: origin.x + point.x * scale, y: origin.y + point.y * scale))
}
}
- context.stroke(path, with: .color(Color(nsColor: model.drawColor)), lineWidth: 2.5)
+ context.stroke(path, with: .color(Color(nsColor: model.drawColor)), lineWidth: model.strokeWidth)
}
.allowsHitTesting(false)
@@ -455,7 +553,7 @@ struct ScreenshotPane: View {
path.addLine(to: CGPoint(x: origin.x + point.x * scale, y: origin.y + point.y * scale))
}
}
- context.stroke(path, with: .color(Color(nsColor: model.drawColor)), lineWidth: 2.5)
+ context.stroke(path, with: .color(Color(nsColor: model.drawColor)), lineWidth: model.strokeWidth)
}
.allowsHitTesting(false)
}
diff --git a/build-app.sh b/build-app.sh
index dc7aa22..d19934a 100755
--- a/build-app.sh
+++ b/build-app.sh
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
CFBundleIconFile
Icon
CFBundleShortVersionString
- 1.3.3
+ 1.4.0
CFBundleVersion
- 33
+ 40
LSUIElement
NSHighResolutionCapable