Skip to content

Commit 8d642d1

Browse files
committed
Add viewport-aware preview bounds projection
- include viewport dimensions in desktop preview bounds - project and clamp preview view bounds to native content size - add tests for sanitization and bounds projection
1 parent 50a8de7 commit 8d642d1

8 files changed

Lines changed: 175 additions & 24 deletions

File tree

apps/desktop/src/preview.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,17 @@ describe("sanitizeDesktopPreviewBounds", () => {
6868
width: 480.2,
6969
height: 320.8,
7070
visible: true,
71+
viewportWidth: 1440.2,
72+
viewportHeight: 900.8,
7173
}),
7274
).toEqual({
7375
x: 10,
7476
y: 21,
7577
width: 480,
7678
height: 321,
7779
visible: true,
80+
viewportWidth: 1440,
81+
viewportHeight: 901,
7882
});
7983

8084
expect(
@@ -84,13 +88,17 @@ describe("sanitizeDesktopPreviewBounds", () => {
8488
width: -10,
8589
height: 0,
8690
visible: true,
91+
viewportWidth: Number.NaN,
92+
viewportHeight: Number.NaN,
8793
}),
8894
).toEqual({
8995
x: 0,
9096
y: 0,
9197
width: 0,
9298
height: 0,
9399
visible: false,
100+
viewportWidth: 0,
101+
viewportHeight: 0,
94102
});
95103
});
96104
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { projectPreviewBoundsToContent } from "./previewBounds";
4+
5+
describe("projectPreviewBoundsToContent", () => {
6+
it("maps renderer bounds into native content coordinates using viewport scaling", () => {
7+
expect(
8+
projectPreviewBoundsToContent(
9+
{
10+
x: 100,
11+
y: 52,
12+
width: 400,
13+
height: 300,
14+
visible: true,
15+
viewportWidth: 1000,
16+
viewportHeight: 800,
17+
},
18+
{ width: 1200, height: 960 },
19+
),
20+
).toEqual({
21+
x: 120,
22+
y: 62,
23+
width: 480,
24+
height: 360,
25+
});
26+
});
27+
28+
it("clamps projected bounds to the native content area", () => {
29+
expect(
30+
projectPreviewBoundsToContent(
31+
{
32+
x: 700,
33+
y: 580,
34+
width: 300,
35+
height: 220,
36+
visible: true,
37+
viewportWidth: 1000,
38+
viewportHeight: 800,
39+
},
40+
{ width: 900, height: 700 },
41+
),
42+
).toEqual({
43+
x: 630,
44+
y: 507,
45+
width: 270,
46+
height: 193,
47+
});
48+
});
49+
50+
it("falls back to content-space coordinates when viewport metadata is unavailable", () => {
51+
expect(
52+
projectPreviewBoundsToContent(
53+
{
54+
x: 64,
55+
y: 96,
56+
width: 320,
57+
height: 240,
58+
visible: true,
59+
viewportWidth: 0,
60+
viewportHeight: 0,
61+
},
62+
{ width: 1200, height: 900 },
63+
),
64+
).toEqual({
65+
x: 64,
66+
y: 96,
67+
width: 320,
68+
height: 240,
69+
});
70+
});
71+
72+
it("hides the native view for invisible or empty regions", () => {
73+
expect(
74+
projectPreviewBoundsToContent(
75+
{
76+
x: 10,
77+
y: 10,
78+
width: 320,
79+
height: 240,
80+
visible: false,
81+
viewportWidth: 800,
82+
viewportHeight: 600,
83+
},
84+
{ width: 1200, height: 900 },
85+
),
86+
).toEqual({
87+
x: 0,
88+
y: 0,
89+
width: 0,
90+
height: 0,
91+
});
92+
});
93+
});

apps/desktop/src/previewBounds.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Rectangle } from "electron";
2+
import type { DesktopPreviewBounds } from "@okcode/contracts";
3+
4+
function normalizeAxisScale(contentSize: number, viewportSize: number): number {
5+
if (contentSize <= 0) {
6+
return 1;
7+
}
8+
if (viewportSize <= 0) {
9+
return 1;
10+
}
11+
return contentSize / viewportSize;
12+
}
13+
14+
export function projectPreviewBoundsToContent(
15+
bounds: DesktopPreviewBounds,
16+
contentBounds: Pick<Rectangle, "width" | "height">,
17+
): Rectangle {
18+
const contentWidth = Math.max(0, Math.round(contentBounds.width));
19+
const contentHeight = Math.max(0, Math.round(contentBounds.height));
20+
21+
if (
22+
!bounds.visible ||
23+
bounds.width <= 0 ||
24+
bounds.height <= 0 ||
25+
contentWidth <= 0 ||
26+
contentHeight <= 0
27+
) {
28+
return { x: 0, y: 0, width: 0, height: 0 };
29+
}
30+
31+
const scaleX = normalizeAxisScale(contentWidth, bounds.viewportWidth);
32+
const scaleY = normalizeAxisScale(contentHeight, bounds.viewportHeight);
33+
const width = Math.min(Math.max(0, Math.round(bounds.width * scaleX)), contentWidth);
34+
const height = Math.min(Math.max(0, Math.round(bounds.height * scaleY)), contentHeight);
35+
36+
if (width <= 0 || height <= 0) {
37+
return { x: 0, y: 0, width: 0, height: 0 };
38+
}
39+
40+
const maxX = Math.max(0, contentWidth - width);
41+
const maxY = Math.max(0, contentHeight - height);
42+
const x = Math.max(0, Math.min(Math.round(bounds.x * scaleX), maxX));
43+
const y = Math.max(0, Math.min(Math.round(bounds.y * scaleY), maxY));
44+
return { x, y, width, height };
45+
}

apps/desktop/src/previewController.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
sanitizeDesktopPreviewBounds,
1313
validateDesktopPreviewUrl,
1414
} from "./preview";
15+
import { projectPreviewBoundsToContent } from "./previewBounds";
1516

1617
const PREVIEW_WEB_PREFERENCES = {
1718
contextIsolation: true,
@@ -36,6 +37,8 @@ export class DesktopPreviewController {
3637
width: 0,
3738
height: 0,
3839
visible: false,
40+
viewportWidth: 0,
41+
viewportHeight: 0,
3942
};
4043
private unsubscribers: Array<() => void> = [];
4144
private disposingView = false;
@@ -150,6 +153,7 @@ export class DesktopPreviewController {
150153
const view = new WebContentsView({
151154
webPreferences: PREVIEW_WEB_PREFERENCES,
152155
});
156+
view.setBorderRadius(8);
153157
this.view = view;
154158
this.window.contentView.addChildView(view);
155159
this.bindView(view);
@@ -280,30 +284,9 @@ export class DesktopPreviewController {
280284
return false;
281285
}
282286

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;
287+
const nextBounds = projectPreviewBoundsToContent(this.bounds, this.window.getContentBounds());
288+
this.view.setBounds(nextBounds);
289+
return nextBounds.width > 0 && nextBounds.height > 0;
307290
}
308291

309292
private disposeView(): void {

apps/web/src/components/PreviewPanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const HIDDEN_PREVIEW_BOUNDS = {
3131
width: 0,
3232
height: 0,
3333
visible: false,
34+
viewportWidth: 0,
35+
viewportHeight: 0,
3436
} as const;
3537

3638
export function resolvePreviewStatusCopy(state: DesktopPreviewState): string {
@@ -147,6 +149,8 @@ export function PreviewPanel({ threadId, projectId, projectName, onClose }: Prev
147149
width: rect.width,
148150
height: rect.height,
149151
visible,
152+
viewportWidth: window.innerWidth,
153+
viewportHeight: window.innerHeight,
150154
};
151155
};
152156

packages/contracts/src/ipc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export interface DesktopPreviewBounds {
120120
width: number;
121121
height: number;
122122
visible: boolean;
123+
viewportWidth: number;
124+
viewportHeight: number;
123125
}
124126

125127
export interface DesktopPreviewState {

packages/shared/src/preview.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ describe("sanitizeLocalPreviewBounds", () => {
5555
width: 480.4,
5656
height: 320.6,
5757
visible: true,
58+
viewportWidth: 1440.4,
59+
viewportHeight: 900.6,
5860
};
5961
expect(sanitizeLocalPreviewBounds(floatingBounds)).toEqual({
6062
x: 10,
6163
y: 21,
6264
width: 480,
6365
height: 321,
6466
visible: true,
67+
viewportWidth: 1440,
68+
viewportHeight: 901,
6569
});
6670

6771
expect(
@@ -71,13 +75,17 @@ describe("sanitizeLocalPreviewBounds", () => {
7175
width: -10,
7276
height: 0,
7377
visible: true,
78+
viewportWidth: Number.NaN,
79+
viewportHeight: Number.NaN,
7480
}),
7581
).toEqual({
7682
x: 0,
7783
y: 0,
7884
width: 0,
7985
height: 0,
8086
visible: false,
87+
viewportWidth: 0,
88+
viewportHeight: 0,
8189
});
8290
});
8391
});

packages/shared/src/preview.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,20 @@ export function validateHttpPreviewUrl(
9090
export function sanitizeLocalPreviewBounds(bounds: DesktopPreviewBounds): DesktopPreviewBounds {
9191
const width = Number.isFinite(bounds.width) ? Math.max(0, Math.round(bounds.width)) : 0;
9292
const height = Number.isFinite(bounds.height) ? Math.max(0, Math.round(bounds.height)) : 0;
93+
const viewportWidth = Number.isFinite(bounds.viewportWidth)
94+
? Math.max(0, Math.round(bounds.viewportWidth))
95+
: 0;
96+
const viewportHeight = Number.isFinite(bounds.viewportHeight)
97+
? Math.max(0, Math.round(bounds.viewportHeight))
98+
: 0;
9399

94100
return {
95101
x: Number.isFinite(bounds.x) ? Math.round(bounds.x) : 0,
96102
y: Number.isFinite(bounds.y) ? Math.round(bounds.y) : 0,
97103
width,
98104
height,
105+
viewportWidth,
106+
viewportHeight,
99107
visible: bounds.visible && width > 0 && height > 0,
100108
};
101109
}

0 commit comments

Comments
 (0)