diff --git a/native/xp-card-games/resize-host.c b/native/xp-card-games/resize-host.c new file mode 100644 index 00000000..fc5db9ff --- /dev/null +++ b/native/xp-card-games/resize-host.c @@ -0,0 +1,96 @@ +#define WIN32_LEAN_AND_MEAN +#include + +#define SIZE_FILE L"D:\\solitaire-size.txt" + +#if defined(RESIZE_FREECELL) +#define TARGET_EXECUTABLE L"freecell.exe" +#define TARGET_TITLE L"FreeCell" +#define NATIVE_SCREEN_WIDTH 640 +#define NATIVE_SCREEN_HEIGHT 480 +#elif defined(RESIZE_SPIDER_SOLITAIRE) +#define TARGET_EXECUTABLE L"spider.exe" +#define TARGET_TITLE L"Spider Solitaire" +#define NATIVE_SCREEN_WIDTH 794 +#define NATIVE_SCREEN_HEIGHT 601 +#else +#error A supported card game must be selected. +#endif + +static BOOL parse_dimension(char **cursor, char *end, unsigned int *value) { + unsigned int result = 0; + while (*cursor < end && (**cursor == ' ' || **cursor == '\r' || + **cursor == '\n' || **cursor == '\t')) + ++*cursor; + if (*cursor == end || **cursor < '0' || **cursor > '9') return FALSE; + while (*cursor < end && **cursor >= '0' && **cursor <= '9') { + result = result * 10 + (unsigned int)(**cursor - '0'); + ++*cursor; + } + *value = result; + return TRUE; +} + +static BOOL read_window_size(unsigned int *width, unsigned int *height) { + char buffer[64]; + DWORD bytes_read = 0; + HANDLE file = CreateFileW(SIZE_FILE, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) return FALSE; + BOOL read = ReadFile(file, buffer, sizeof(buffer), &bytes_read, NULL); + CloseHandle(file); + char *cursor = buffer; + char *end = buffer + bytes_read; + return read && parse_dimension(&cursor, end, width) && + parse_dimension(&cursor, end, height) && *width >= 100 && + *height >= 100; +} + +static DWORD run(void) { + STARTUPINFOW startup = {0}; + PROCESS_INFORMATION process = {0}; + WCHAR application_path[MAX_PATH]; + unsigned int applied_width = 0; + unsigned int applied_height = 0; + int width_adjustment = 0; + int height_adjustment = 0; + BOOL calibrated = FALSE; + + startup.cb = sizeof(startup); + DWORD path_length = GetModuleFileNameW(NULL, application_path, MAX_PATH); + if (!path_length || path_length == MAX_PATH) return 1; + WCHAR *filename = application_path + path_length; + while (filename > application_path && filename[-1] != L'\\') --filename; + if (filename == application_path) return 1; + lstrcpyW(filename, TARGET_EXECUTABLE); + if (!CreateProcessW(application_path, NULL, NULL, NULL, FALSE, 0, NULL, NULL, + &startup, &process)) + return 1; + + CloseHandle(process.hThread); + while (WaitForSingleObject(process.hProcess, 50) == WAIT_TIMEOUT) { + unsigned int width; + unsigned int height; + HWND application = FindWindowW(NULL, TARGET_TITLE); + if (!application || !read_window_size(&width, &height)) continue; + if (width == applied_width && height == applied_height) continue; + if (!calibrated) { + RECT bounds; + if (!GetWindowRect(application, &bounds)) continue; + width_adjustment = bounds.right - bounds.left - NATIVE_SCREEN_WIDTH; + height_adjustment = bounds.bottom - bounds.top - NATIVE_SCREEN_HEIGHT; + calibrated = TRUE; + } + if (SetWindowPos(application, NULL, 0, 0, (int)width + width_adjustment, + (int)height + height_adjustment, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE)) { + applied_width = width; + applied_height = height; + } + } + CloseHandle(process.hProcess); + return 0; +} + +void WinMainCRTStartup(void) { ExitProcess(run()); } diff --git a/site/apps/catalog/accessories.js b/site/apps/catalog/accessories.js index c0efc088..beabf173 100644 --- a/site/apps/catalog/accessories.js +++ b/site/apps/catalog/accessories.js @@ -7,7 +7,7 @@ export const accessoryApplications = [ program("__calculator", "Calculator", "Calculator.png", "calculator", { window: { width: 260, - height: 259, + height: 260, resizable: false, maximizable: false, className: "xp-calculator-window", @@ -24,8 +24,8 @@ export const accessoryApplications = [ height: 338, left: 24, top: 30, - resizable: false, - maximizable: false, + resizable: true, + maximizable: true, className: "xp-command-prompt-window", }, }, diff --git a/site/apps/programs/renderers/terminal.js b/site/apps/programs/renderers/terminal.js index e0630eaa..af238e98 100644 --- a/site/apps/programs/renderers/terminal.js +++ b/site/apps/programs/renderers/terminal.js @@ -49,6 +49,10 @@ export const renderTerminal = (context, program, programId) => { screen.scrollTop = screen.scrollHeight; }); screen.addEventListener("pointerdown", () => input.focus()); + context.windowElement.addEventListener("pointerdown", (event) => { + if (event.target.closest(".title-buttons, .resize-handle")) return; + setTimeout(() => input.focus(), 0); + }); setTimeout(() => input.focus(), 0); return content; }; diff --git a/site/apps/xp-card-games/index.js b/site/apps/xp-card-games/index.js index 2733f9f4..6f812f78 100644 --- a/site/apps/xp-card-games/index.js +++ b/site/apps/xp-card-games/index.js @@ -6,7 +6,6 @@ const defineXpCardGame = ({ title, icon, archive, - executable, width, height, nativeWidth = width - 6, @@ -23,20 +22,19 @@ const defineXpCardGame = ({ width, height, fitToWorkArea: true, - resizable: false, - maximizable: false, + resizable: true, + maximizable: true, }, mount: () => mountBoxedWineApplication({ title: `Windows XP ${title}`, packageId: id, archive, - executable, + executable: "resize-host.exe", nativeWidth, nativeHeight, frameTop: 32, background: "#27811f", - scaleOnly: true, }), }); @@ -46,7 +44,6 @@ export const xpCardGameApplications = [ title: "FreeCell", icon: "FreeCell.png", archive: "xp-freecell", - executable: "freecell.exe", width: 646, height: 479, nativeWidth: 640, @@ -57,7 +54,6 @@ export const xpCardGameApplications = [ title: "Spider Solitaire", icon: "SpiderSolitaire.png", archive: "xp-spider-solitaire", - executable: "spider.exe", width: 800, height: 600, }), diff --git a/site/css/shell/themes.css b/site/css/shell/themes.css index abc009e6..9ee27511 100644 --- a/site/css/shell/themes.css +++ b/site/css/shell/themes.css @@ -691,7 +691,7 @@ html[data-xp-appearance="classic"] .start-program-flyout button:focus-visible { .xp-calculator-window { min-width: min(260px, calc(100% - 8px)); - min-height: min(259px, calc(100% - 8px)); + min-height: min(260px, calc(100% - 8px)); } .xp-native-calculator { @@ -714,7 +714,7 @@ html[data-xp-appearance="classic"] .start-program-flyout button:focus-visible { z-index: 3; display: flex; height: 21px; - padding-left: 1px; + padding-left: 0; background: #ece9d8; } @@ -723,13 +723,23 @@ html[data-xp-appearance="classic"] .start-program-flyout button:focus-visible { } .xp-calculator-menu-trigger { + box-sizing: border-box; height: 21px; - padding: 1px 6px 2px; + padding: 1px 3px 2px; border: 1px solid transparent; background: transparent; color: #000; } +.xp-calculator-menu-group:nth-child(1) .xp-calculator-menu-trigger { + width: 30px; +} + +.xp-calculator-menu-group:nth-child(2) .xp-calculator-menu-trigger, +.xp-calculator-menu-group:nth-child(3) .xp-calculator-menu-trigger { + width: 34px; +} + .xp-calculator-menu-trigger:hover, .xp-calculator-menu-trigger[aria-expanded="true"] { border-color: #316ac5; @@ -786,14 +796,19 @@ html[data-xp-appearance="classic"] .start-program-flyout button:focus-visible { } .xp-calculator-body { - padding: 3px 6px 6px; + position: relative; + height: 209px; + padding: 0; } .xp-calculator-display { box-sizing: border-box; - width: 100%; + position: absolute; + top: 0; + left: 7px; + width: 239px; height: 23px; - margin: 0 0 13px; + margin: 0; padding: 2px 4px; border: 1px solid #7f9db9; border-radius: 0; @@ -806,26 +821,46 @@ html[data-xp-appearance="classic"] .start-program-flyout button:focus-visible { } .xp-calculator-clear-row { - display: grid; - grid-template-columns: 34px 61px 60px 60px; - gap: 5px; - margin-bottom: 6px; + position: absolute; + top: 36px; + left: 11px; + width: 234px; + height: 28px; } .xp-calculator-clear-row .xp-calculator-memory-indicator { box-sizing: border-box; width: 26px; - height: 26px; - margin-left: 4px; + height: 27px; + margin: 1px 0 0; border: 1px solid; border-color: #7f7f7f #fff #fff #7f7f7f; - color: #d00000; + color: #f00; text-align: center; font: 11px/24px Tahoma, sans-serif; } +.xp-calculator-clear-row button { + position: absolute; + top: 0; + width: 61px; + height: 28px; +} + +.xp-calculator-clear-row button:nth-of-type(1) { + left: 43px; +} + +.xp-calculator-clear-row button:nth-of-type(2) { + left: 108px; +} + +.xp-calculator-clear-row button:nth-of-type(3) { + left: 173px; +} + .xp-calculator-clear-row button, .xp-calculator-keys button { box-sizing: border-box; @@ -833,10 +868,8 @@ html[data-xp-appearance="classic"] .start-program-flyout button:focus-visible { padding: 1px 2px; border: 1px solid #003c74; border-radius: 3px; - background: #ece9d8; - box-shadow: - inset 1px 1px #fff, - inset -1px -1px #aca899; + background: linear-gradient(180deg, #fff, #f7f6f1 8%, #ece9d8 88%, #d6d0c4); + box-shadow: inset 1px 1px #fff; } .xp-calculator-clear-row button:active, @@ -847,27 +880,34 @@ html[data-xp-appearance="classic"] .start-program-flyout button:focus-visible { } .xp-calculator-clear-row button { - color: #d00000; + color: #f00; } .xp-calculator-keys { + position: absolute; + top: 72px; + left: 9px; display: grid; - grid-template-columns: repeat(6, 34px); - gap: 6px 5px; + grid-template-columns: 39px repeat(5, 34px); + gap: 6px; +} + +.xp-calculator-keys button { + width: 34px; } .xp-calculator-keys button[data-key-type="number"] { - color: #0000d0; + color: #00f; } .xp-calculator-keys button[data-key-type="operator"], .xp-calculator-keys button[data-key-type="memory"] { - color: #d00000; + color: #f00; } .xp-command-prompt-window { - min-width: min(668px, calc(100% - 8px)); - min-height: min(338px, calc(100% - 8px)); + min-width: min(320px, calc(100% - 8px)); + min-height: min(180px, calc(100% - 8px)); } .xp-command-prompt-window .window-content, diff --git a/site/iframe/freecell/SOURCES.json b/site/iframe/freecell/SOURCES.json index 05254ae6..677e55bf 100644 --- a/site/iframe/freecell/SOURCES.json +++ b/site/iframe/freecell/SOURCES.json @@ -5,8 +5,8 @@ "sourceSha256": "fd8c8d42c1581e8767217fe800bfc0d5649c0ad20d754c927d6c763e446d1927", "package": { "file": "xp-freecell.zip", - "bytes": 177107, - "sha256": "a450542c141eab60dec0ff0611ef2c4ab6a1e2a03c090a81d3769d5ad92f1348" + "bytes": 178462, + "sha256": "048b03721fd9205387274e24c095a21344b1cf97cb3b03d08dd496372a9ee6a7" }, "files": { "freecell.exe": { @@ -16,6 +16,11 @@ "cards.dll": { "member": "I386/CARDS.DL_", "sha256": "0fb81e48d8b45e9995ae1e05fd28c1631dbbcdc71180904c1356bfa2c1ead506" + }, + "resize-host.exe": { + "source": "native/xp-card-games/resize-host.c", + "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows -DRESIZE_FREECELL native/xp-card-games/resize-host.c -lkernel32 -luser32 -o resize-host.exe", + "sha256": "ad94b439cc8bc9e18e00ee9332f558c347773f66e97dcefb2548346b9d6f4426" } } } diff --git a/site/iframe/freecell/xp-freecell.zip b/site/iframe/freecell/xp-freecell.zip index 6ccb12f4..541ea160 100644 Binary files a/site/iframe/freecell/xp-freecell.zip and b/site/iframe/freecell/xp-freecell.zip differ diff --git a/site/iframe/spider-solitaire/SOURCES.json b/site/iframe/spider-solitaire/SOURCES.json index c9e44b97..b6fd36e5 100644 --- a/site/iframe/spider-solitaire/SOURCES.json +++ b/site/iframe/spider-solitaire/SOURCES.json @@ -5,13 +5,18 @@ "sourceSha256": "fd8c8d42c1581e8767217fe800bfc0d5649c0ad20d754c927d6c763e446d1927", "package": { "file": "xp-spider-solitaire.zip", - "bytes": 270263, - "sha256": "ba3b5323193d91fcf5d32a2985e5e162881452228f9664b69f597b18de7dd70a" + "bytes": 271617, + "sha256": "950bc7a733a96cbc4cdc222e394f4aea8fe710c343a2857bf8b5460422ce5112" }, "files": { "spider.exe": { "member": "I386/SPIDER.EX_", "sha256": "d309a839843ccd8b9edef7a271a611eb7794f83a825b69a87cc259c9ede87288" + }, + "resize-host.exe": { + "source": "native/xp-card-games/resize-host.c", + "build": "i686-w64-mingw32-gcc -Os -s -nostdlib -mwindows -DRESIZE_SPIDER_SOLITAIRE native/xp-card-games/resize-host.c -lkernel32 -luser32 -o resize-host.exe", + "sha256": "a54053947fc3cfda0ded2832b307d3ae075921b19efd4afefefd54006d84f3a5" } } } diff --git a/site/iframe/spider-solitaire/xp-spider-solitaire.zip b/site/iframe/spider-solitaire/xp-spider-solitaire.zip index c9c7b00b..a760ee39 100644 Binary files a/site/iframe/spider-solitaire/xp-spider-solitaire.zip and b/site/iframe/spider-solitaire/xp-spider-solitaire.zip differ diff --git a/site/js/apps/programs.js b/site/js/apps/programs.js index 696b68eb..6be94193 100644 --- a/site/js/apps/programs.js +++ b/site/js/apps/programs.js @@ -18,6 +18,7 @@ const wireSystemWindowControls = (win) => { }; const applicationContext = (win) => ({ + windowElement: win.el, XP_ICON_PATHS, dialogs: XPDialogs, fileOps, diff --git a/tests/calculator.test.ts b/tests/calculator.test.ts index 3996a34d..2a56efac 100644 --- a/tests/calculator.test.ts +++ b/tests/calculator.test.ts @@ -30,7 +30,7 @@ describe("Windows XP Calculator", () => { const { calculator, display } = await launchCalculator(); expect(calculator.style.width).toBe("260px"); - expect(calculator.style.height).toBe("259px"); + expect(calculator.style.height).toBe("260px"); expect(calculator.querySelector(".maximize-btn").disabled).toBeTrue(); expect(display.value).toBe("0."); expect( diff --git a/tests/shell-behavior.test.ts b/tests/shell-behavior.test.ts index 2a8685dd..71a1c683 100644 --- a/tests/shell-behavior.test.ts +++ b/tests/shell-behavior.test.ts @@ -662,8 +662,8 @@ test("Command Prompt uses the XP console layout and operates on the shared files expect(commandWindow.style.top).toBe("30px"); expect( commandWindow.querySelector(".maximize-btn")!.disabled, - ).toBeTrue(); - expect(commandWindow.querySelector(".resize-handle")).toBeNull(); + ).toBeFalse(); + expect(commandWindow.querySelector(".resize-handle")).not.toBeNull(); expect(commandWindow.querySelector(".title-text")!.textContent).toBe( "C:\\WINDOWS\\system32\\cmd.exe", ); @@ -672,6 +672,18 @@ test("Command Prompt uses the XP console layout and operates on the shared files ); expect(prompt.textContent).toBe("C:\\Documents and Settings\\Administrator>"); + commandWindow + .querySelector(".title-bar")! + .dispatchEvent( + new shell.window.PointerEvent("pointerdown", { bubbles: true }), + ); + await flushShell(); + expect(shell.document.activeElement).toBe(input); + + commandWindow.querySelector(".maximize-btn")!.click(); + expect(commandWindow.classList.contains("maximized")).toBeTrue(); + commandWindow.querySelector(".maximize-btn")!.click(); + run('cd "My Documents"'); expect(prompt.textContent).toBe( "C:\\Documents and Settings\\Administrator\\My Documents>", diff --git a/tests/xp-card-games.test.ts b/tests/xp-card-games.test.ts index 0b00fd68..6893d87d 100644 --- a/tests/xp-card-games.test.ts +++ b/tests/xp-card-games.test.ts @@ -25,16 +25,16 @@ const games = [ applicationId: "__freecell", title: "FreeCell", archive: "xp-freecell", - executable: "freecell.exe", - files: ["cards.dll", "freecell.exe"], + executable: "resize-host.exe", + files: ["cards.dll", "freecell.exe", "resize-host.exe"], }, { id: "spider-solitaire", applicationId: "__spider-solitaire", title: "Spider Solitaire", archive: "xp-spider-solitaire", - executable: "spider.exe", - files: ["spider.exe"], + executable: "resize-host.exe", + files: ["resize-host.exe", "spider.exe"], }, ]; @@ -73,8 +73,8 @@ describe("Windows XP card games through BoxedWine", () => { expect(url.searchParams.get("executable")).toBe(game.executable); expect(url.searchParams.get("frameTop")).toBe("32"); expect(url.searchParams.get("sound")).toBe("true"); - expect(gameWindow.querySelector(".maximize-btn").disabled).toBeTrue(); - expect(gameWindow.querySelector(".resize-handle")).toBeNull(); + expect(gameWindow.querySelector(".maximize-btn").disabled).toBeFalse(); + expect(gameWindow.querySelector(".resize-handle")).not.toBeNull(); expect(shell.window.location.hash).toBe(`#${game.id}`); await flushShell(); expect(shell.offlineDownloads).toEqual([game.id]); @@ -131,6 +131,22 @@ describe("Windows XP card games through BoxedWine", () => { shell.window.dispatchEvent(new shell.window.Event("resize")); expect(gameWindow.style.minWidth).not.toBe("0px"); expect(gameWindow.style.minHeight).not.toBe("0px"); + + gameWindow.querySelector(".maximize-btn").click(); + expect(gameWindow.classList.contains("maximized")).toBeTrue(); + Object.defineProperties(host, { + clientWidth: { configurable: true, value: 1018 }, + clientHeight: { configurable: true, value: 707 }, + }); + resize(); + expect(frame.style.width).toBe("100%"); + expect(frame.style.height).toBe("100%"); + expect(frame.style.transform).toBe(""); + expect(resizeMessages.at(-1).message).toEqual({ + type: "boxedwine-framebuffer-resize", + width: 1018, + height: 739, + }); }); test(`${game.title} package matches its provenance manifest`, async () => {