import { create } from "zustand"; export interface BrowserSurfaceRect { readonly x: number; readonly y: number; readonly width: number; readonly height: number; } export interface BrowserSurfacePresentation { readonly rect: BrowserSurfaceRect | null; readonly visible: boolean; readonly content: BrowserSurfaceContentPresentation | null; readonly updatedAt: number; readonly owner: symbol | null; } export interface BrowserSurfaceContentPresentation { readonly x: number; readonly y: number; readonly width: number; readonly height: number; readonly scale: number; readonly scrollLeft: number; readonly scrollTop: number; } interface BrowserSurfaceStoreState { readonly byTabId: Record; readonly claim: (tabId: string, owner: symbol) => void; readonly present: ( tabId: string, owner: symbol, rect: BrowserSurfaceRect, visible: boolean, ) => void; readonly presentContent: (tabId: string, content: BrowserSurfaceContentPresentation) => void; readonly release: (tabId: string, owner: symbol) => void; } export interface BrowserSurfaceLease { readonly present: (rect: BrowserSurfaceRect, visible: boolean) => void; readonly release: () => void; } export function resolveBrowserSurfacePanelRect( byTabId: Readonly>, tabId: string, ): BrowserSurfaceRect | null { const current = byTabId[tabId]; if (current?.visible && current.rect) return current.rect; let latestVisible: BrowserSurfacePresentation | undefined; for (const presentation of Object.values(byTabId)) { if ( presentation.visible && presentation.rect && (!latestVisible || presentation.updatedAt > latestVisible.updatedAt) ) { latestVisible = presentation; } } return latestVisible?.rect ?? current?.rect ?? null; } const rectEquals = (left: BrowserSurfaceRect | null, right: BrowserSurfaceRect): boolean => left !== null && left.x === right.x && left.y === right.y && left.width === right.width && left.height === right.height; export const useBrowserSurfaceStore = create()((set) => ({ byTabId: {}, claim: (tabId, owner) => set((state) => { const current = state.byTabId[tabId]; if (current?.owner === owner) return state; return { byTabId: { ...state.byTabId, [tabId]: { rect: current?.rect ?? null, visible: false, content: current?.content ?? null, updatedAt: Date.now(), owner, }, }, }; }), present: (tabId, owner, rect, visible) => set((state) => { const current = state.byTabId[tabId]; if (current?.owner !== owner) return state; if (current && current.visible === visible && rectEquals(current.rect, rect)) return state; return { byTabId: { ...state.byTabId, [tabId]: { ...current, rect, visible, updatedAt: Date.now() }, }, }; }), presentContent: (tabId, content) => set((state) => { const current = state.byTabId[tabId]; if (!current) { return { byTabId: { ...state.byTabId, [tabId]: { rect: null, visible: false, content, updatedAt: Date.now(), owner: null, }, }, }; } const previous = current.content; if ( previous && previous.x === content.x && previous.y === content.y && previous.width === content.width && previous.height === content.height && previous.scale === content.scale && previous.scrollLeft === content.scrollLeft && previous.scrollTop === content.scrollTop ) { return state; } return { byTabId: { ...state.byTabId, [tabId]: { ...current, content, updatedAt: Date.now() }, }, }; }), release: (tabId, owner) => set((state) => { const current = state.byTabId[tabId]; if (current?.owner !== owner) return state; return { byTabId: { ...state.byTabId, [tabId]: { ...current, visible: false, updatedAt: Date.now(), owner: null }, }, }; }), })); export function acquireBrowserSurface(tabId: string): BrowserSurfaceLease { const owner = Symbol(`browser-surface:${tabId}`); let released = false; useBrowserSurfaceStore.getState().claim(tabId, owner); return { present: (rect, visible) => { if (released) return; useBrowserSurfaceStore.getState().present(tabId, owner, rect, visible); }, release: () => { if (released) return; released = true; useBrowserSurfaceStore.getState().release(tabId, owner); }, }; }