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
109 lines
2.4 KiB
TypeScript
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
|
|
}
|
|
}
|