Skip to content

Commit b4feca5

Browse files
committed
refactor: enhance preview bounds handling in DesktopPreviewController and PreviewPanel
- Updated the `applyViewBounds` method to return a boolean indicating success, improving visibility handling. - Introduced a constant for hidden preview bounds in `PreviewPanel` to streamline state management. - Refactored bounds synchronization logic to ensure accurate updates based on visibility and dimensions. - Improved cleanup logic in the `useEffect` hook to maintain consistent state during component unmounting.
1 parent f7928af commit b4feca5

2 files changed

Lines changed: 84 additions & 46 deletions

File tree

apps/desktop/src/previewController.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ export class DesktopPreviewController {
126126

127127
setBounds(bounds: DesktopPreviewBounds): void {
128128
this.bounds = sanitizeDesktopPreviewBounds(bounds);
129-
this.applyViewBounds();
129+
const appliedVisible = this.applyViewBounds();
130130
if (this.state.status !== "closed") {
131131
this.setState({
132132
...this.state,
133-
visible: previewVisible(this.state, this.bounds),
133+
visible: previewVisible(this.state, this.bounds) && appliedVisible,
134134
});
135135
}
136136
}
@@ -275,20 +275,35 @@ export class DesktopPreviewController {
275275
return { action: "deny" };
276276
}
277277

278-
private applyViewBounds(): void {
278+
private applyViewBounds(): boolean {
279279
if (!this.view || this.view.webContents.isDestroyed()) {
280-
return;
280+
return false;
281281
}
282282

283-
const nextBounds = this.bounds.visible
284-
? {
285-
x: this.bounds.x,
286-
y: this.bounds.y,
287-
width: this.bounds.width,
288-
height: this.bounds.height,
289-
}
290-
: { x: 0, y: 0, width: 0, height: 0 };
291-
this.view.setBounds(nextBounds);
283+
const contentBounds = this.window.getContentBounds();
284+
const rawWidth = Math.round(this.bounds.width);
285+
const rawHeight = Math.round(this.bounds.height);
286+
287+
if (
288+
!this.bounds.visible ||
289+
rawWidth <= 0 ||
290+
rawHeight <= 0 ||
291+
contentBounds.width <= 0 ||
292+
contentBounds.height <= 0
293+
) {
294+
this.view.setBounds({ x: 0, y: 0, width: 0, height: 0 });
295+
return false;
296+
}
297+
298+
const width = Math.min(rawWidth, contentBounds.width);
299+
const height = Math.min(rawHeight, contentBounds.height);
300+
const maxX = Math.max(0, contentBounds.width - width);
301+
const maxY = Math.max(0, contentBounds.height - height);
302+
const x = Math.max(0, Math.min(Math.round(this.bounds.x), maxX));
303+
const y = Math.max(0, Math.min(Math.round(this.bounds.y), maxY));
304+
305+
this.view.setBounds({ x, y, width, height });
306+
return width > 0 && height > 0;
292307
}
293308

294309
private disposeView(): void {

apps/web/src/components/PreviewPanel.tsx

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ const CLOSED_PREVIEW_STATE: DesktopPreviewState = {
2525
error: null,
2626
};
2727

28+
const HIDDEN_PREVIEW_BOUNDS = {
29+
x: 0,
30+
y: 0,
31+
width: 0,
32+
height: 0,
33+
visible: false,
34+
} as const;
35+
2836
export function resolvePreviewStatusCopy(state: DesktopPreviewState): string {
2937
if (state.error) {
3038
return state.error.message;
@@ -88,14 +96,21 @@ export function PreviewPanel({ threadId, projectId, projectName, onClose }: Prev
8896
}
8997

9098
if (storedUrl.trim().length === 0) {
91-
void previewBridge.close();
99+
void previewBridge.setBounds(HIDDEN_PREVIEW_BOUNDS).finally(() => {
100+
void previewBridge.close();
101+
});
92102
setPreviewState(CLOSED_PREVIEW_STATE);
93103
return;
94104
}
95105

96-
void previewBridge.close().finally(() => {
97-
void previewBridge.open({ url: storedUrl, title: `${projectName} preview` });
98-
});
106+
void previewBridge
107+
.setBounds(HIDDEN_PREVIEW_BOUNDS)
108+
.catch(() => undefined)
109+
.finally(() => {
110+
void previewBridge.close().finally(() => {
111+
void previewBridge.open({ url: storedUrl, title: `${projectName} preview` });
112+
});
113+
});
99114
}, [previewBridge, projectName, storedUrl, threadId]);
100115

101116
useEffect(() => {
@@ -114,33 +129,43 @@ export function PreviewPanel({ threadId, projectId, projectName, onClose }: Prev
114129
let lastBoundsKey = "";
115130
let resizeObserver: ResizeObserver | null = null;
116131

117-
const syncBounds = () => {
118-
frameId = 0;
119-
if (destroyed) {
120-
return;
121-
}
122-
132+
const computeBounds = () => {
123133
const element = surfaceRef.current;
124134
if (!element) {
125-
return;
135+
return HIDDEN_PREVIEW_BOUNDS;
126136
}
127137

128138
const rect = element.getBoundingClientRect();
129-
const nextBounds = {
139+
const visible =
140+
storedUrl.trim().length > 0 &&
141+
document.visibilityState === "visible" &&
142+
rect.width > 0 &&
143+
rect.height > 0;
144+
return {
130145
x: rect.left,
131146
y: rect.top,
132147
width: rect.width,
133148
height: rect.height,
134-
visible: rect.width > 0 && rect.height > 0,
149+
visible,
135150
};
151+
};
152+
153+
const syncBounds = () => {
154+
if (destroyed) {
155+
return;
156+
}
157+
158+
const nextBounds = computeBounds();
136159
const nextKey = `${Math.round(nextBounds.x)}:${Math.round(nextBounds.y)}:${Math.round(nextBounds.width)}:${Math.round(nextBounds.height)}:${nextBounds.visible ? 1 : 0}`;
137160
if (nextKey !== lastBoundsKey) {
138161
lastBoundsKey = nextKey;
139162
void previewBridge.setBounds(nextBounds);
140163
}
164+
165+
frameId = window.requestAnimationFrame(syncBounds);
141166
};
142167

143-
const scheduleSync = () => {
168+
const scheduleImmediateSync = () => {
144169
if (destroyed || frameId !== 0) {
145170
return;
146171
}
@@ -150,38 +175,36 @@ export function PreviewPanel({ threadId, projectId, projectName, onClose }: Prev
150175
const element = surfaceRef.current;
151176
if (typeof ResizeObserver !== "undefined" && element) {
152177
resizeObserver = new ResizeObserver(() => {
153-
scheduleSync();
178+
lastBoundsKey = "";
154179
});
155180
resizeObserver.observe(element);
156181
}
157182

158183
const visualViewport = window.visualViewport;
159-
window.addEventListener("resize", scheduleSync);
160-
window.addEventListener("scroll", scheduleSync, true);
161-
visualViewport?.addEventListener("resize", scheduleSync);
162-
visualViewport?.addEventListener("scroll", scheduleSync);
184+
const invalidateBounds = () => {
185+
lastBoundsKey = "";
186+
};
163187

164-
scheduleSync();
165-
const settleTimeoutId = window.setTimeout(scheduleSync, 50);
188+
window.addEventListener("resize", invalidateBounds);
189+
window.addEventListener("scroll", invalidateBounds, true);
190+
document.addEventListener("visibilitychange", invalidateBounds);
191+
visualViewport?.addEventListener("resize", invalidateBounds);
192+
visualViewport?.addEventListener("scroll", invalidateBounds);
193+
194+
scheduleImmediateSync();
166195

167196
return () => {
168197
destroyed = true;
169-
window.clearTimeout(settleTimeoutId);
170198
if (frameId !== 0) {
171199
window.cancelAnimationFrame(frameId);
172200
}
173201
resizeObserver?.disconnect();
174-
window.removeEventListener("resize", scheduleSync);
175-
window.removeEventListener("scroll", scheduleSync, true);
176-
visualViewport?.removeEventListener("resize", scheduleSync);
177-
visualViewport?.removeEventListener("scroll", scheduleSync);
178-
void previewBridge.setBounds({
179-
x: 0,
180-
y: 0,
181-
width: 0,
182-
height: 0,
183-
visible: false,
184-
});
202+
window.removeEventListener("resize", invalidateBounds);
203+
window.removeEventListener("scroll", invalidateBounds, true);
204+
document.removeEventListener("visibilitychange", invalidateBounds);
205+
visualViewport?.removeEventListener("resize", invalidateBounds);
206+
visualViewport?.removeEventListener("scroll", invalidateBounds);
207+
void previewBridge.setBounds(HIDDEN_PREVIEW_BOUNDS);
185208
};
186209
}, [previewBridge, storedUrl, threadId, projectId]);
187210

0 commit comments

Comments
 (0)