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
82 changes: 82 additions & 0 deletions native/calculator/window-host.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#define SIZE_FILE L"D:\\boxedwine-window-size.txt"

typedef struct {
DWORD process_id;
HWND window;
} WindowSearch;

static BOOL CALLBACK find_process_window(HWND window, LPARAM parameter) {
WindowSearch *search = (WindowSearch *)parameter;
DWORD process_id = 0;
GetWindowThreadProcessId(window, &process_id);
if (process_id == search->process_id && IsWindowVisible(window) &&
!GetWindow(window, GW_OWNER)) {
search->window = window;
return FALSE;
}
return TRUE;
}

static void append_number(char **cursor, unsigned int value) {
char digits[16];
unsigned int length = 0;
do {
digits[length++] = (char)('0' + value % 10);
value /= 10;
} while (value);
while (length) *(*cursor)++ = digits[--length];
}

static void write_window_size(unsigned int width, unsigned int height) {
char buffer[40];
char *cursor = buffer;
append_number(&cursor, width);
*cursor++ = ' ';
append_number(&cursor, height);
HANDLE file = CreateFileW(SIZE_FILE, GENERIC_WRITE, FILE_SHARE_READ, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) return;
DWORD written;
WriteFile(file, buffer, (DWORD)(cursor - buffer), &written, NULL);
CloseHandle(file);
}

static DWORD run(void) {
STARTUPINFOW startup = {0};
PROCESS_INFORMATION process = {0};
WCHAR calculator_path[MAX_PATH];
unsigned int previous_width = 0;
unsigned int previous_height = 0;

startup.cb = sizeof(startup);
DWORD path_length = GetModuleFileNameW(NULL, calculator_path, MAX_PATH);
if (!path_length || path_length == MAX_PATH) return 1;
WCHAR *filename = calculator_path + path_length;
while (filename > calculator_path && filename[-1] != L'\\') --filename;
if (filename == calculator_path) return 1;
lstrcpyW(filename, L"calc.exe");
if (!CreateProcessW(calculator_path, NULL, NULL, NULL, FALSE, 0, NULL, NULL,
&startup, &process))
return 1;

CloseHandle(process.hThread);
while (WaitForSingleObject(process.hProcess, 50) == WAIT_TIMEOUT) {
WindowSearch search = {process.dwProcessId, NULL};
RECT bounds;
EnumWindows(find_process_window, (LPARAM)&search);
if (!search.window || !GetWindowRect(search.window, &bounds)) continue;
unsigned int width = (unsigned int)(bounds.right - bounds.left);
unsigned int height = (unsigned int)(bounds.bottom - bounds.top);
if (width == previous_width && height == previous_height) continue;
write_window_size(width, height);
previous_width = width;
previous_height = height;
}
CloseHandle(process.hProcess);
return 0;
}

void WinMainCRTStartup(void) { ExitProcess(run()); }
36 changes: 36 additions & 0 deletions site/apps/calculator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defineApplication } from "../core/application.js";
import { mountBoxedWineApplication } from "../core/boxedwine.js";

export const calculatorApplication = defineApplication({
id: "__calculator",
title: "Calculator",
icon: "Calculator.png",
kind: "native-game",
window: {
width: 260,
height: 260,
resizable: false,
maximizable: false,
className: "xp-calculator-window",
},
mount: (context) =>
mountBoxedWineApplication({
title: "Windows XP Calculator",
packageId: "calculator",
archive: "xp-calculator",
executable: "window-host.exe",
nativeWidth: 254,
nativeHeight: 229,
frameTop: 32,
nativeFrameWidth: 6,
nativeFrameHeight: 31,
background: "#ece9d8",
onWindowSize(width, height) {
const workArea = context.getDesktopSize();
context.setSize(
workArea.width > 0 ? Math.min(width, workArea.width) : width,
workArea.height > 0 ? Math.min(height, workArea.height) : height,
);
},
}),
});
9 changes: 0 additions & 9 deletions site/apps/catalog/accessories.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ const program = (id, title, icon, kind, extra = {}) =>
defineProgram({ id, title, icon, kind, ...extra });

export const accessoryApplications = [
program("__calculator", "Calculator", "Calculator.png", "calculator", {
window: {
width: 260,
height: 260,
resizable: false,
maximizable: false,
className: "xp-calculator-window",
},
}),
program(
"__command-prompt",
"Command Prompt",
Expand Down
37 changes: 36 additions & 1 deletion site/apps/core/boxedwine-resize.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ const isResizeMessage = (event, origin) => {
export const installBoxedWineResizeBridge = (hostWindow, module) => {
let pendingResize = null;
let runtimeInitialized = false;
let windowSizeTimer = null;
let previousWindowSize = "";

const reportWindowSize = () => {
if (!runtimeInitialized || typeof module.FS?.readFile !== "function")
return;
try {
const text = module.FS.readFile("/d_drive/boxedwine-window-size.txt", {
encoding: "utf8",
}).trim();
if (text === previousWindowSize) return;
const match = /^(\d+) (\d+)$/.exec(text);
if (!match) return;
previousWindowSize = text;
hostWindow.parent.postMessage(
{
type: "boxedwine-window-size",
width: Number(match[1]),
height: Number(match[2]),
},
hostWindow.location.origin,
);
} catch {
// Most applications do not publish their native window size.
}
};

const applyResize = () => {
if (
Expand Down Expand Up @@ -53,8 +79,17 @@ export const installBoxedWineResizeBridge = (hostWindow, module) => {
onRuntimeInitialized?.();
runtimeInitialized = true;
applyResize();
if (
typeof module.FS?.readFile === "function" &&
typeof hostWindow.setInterval === "function"
) {
windowSizeTimer = hostWindow.setInterval(reportWindowSize, 100);
}
};
hostWindow.addEventListener("message", onMessage);

return () => hostWindow.removeEventListener("message", onMessage);
return () => {
hostWindow.removeEventListener("message", onMessage);
if (windowSizeTimer !== null) hostWindow.clearInterval(windowSizeTimer);
};
};
51 changes: 40 additions & 11 deletions site/apps/core/boxedwine.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export const mountBoxedWineApplication = ({
sound = true,
background = "#000",
scaleOnly = false,
onWindowSize,
nativeFrameWidth = 0,
nativeFrameHeight = 0,
}) => {
let activeNativeWidth = nativeWidth;
let activeNativeHeight = nativeHeight;
const host = document.createElement("div");
host.className = "window-content boxedwine-app-host";
host.style.background = background;
Expand Down Expand Up @@ -80,26 +85,29 @@ export const mountBoxedWineApplication = ({
host.appendChild(frame);

const updateLayout = () => {
const hostSize = getHostSize(host, nativeWidth, nativeHeight);
const hostSize = getHostSize(host, activeNativeWidth, activeNativeHeight);
const shouldScale =
scaleOnly ||
hostSize.width < nativeWidth ||
hostSize.height < nativeHeight;
hostSize.width < activeNativeWidth ||
hostSize.height < activeNativeHeight;
const scale = shouldScale
? Math.min(
1,
hostSize.width / nativeWidth,
hostSize.height / nativeHeight,
hostSize.width / activeNativeWidth,
hostSize.height / activeNativeHeight,
)
: 1;
const framebuffer = shouldScale
? { width: nativeWidth, height: nativeHeight + frameTop }
? {
width: activeNativeWidth,
height: activeNativeHeight + frameTop,
}
: { width: hostSize.width, height: hostSize.height + frameTop };

if (shouldScale) {
frame.style.width = `${nativeWidth}px`;
frame.style.height = `${nativeHeight}px`;
frame.style.left = `${Math.max(0, (hostSize.width - nativeWidth * scale) / 2)}px`;
frame.style.width = `${activeNativeWidth}px`;
frame.style.height = `${activeNativeHeight}px`;
frame.style.left = `${Math.max(0, (hostSize.width - activeNativeWidth * scale) / 2)}px`;
frame.style.transform = `scale(${scale})`;
frame.style.transformOrigin = "top left";
} else {
Expand All @@ -113,8 +121,8 @@ export const mountBoxedWineApplication = ({
{
type: "boxedwine-framebuffer-resize",
...framebuffer,
baseWidth: nativeWidth,
baseHeight: nativeHeight + frameTop,
baseWidth: activeNativeWidth,
baseHeight: activeNativeHeight + frameTop,
},
new URL(frame.src).origin,
);
Expand All @@ -124,12 +132,33 @@ export const mountBoxedWineApplication = ({
: null;
resizeObserver?.observe(host);
frame.addEventListener("load", updateLayout);
const handleWindowMessage = (event) => {
const { type, width, height } = event.data || {};
if (
event.source !== frame.contentWindow ||
event.origin !== new URL(frame.src).origin ||
type !== "boxedwine-window-size" ||
!Number.isInteger(width) ||
!Number.isInteger(height) ||
width < 100 ||
height < 100
)
return;
if (nativeFrameWidth || nativeFrameHeight) {
activeNativeWidth = Math.max(1, width - nativeFrameWidth);
activeNativeHeight = Math.max(1, height - nativeFrameHeight);
}
onWindowSize?.(width, height);
updateLayout();
};
window.addEventListener("message", handleWindowMessage);
updateLayout();

return {
element: host,
unmount() {
resizeObserver?.disconnect();
window.removeEventListener("message", handleWindowMessage);
frame.removeEventListener("load", updateLayout);
frame.src = "about:blank";
frame.remove();
Expand Down
2 changes: 2 additions & 0 deletions site/apps/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { minesweeperApplication } from "./minesweeper/index.js";
import { solitaireApplication } from "./solitaire/index.js";
import { pinballApplication } from "./pinball/index.js";
import { xpCardGameApplications } from "./xp-card-games/index.js";
import { calculatorApplication } from "./calculator/index.js";

export const applicationRegistry = createApplicationRegistry([
...accessoryApplications,
...systemToolApplications,
...systemApplications,
calculatorApplication,
minesweeperApplication,
notepadApplication,
paintApplication,
Expand Down
1 change: 0 additions & 1 deletion site/apps/programs/define-program.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { defineApplication } from "../core/application.js";
import * as renderers from "./renderers/index.js";

const PROGRAM_RENDERERS = Object.freeze({
calculator: renderers.renderCalculator,
terminal: renderers.renderTerminal,
volume: renderers.renderVolume,
});
Expand Down
Loading