170 lines
5.2 KiB
Vue
170 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuPortal,
|
|
DropdownMenuRoot,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger
|
|
} from 'reka-ui'
|
|
import { nextTick, ref, watch } from 'vue'
|
|
|
|
import { useEditorCommands, useI18n } from '@open-pencil/vue'
|
|
import { menuItem, useMenuUI } from '@/components/ui/menu'
|
|
import { useEditorStore } from '@/stores/editor'
|
|
|
|
const store = useEditorStore()
|
|
const { getCommand } = useEditorCommands()
|
|
const { menu: menuText, commands, panels } = useI18n()
|
|
|
|
const open = ref(false)
|
|
const editing = ref(false)
|
|
const inputRef = ref<HTMLInputElement | null>()
|
|
const inputValue = ref('')
|
|
|
|
const menuCls = useMenuUI({ content: 'min-w-52' })
|
|
const itemCls = menuItem({ justify: 'start', class: 'relative pl-7' })
|
|
|
|
function zoomPercent() {
|
|
return Math.round(store.state.zoom * 100)
|
|
}
|
|
|
|
function startEditing() {
|
|
editing.value = true
|
|
inputValue.value = String(zoomPercent())
|
|
void nextTick(() => inputRef.value?.select())
|
|
}
|
|
|
|
function commitInput() {
|
|
const parsed = Number.parseInt(inputValue.value, 10)
|
|
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
store.zoomToLevel(parsed / 100)
|
|
}
|
|
editing.value = false
|
|
}
|
|
|
|
function cancelInput() {
|
|
editing.value = false
|
|
}
|
|
|
|
function toggleRulers() {
|
|
store.state.showRulers = !store.state.showRulers
|
|
store.requestRepaint()
|
|
}
|
|
|
|
function toggleRemoteCursors() {
|
|
store.state.showRemoteCursors = !store.state.showRemoteCursors
|
|
store.requestRepaint()
|
|
}
|
|
|
|
function zoomIn() {
|
|
const center = store.viewportScreenCenter()
|
|
store.applyZoom(-100, center.x, center.y)
|
|
}
|
|
|
|
function zoomOut() {
|
|
const center = store.viewportScreenCenter()
|
|
store.applyZoom(100, center.x, center.y)
|
|
}
|
|
|
|
const ZOOM_PRESETS: ReadonlyArray<{ label: string; level: number; shortcut?: string }> = [
|
|
{ label: '50%', level: 0.5 },
|
|
{ label: '100%', level: 1, shortcut: '⌘0' },
|
|
{ label: '200%', level: 2 }
|
|
]
|
|
|
|
function isActivePreset(level: number) {
|
|
return Math.abs(store.state.zoom - level) < 0.005
|
|
}
|
|
|
|
watch(open, (v) => {
|
|
if (!v) editing.value = false
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<DropdownMenuRoot v-model:open="open">
|
|
<DropdownMenuTrigger as-child>
|
|
<button
|
|
data-test-id="zoom-dropdown-trigger"
|
|
class="ml-auto cursor-pointer rounded px-1.5 py-0.5 text-[11px] text-muted hover:bg-hover"
|
|
>
|
|
{{ zoomPercent() }}%
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuPortal>
|
|
<DropdownMenuContent
|
|
side="bottom"
|
|
:side-offset="4"
|
|
align="end"
|
|
:class="menuCls.content"
|
|
@escape-key-down="cancelInput"
|
|
>
|
|
<div class="px-1 py-1">
|
|
<input
|
|
v-if="editing"
|
|
ref="inputRef"
|
|
v-model="inputValue"
|
|
data-test-id="zoom-input"
|
|
class="w-full rounded border border-accent bg-input px-2 py-1 text-xs text-surface outline-none"
|
|
@blur="commitInput"
|
|
@keydown.enter="commitInput"
|
|
@keydown.escape.stop="cancelInput"
|
|
/>
|
|
<button
|
|
v-else
|
|
data-test-id="zoom-input-trigger"
|
|
class="w-full cursor-pointer rounded border border-border bg-input px-2 py-1 text-left text-xs text-surface hover:border-muted"
|
|
@click="startEditing"
|
|
>
|
|
{{ zoomPercent() }}%
|
|
</button>
|
|
</div>
|
|
|
|
<DropdownMenuSeparator :class="menuCls.separator" />
|
|
|
|
<DropdownMenuItem :class="itemCls" @select="zoomIn">
|
|
<span class="flex-1">{{ menuText.zoomIn }}</span>
|
|
<span class="text-[11px] text-muted">⌘+</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem :class="itemCls" @select="zoomOut">
|
|
<span class="flex-1">{{ menuText.zoomOut }}</span>
|
|
<span class="text-[11px] text-muted">⌘−</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem :class="itemCls" @select="getCommand('view.zoomFit').run()">
|
|
<span class="flex-1">{{ commands.zoomToFit }}</span>
|
|
<span class="text-[11px] text-muted">⇧1</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
v-for="preset in ZOOM_PRESETS"
|
|
:key="preset.level"
|
|
:class="itemCls"
|
|
@select="store.zoomToLevel(preset.level)"
|
|
>
|
|
<icon-lucide-check
|
|
v-if="isActivePreset(preset.level)"
|
|
class="absolute left-2 size-3.5"
|
|
/>
|
|
<span class="flex-1">{{ preset.label }}</span>
|
|
<span v-if="preset.shortcut" class="text-[11px] text-muted">{{ preset.shortcut }}</span>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator :class="menuCls.separator" />
|
|
|
|
<DropdownMenuItem :class="itemCls" @select.prevent="toggleRulers">
|
|
<icon-lucide-check v-if="store.state.showRulers" class="absolute left-2 size-3.5" />
|
|
<span class="flex-1">{{ panels.rulers }}</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem :class="itemCls" @select.prevent="toggleRemoteCursors">
|
|
<icon-lucide-check
|
|
v-if="store.state.showRemoteCursors"
|
|
class="absolute left-2 size-3.5"
|
|
/>
|
|
<span class="flex-1">{{ panels.multiplayerCursors }}</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenuPortal>
|
|
</DropdownMenuRoot>
|
|
</template>
|