openpencil/src/stores/tabs.ts
Danila Poyarkov 1b4dc08c54 Add multi-file tabs
Open multiple documents in tabs within a single window, like Figma.

- Tab bar appears when 2+ documents are open, with close (×) and new (+) buttons
- ⌘N / ⌘T create a new empty tab
- ⌘W closes the active tab (last tab closes and reopens as Untitled)
- ⌘O opens .fig files in a new tab (reuses current tab if Untitled with no changes)
- Middle-click closes a tab
- Each tab has its own SceneGraph, UndoManager, viewport, and selection state
- Switching tabs remounts the canvas to rebind the Skia renderer to the active store
- Keyboard shortcuts, menu actions, and AI tools all operate on the active tab
2026-03-02 18:36:27 +03:00

109 lines
2.4 KiB
TypeScript

import { shallowRef, computed } from 'vue'
import { createEditorStore, setActiveEditorStore } from './editor'
import type { EditorStore } from './editor'
export interface Tab {
id: string
store: EditorStore
}
let nextTabId = 1
function generateTabId(): string {
return `tab-${nextTabId++}`
}
const tabsRef = shallowRef<Tab[]>([])
const activeTabId = shallowRef('')
export const activeTab = computed(() => tabsRef.value.find((t) => t.id === activeTabId.value))
export const allTabs = computed(() =>
tabsRef.value.map((t) => ({
id: t.id,
name: t.store.state.documentName,
isActive: t.id === activeTabId.value
}))
)
export function getActiveStore(): EditorStore {
const tab = tabsRef.value.find((t) => t.id === activeTabId.value)
if (!tab) throw new Error('No active tab')
return tab.store
}
export function createTab(store?: EditorStore): Tab {
const s = store ?? createEditorStore()
const tab: Tab = { id: generateTabId(), store: s }
tabsRef.value = [...tabsRef.value, tab]
activateTab(tab)
return tab
}
function activateTab(tab: Tab) {
activeTabId.value = tab.id
setActiveEditorStore(tab.store)
window.__OPEN_PENCIL_STORE__ = tab.store
}
export function switchTab(tabId: string) {
const tab = tabsRef.value.find((t) => t.id === tabId)
if (!tab) return
activateTab(tab)
}
export function closeTab(tabId: string) {
const idx = tabsRef.value.findIndex((t) => t.id === tabId)
if (idx < 0) return
const wasActive = activeTabId.value === tabId
tabsRef.value = tabsRef.value.filter((t) => t.id !== tabId)
if (tabsRef.value.length === 0) {
createTab()
return
}
if (wasActive) {
const newIdx = Math.min(idx, tabsRef.value.length - 1)
activateTab(tabsRef.value[newIdx])
}
}
export async function openFileInNewTab(
file: File,
handle?: FileSystemFileHandle,
path?: string
): Promise<void> {
const current = activeTab.value
const isUntouched =
current && current.store.state.documentName === 'Untitled' && !current.store.undo.canUndo
if (isUntouched) {
await current.store.openFigFile(file, handle, path)
} else {
const store = createEditorStore()
createTab(store)
await store.openFigFile(file, handle, path)
}
}
export function tabCount(): number {
return tabsRef.value.length
}
export function useTabsStore() {
return {
tabs: allTabs,
activeTabId,
createTab,
switchTab,
closeTab,
openFileInNewTab,
getActiveStore,
tabCount
}
}