Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/desktop/src/preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,17 @@ describe("sanitizeDesktopPreviewBounds", () => {
width: 480.2,
height: 320.8,
visible: true,
viewportWidth: 1440.2,
viewportHeight: 900.8,
}),
).toEqual({
x: 10,
y: 21,
width: 480,
height: 321,
visible: true,
viewportWidth: 1440,
viewportHeight: 901,
});

expect(
Expand All @@ -84,13 +88,17 @@ describe("sanitizeDesktopPreviewBounds", () => {
width: -10,
height: 0,
visible: true,
viewportWidth: Number.NaN,
viewportHeight: Number.NaN,
}),
).toEqual({
x: 0,
y: 0,
width: 0,
height: 0,
visible: false,
viewportWidth: 0,
viewportHeight: 0,
});
});
});
Expand Down
93 changes: 93 additions & 0 deletions apps/desktop/src/previewBounds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";

import { projectPreviewBoundsToContent } from "./previewBounds";

describe("projectPreviewBoundsToContent", () => {
it("maps renderer bounds into native content coordinates using viewport scaling", () => {
expect(
projectPreviewBoundsToContent(
{
x: 100,
y: 52,
width: 400,
height: 300,
visible: true,
viewportWidth: 1000,
viewportHeight: 800,
},
{ width: 1200, height: 960 },
),
).toEqual({
x: 120,
y: 62,
width: 480,
height: 360,
});
});

it("clamps projected bounds to the native content area", () => {
expect(
projectPreviewBoundsToContent(
{
x: 700,
y: 580,
width: 300,
height: 220,
visible: true,
viewportWidth: 1000,
viewportHeight: 800,
},
{ width: 900, height: 700 },
),
).toEqual({
x: 630,
y: 507,
width: 270,
height: 193,
});
});

it("falls back to content-space coordinates when viewport metadata is unavailable", () => {
expect(
projectPreviewBoundsToContent(
{
x: 64,
y: 96,
width: 320,
height: 240,
visible: true,
viewportWidth: 0,
viewportHeight: 0,
},
{ width: 1200, height: 900 },
),
).toEqual({
x: 64,
y: 96,
width: 320,
height: 240,
});
});

it("hides the native view for invisible or empty regions", () => {
expect(
projectPreviewBoundsToContent(
{
x: 10,
y: 10,
width: 320,
height: 240,
visible: false,
viewportWidth: 800,
viewportHeight: 600,
},
{ width: 1200, height: 900 },
),
).toEqual({
x: 0,
y: 0,
width: 0,
height: 0,
});
});
});
45 changes: 45 additions & 0 deletions apps/desktop/src/previewBounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Rectangle } from "electron";
import type { DesktopPreviewBounds } from "@okcode/contracts";

function normalizeAxisScale(contentSize: number, viewportSize: number): number {
if (contentSize <= 0) {
return 1;
}
if (viewportSize <= 0) {
return 1;
}
return contentSize / viewportSize;
}

export function projectPreviewBoundsToContent(
bounds: DesktopPreviewBounds,
contentBounds: Pick<Rectangle, "width" | "height">,
): Rectangle {
const contentWidth = Math.max(0, Math.round(contentBounds.width));
const contentHeight = Math.max(0, Math.round(contentBounds.height));

if (
!bounds.visible ||
bounds.width <= 0 ||
bounds.height <= 0 ||
contentWidth <= 0 ||
contentHeight <= 0
) {
return { x: 0, y: 0, width: 0, height: 0 };
}

const scaleX = normalizeAxisScale(contentWidth, bounds.viewportWidth);
const scaleY = normalizeAxisScale(contentHeight, bounds.viewportHeight);
const width = Math.min(Math.max(0, Math.round(bounds.width * scaleX)), contentWidth);
const height = Math.min(Math.max(0, Math.round(bounds.height * scaleY)), contentHeight);

if (width <= 0 || height <= 0) {
return { x: 0, y: 0, width: 0, height: 0 };
}

const maxX = Math.max(0, contentWidth - width);
const maxY = Math.max(0, contentHeight - height);
const x = Math.max(0, Math.min(Math.round(bounds.x * scaleX), maxX));
const y = Math.max(0, Math.min(Math.round(bounds.y * scaleY), maxY));
return { x, y, width, height };
}
31 changes: 7 additions & 24 deletions apps/desktop/src/previewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
sanitizeDesktopPreviewBounds,
validateDesktopPreviewUrl,
} from "./preview";
import { projectPreviewBoundsToContent } from "./previewBounds";

const PREVIEW_WEB_PREFERENCES = {
contextIsolation: true,
Expand All @@ -36,6 +37,8 @@ export class DesktopPreviewController {
width: 0,
height: 0,
visible: false,
viewportWidth: 0,
viewportHeight: 0,
};
private unsubscribers: Array<() => void> = [];
private disposingView = false;
Expand Down Expand Up @@ -150,6 +153,7 @@ export class DesktopPreviewController {
const view = new WebContentsView({
webPreferences: PREVIEW_WEB_PREFERENCES,
});
view.setBorderRadius(8);
this.view = view;
this.window.contentView.addChildView(view);
this.bindView(view);
Expand Down Expand Up @@ -280,30 +284,9 @@ export class DesktopPreviewController {
return false;
}

const contentBounds = this.window.getContentBounds();
const rawWidth = Math.round(this.bounds.width);
const rawHeight = Math.round(this.bounds.height);

if (
!this.bounds.visible ||
rawWidth <= 0 ||
rawHeight <= 0 ||
contentBounds.width <= 0 ||
contentBounds.height <= 0
) {
this.view.setBounds({ x: 0, y: 0, width: 0, height: 0 });
return false;
}

const width = Math.min(rawWidth, contentBounds.width);
const height = Math.min(rawHeight, contentBounds.height);
const maxX = Math.max(0, contentBounds.width - width);
const maxY = Math.max(0, contentBounds.height - height);
const x = Math.max(0, Math.min(Math.round(this.bounds.x), maxX));
const y = Math.max(0, Math.min(Math.round(this.bounds.y), maxY));

this.view.setBounds({ x, y, width, height });
return width > 0 && height > 0;
const nextBounds = projectPreviewBoundsToContent(this.bounds, this.window.getContentBounds());
this.view.setBounds(nextBounds);
return nextBounds.width > 0 && nextBounds.height > 0;
}

private disposeView(): void {
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/PreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const HIDDEN_PREVIEW_BOUNDS = {
width: 0,
height: 0,
visible: false,
viewportWidth: 0,
viewportHeight: 0,
} as const;

export function resolvePreviewStatusCopy(state: DesktopPreviewState): string {
Expand Down Expand Up @@ -147,6 +149,8 @@ export function PreviewPanel({ threadId, projectId, projectName, onClose }: Prev
width: rect.width,
height: rect.height,
visible,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
};
};

Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export interface DesktopPreviewBounds {
width: number;
height: number;
visible: boolean;
viewportWidth: number;
viewportHeight: number;
}

export interface DesktopPreviewState {
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ describe("sanitizeLocalPreviewBounds", () => {
width: 480.4,
height: 320.6,
visible: true,
viewportWidth: 1440.4,
viewportHeight: 900.6,
};
expect(sanitizeLocalPreviewBounds(floatingBounds)).toEqual({
x: 10,
y: 21,
width: 480,
height: 321,
visible: true,
viewportWidth: 1440,
viewportHeight: 901,
});

expect(
Expand All @@ -71,13 +75,17 @@ describe("sanitizeLocalPreviewBounds", () => {
width: -10,
height: 0,
visible: true,
viewportWidth: Number.NaN,
viewportHeight: Number.NaN,
}),
).toEqual({
x: 0,
y: 0,
width: 0,
height: 0,
visible: false,
viewportWidth: 0,
viewportHeight: 0,
});
});
});
8 changes: 8 additions & 0 deletions packages/shared/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,20 @@ export function validateHttpPreviewUrl(
export function sanitizeLocalPreviewBounds(bounds: DesktopPreviewBounds): DesktopPreviewBounds {
const width = Number.isFinite(bounds.width) ? Math.max(0, Math.round(bounds.width)) : 0;
const height = Number.isFinite(bounds.height) ? Math.max(0, Math.round(bounds.height)) : 0;
const viewportWidth = Number.isFinite(bounds.viewportWidth)
? Math.max(0, Math.round(bounds.viewportWidth))
: 0;
const viewportHeight = Number.isFinite(bounds.viewportHeight)
? Math.max(0, Math.round(bounds.viewportHeight))
: 0;

return {
x: Number.isFinite(bounds.x) ? Math.round(bounds.x) : 0,
y: Number.isFinite(bounds.y) ? Math.round(bounds.y) : 0,
width,
height,
viewportWidth,
viewportHeight,
visible: bounds.visible && width > 0 && height > 0,
};
}
Loading