Add draw toolbar with thickness, undo/redo, and color picker
When drawing is enabled a dedicated top bar shows a thickness slider,
color picker, and undo/redo buttons (⌘Z / ⌘⇧Z). Strokes render and export
with the chosen width and color.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4-flash-0731
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Icon</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.3</string>
|
||||
<string>1.4.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>33</string>
|
||||
<string>40</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
|
||||
Reference in New Issue
Block a user