From 18a9f53062c1e849efe42247a55ab391058790cd Mon Sep 17 00:00:00 2001 From: mamamiyear Date: Wed, 8 Jul 2026 16:20:51 +0800 Subject: [PATCH] feat: add annotation tool settings menus --- frontend/src/App.vue | 1 + frontend/src/views/CaptureOverlay.vue | 301 ++++++++++++++----- frontend/wailsjs/go/models.ts | 2 + internal/application/annotation.go | 17 +- internal/application/annotation_test.go | 31 ++ native_overlay_darwin.go | 384 +++++++++++++++++++----- 6 files changed, 587 insertions(+), 149 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1a474ba..5ec7dd8 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -82,6 +82,7 @@ interface OverlayAnnotation { color: string points: Array<{ x: number; y: number }> text?: string + strokeWidth?: number fontSize?: number } diff --git a/frontend/src/views/CaptureOverlay.vue b/frontend/src/views/CaptureOverlay.vue index fe4c315..0bea443 100644 --- a/frontend/src/views/CaptureOverlay.vue +++ b/frontend/src/views/CaptureOverlay.vue @@ -33,6 +33,7 @@ interface Annotation { color: string points: Point[] text?: string + strokeWidth?: number fontSize?: number } @@ -65,11 +66,13 @@ const annotations = ref([]) const draftAnnotation = ref(null) const activeTool = ref('pen') const activeColor = ref('#ef4444') -const paletteOpen = ref(false) +const activeStrokeWidth = ref(3) +const activeFontSize = ref(20) const textDraft = ref<{ point: Point value: string color: string + fontSize: number index: number | null } | null>(null) const textInputRef = ref(null) @@ -90,19 +93,18 @@ const startRect = ref(null) const textDragStartPoint = ref(null) const textDragOriginalPoint = ref(null) -const TEXT_FONT_SIZE = 20 +const DEFAULT_TEXT_FONT_SIZE = 20 +const STROKE_WIDTHS = [2, 4, 6] +const FONT_SIZES = [16, 20, 28, 36] const colors = [ '#ef4444', - '#f97316', '#facc15', '#22c55e', - '#06b6d4', '#3b82f6', - '#8b5cf6', - '#ec4899', - '#ffffff', '#111827', + '#9ca3af', + '#ffffff', ] const maskPath = computed(() => { @@ -135,10 +137,32 @@ const sizeLabel = computed(() => { return `${Math.round(rect.value.w)} × ${Math.round(rect.value.h)}` }) -const MARK_TOOLBAR_W = 222 +const MARK_TOOLBAR_W = 178 const ACTION_TOOLBAR_W = 216 const TOOLBAR_GROUP_GAP = 8 +const toolOrder: Tool[] = ['pen', 'rect', 'ellipse', 'text'] + +const toolSettingsPos = computed(() => { + if (!leftToolbarPos.value || !rect.value || !activeTool.value) return null + const menuW = activeTool.value === 'text' ? 462 : 398 + const menuH = 58 + const buttonIndex = toolOrder.indexOf(activeTool.value) + const buttonCenter = 4 + buttonIndex * 34 + 14 + let x = leftToolbarPos.value.x + let y = leftToolbarPos.value.y + 44 + if (y + menuH > props.height) { + y = leftToolbarPos.value.y - menuH - 8 + } + x = clamp(x, 8, Math.max(8, props.width - menuW - 8)) + return { + x, + y, + w: menuW, + arrowX: clamp(leftToolbarPos.value.x + buttonCenter - x, 18, menuW - 18), + } +}) + const rightToolbarPos = computed(() => { if (!rect.value) return null return placeToolbar(rect.value, ACTION_TOOLBAR_W, 40, 'right') @@ -226,12 +250,12 @@ function insideRect(p: Point, r: Rect | null) { let measureCanvas: HTMLCanvasElement | null = null -function textFont(fontSize = TEXT_FONT_SIZE) { +function textFont(fontSize = DEFAULT_TEXT_FONT_SIZE) { return `600 ${fontSize}px -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif` } function textSize(annotation: Pick) { - const fontSize = annotation.fontSize || TEXT_FONT_SIZE + const fontSize = annotation.fontSize || DEFAULT_TEXT_FONT_SIZE const lines = (annotation.text || ' ').split('\n') if (typeof document === 'undefined') { return { @@ -254,6 +278,18 @@ function textSize(annotation: Pick) { } } +function textEditorSize(draft: { value: string; fontSize: number }) { + const size = textSize({ + text: draft.value || ' ', + fontSize: draft.fontSize, + }) + return { + width: Math.max(180, Math.min(420, size.w + 28)), + height: Math.max(30, Math.ceil(draft.fontSize * 1.6)), + lineHeight: Math.ceil(draft.fontSize * 1.25), + } +} + function textRenderBox(annotation: Annotation) { const point = annotation.points[0] || { x: 0, y: 0 } const size = textSize(annotation) @@ -319,7 +355,6 @@ function hitHandle(p: Point): ResizeHandle | null { function onMouseDown(e: MouseEvent) { if (e.button !== 0) return commitTextDraft() - paletteOpen.value = false textDragStartPoint.value = null textDragOriginalPoint.value = null const p = pointFromEvent(e) @@ -348,12 +383,14 @@ function onSelectionMouseDown(e: MouseEvent) { if (e.button !== 0 || !rect.value) return e.stopPropagation() commitTextDraft() - paletteOpen.value = false const p = pointFromEvent(e) const hitTextIndex = textAnnotationIndexAtPoint(p) if (hitTextIndex !== null) { selectedTextIndex.value = hitTextIndex const annotation = annotations.value[hitTextIndex] + activeTool.value = 'text' + activeColor.value = annotation.color + activeFontSize.value = annotation.fontSize || DEFAULT_TEXT_FONT_SIZE if (e.detail >= 2) { beginTextAnnotation(annotation.points[0], hitTextIndex) return @@ -386,6 +423,7 @@ function onSelectionMouseDown(e: MouseEvent) { draftAnnotation.value = { tool: activeTool.value, color: activeColor.value, + strokeWidth: activeStrokeWidth.value, points: [local], } } @@ -479,7 +517,38 @@ function selectTool(tool: Tool) { function chooseColor(color: string) { activeColor.value = color - paletteOpen.value = false + if (textDraft.value) { + textDraft.value.color = color + } else if ( + activeTool.value === 'text' && + selectedTextIndex.value !== null && + annotations.value[selectedTextIndex.value]?.tool === 'text' + ) { + annotations.value[selectedTextIndex.value].color = color + } +} + +function chooseStrokeWidth(width: number) { + activeStrokeWidth.value = width +} + +function chooseFontSize(size: number) { + activeFontSize.value = size + if (textDraft.value) { + textDraft.value.fontSize = size + textDraft.value.point = clampTextLocalPoint(textDraft.value.point, { + text: textDraft.value.value, + fontSize: size, + }) + } else if ( + activeTool.value === 'text' && + selectedTextIndex.value !== null && + annotations.value[selectedTextIndex.value]?.tool === 'text' + ) { + const annotation = annotations.value[selectedTextIndex.value] + annotation.fontSize = size + annotation.points[0] = clampTextLocalPoint(annotation.points[0], annotation) + } } function onConfirm() { @@ -538,7 +607,9 @@ function onKeydown(e: KeyboardEvent) { } function undoAnnotation() { + if (!textDraft.value && annotations.value.length === 0) return cancelTextDraft() + if (annotations.value.length === 0) return annotations.value.pop() if ( selectedTextIndex.value !== null && @@ -561,10 +632,16 @@ function beginTextAnnotation(point: Point, index: number | null = null) { draftAnnotation.value = null const existing = index !== null ? annotations.value[index] : null const color = existing?.color || activeColor.value + const fontSize = existing?.fontSize || activeFontSize.value + if (existing) { + activeColor.value = color + activeFontSize.value = fontSize + } textDraft.value = { - point: clampTextLocalPoint(point, existing || { text: '', fontSize: TEXT_FONT_SIZE }), + point: clampTextLocalPoint(point, existing || { text: '', fontSize }), value: existing?.text || '', color, + fontSize, index, } void nextTick(() => { @@ -581,10 +658,10 @@ function commitTextDraft() { const target = annotations.value[draft.index] if (target?.tool === 'text') { if (text !== '') { - target.points = [clampTextLocalPoint(draft.point, { text, fontSize: target.fontSize || TEXT_FONT_SIZE })] + target.points = [clampTextLocalPoint(draft.point, { text, fontSize: draft.fontSize })] target.text = text target.color = draft.color - target.fontSize = target.fontSize || TEXT_FONT_SIZE + target.fontSize = draft.fontSize selectedTextIndex.value = draft.index } else { annotations.value.splice(draft.index, 1) @@ -592,13 +669,13 @@ function commitTextDraft() { } } } else if (text !== '') { - const point = clampTextLocalPoint(draft.point, { text, fontSize: TEXT_FONT_SIZE }) + const point = clampTextLocalPoint(draft.point, { text, fontSize: draft.fontSize }) annotations.value.push({ tool: 'text', color: draft.color, points: [point], text, - fontSize: TEXT_FONT_SIZE, + fontSize: draft.fontSize, }) selectedTextIndex.value = annotations.value.length - 1 } @@ -660,7 +737,7 @@ onUnmounted(() => { v-if="annotation.tool === 'pen'" :points="annotation.points.map((p) => `${p.x},${p.y}`).join(' ')" :stroke="annotation.color" - stroke-width="3" + :stroke-width="annotation.strokeWidth || 3" stroke-linecap="round" stroke-linejoin="round" fill="none" @@ -672,7 +749,7 @@ onUnmounted(() => { :width="Math.abs(annotation.points[1].x - annotation.points[0].x)" :height="Math.abs(annotation.points[1].y - annotation.points[0].y)" :stroke="annotation.color" - stroke-width="3" + :stroke-width="annotation.strokeWidth || 3" fill="none" /> { :rx="Math.abs(annotation.points[1].x - annotation.points[0].x) / 2" :ry="Math.abs(annotation.points[1].y - annotation.points[0].y) / 2" :stroke="annotation.color" - stroke-width="3" + :stroke-width="annotation.strokeWidth || 3" fill="none" />