1import { create } from "zustand";
2
3export interface BrowserSurfaceRect {
4 readonly x: number;
5 readonly y: number;
6 readonly width: number;
7 readonly height: number;
8}
9
10export interface BrowserSurfacePresentation {
11 readonly rect: BrowserSurfaceRect | null;
12 readonly visible: boolean;
13 readonly zIndex: number;
14 readonly content: BrowserSurfaceContentPresentation | null;
15 readonly fittedSourceContent: BrowserSurfaceContentPresentation | null;
16 readonly fitSourceContent: boolean;
17 readonly cornerRadius: number;
18 readonly updatedAt: number;
19 readonly owner: symbol | null;
20}
21
22export interface BrowserSurfaceContentPresentation {
23 readonly x: number;
24 readonly y: number;
25 readonly width: number;
26 readonly height: number;
27 readonly scale: number;
28 readonly scrollLeft: number;
29 readonly scrollTop: number;
30}
31
32interface BrowserSurfaceStoreState {
33 readonly activityByTabId: Record<string, number>;
34 readonly byTabId: Record<string, BrowserSurfacePresentation>;
35 readonly acquireActivity: (tabId: string) => () => void;
36 readonly claim: (tabId: string, owner: symbol, fitSourceContent: boolean) => void;
37 readonly present: (
38 tabId: string,
39 owner: symbol,
40 rect: BrowserSurfaceRect,
41 visible: boolean,
42 cornerRadius: number,
43 zIndex: number,
44 ) => void;
45 readonly presentContent: (tabId: string, content: BrowserSurfaceContentPresentation) => void;
46 readonly release: (tabId: string, owner: symbol) => void;
47}
48
49export interface BrowserSurfaceLease {
50 readonly present: (
51 rect: BrowserSurfaceRect,
52 visible: boolean,
53 cornerRadius?: number,
54 zIndex?: number,
55 ) => boolean;
56 readonly release: () => void;
57}
58
59export function resolveBrowserSurfacePanelRect(
60 byTabId: Readonly<Record<string, BrowserSurfacePresentation>>,
61 tabId: string,
62): BrowserSurfaceRect | null {
63 const current = byTabId[tabId];
64 return current?.rect ?? null;
65}
66
67const rectEquals = (left: BrowserSurfaceRect | null, right: BrowserSurfaceRect): boolean =>
68 left !== null &&
69 left.x === right.x &&
70 left.y === right.y &&
71 left.width === right.width &&
72 left.height === right.height;
73
74export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) => ({
75 activityByTabId: {},
76 byTabId: {},
77 acquireActivity: (tabId) => {
78 let released = false;
79 set((state) => ({
80 activityByTabId: {
81 ...state.activityByTabId,
82 [tabId]: (state.activityByTabId[tabId] ?? 0) + 1,
83 },
84 }));
85 return () => {
86 if (released) return;
87 released = true;
88 set((state) => {
89 const count = state.activityByTabId[tabId] ?? 0;
90 const activityByTabId = { ...state.activityByTabId };
91 if (count <= 1) delete activityByTabId[tabId];
92 else activityByTabId[tabId] = count - 1;
93 return { activityByTabId };
94 });
95 };
96 },
97 claim: (tabId, owner, fitSourceContent) =>
98 set((state) => {
99 const current = state.byTabId[tabId];
100 if (current?.owner === owner) return state;
101 return {
102 byTabId: {
103 ...state.byTabId,
104 [tabId]: {
105 rect: current?.rect ?? null,
106 visible: false,
107 zIndex: current?.zIndex ?? 30,
108 content: current?.content ?? null,
109 fittedSourceContent: fitSourceContent ? (current?.content ?? null) : null,
110 fitSourceContent,
111 cornerRadius: current?.cornerRadius ?? 0,
112 updatedAt: Date.now(),
113 owner,
114 },
115 },
116 };
117 }),
118 present: (tabId, owner, rect, visible, cornerRadius, zIndex) =>
119 set((state) => {
120 const current = state.byTabId[tabId];
121 if (current?.owner !== owner) return state;
122 if (
123 current &&
124 current.visible === visible &&
125 current.cornerRadius === cornerRadius &&
126 current.zIndex === zIndex &&
127 rectEquals(current.rect, rect)
128 ) {
129 return state;
130 }
131 return {
132 byTabId: {
133 ...state.byTabId,
134 [tabId]: { ...current, rect, visible, cornerRadius, zIndex, updatedAt: Date.now() },
135 },
136 };
137 }),
138 presentContent: (tabId, content) =>
139 set((state) => {
140 const current = state.byTabId[tabId];
141 if (!current) {
142 return {
143 byTabId: {
144 ...state.byTabId,
145 [tabId]: {
146 rect: null,
147 visible: false,
148 zIndex: 30,
149 content,
150 fittedSourceContent: null,
151 fitSourceContent: false,
152 cornerRadius: 0,
153 updatedAt: Date.now(),
154 owner: null,
155 },
156 },
157 };
158 }
159 const previous = current.content;
160 if (
161 previous &&
162 previous.x === content.x &&
163 previous.y === content.y &&
164 previous.width === content.width &&
165 previous.height === content.height &&
166 previous.scale === content.scale &&
167 previous.scrollLeft === content.scrollLeft &&
168 previous.scrollTop === content.scrollTop
169 ) {
170 return state;
171 }
172 return {
173 byTabId: {
174 ...state.byTabId,
175 [tabId]: {
176 ...current,
177 content,
178 fittedSourceContent:
179 current.fitSourceContent && current.fittedSourceContent === null
180 ? content
181 : current.fittedSourceContent,
182 updatedAt: Date.now(),
183 },
184 },
185 };
186 }),
187 release: (tabId, owner) =>
188 set((state) => {
189 const current = state.byTabId[tabId];
190 if (current?.owner !== owner) return state;
191 return {
192 byTabId: {
193 ...state.byTabId,
194 [tabId]: {
195 ...current,
196 visible: false,
197 fittedSourceContent: null,
198 fitSourceContent: false,
199 updatedAt: Date.now(),
200 owner: null,
201 },
202 },
203 };
204 }),
205}));
206
207export const acquireBrowserSurfaceActivity = (tabId: string): (() => void) =>
208 useBrowserSurfaceStore.getState().acquireActivity(tabId);
209
210export function acquireBrowserSurface(
211 tabId: string,
212 fitSourceContent = false,
213): BrowserSurfaceLease {
214 const owner = Symbol(`browser-surface:${tabId}`);
215 let released = false;
216 useBrowserSurfaceStore.getState().claim(tabId, owner, fitSourceContent);
217
218 return {
219 present: (rect, visible, cornerRadius = 0, zIndex = 30) => {
220 if (released) return false;
221 if (useBrowserSurfaceStore.getState().byTabId[tabId]?.owner !== owner) return false;
222 useBrowserSurfaceStore.getState().present(tabId, owner, rect, visible, cornerRadius, zIndex);
223 return true;
224 },
225 release: () => {
226 if (released) return;
227 released = true;
228 useBrowserSurfaceStore.getState().release(tabId, owner);
229 },
230 };
231}