From 4077048a8c7222fbbc237506fcdd7e0d1dee9a40 Mon Sep 17 00:00:00 2001 From: astrovm <~@4st.li> Date: Thu, 13 Aug 2026 01:49:58 -0300 Subject: [PATCH] Fix XP application window behavior --- native/xp-card-games/resize-host.c | 96 ++++++++++++++++++ site/apps/catalog/accessories.js | 6 +- site/apps/programs/renderers/terminal.js | 4 + site/apps/xp-card-games/index.js | 10 +- site/css/shell/themes.css | 88 +++++++++++----- site/iframe/freecell/SOURCES.json | 9 +- site/iframe/freecell/xp-freecell.zip | Bin 177107 -> 178462 bytes site/iframe/spider-solitaire/SOURCES.json | 9 +- .../spider-solitaire/xp-spider-solitaire.zip | Bin 270263 -> 271617 bytes site/js/apps/programs.js | 1 + tests/calculator.test.ts | 2 +- tests/shell-behavior.test.ts | 16 ++- tests/xp-card-games.test.ts | 28 +++-- 13 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 native/xp-card-games/resize-host.c 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 6ccb12f489b044140775ec37c4836e570c6df269..541ea1601fbedcaf5f77a83d456fa6fb4fe7515a 100644 GIT binary patch delta 1403 zcmV->1%&$3=L(+k3V?(Gv;t1Ee****000080CWuvU4tI+oZ$oj000gE01p5F0CHt> zX?kTXXm4|LE@gOS?N(n*TU8kUO0Tojjy9n@7!~~Kfmw$&OP_uHXa!QJpgcsd>&u~RB^%IOA1l6`*IO{T6ks8 zh_2zvp0=(~obQQ6PsIG)e83-xL?!;1$j1^9J`~~I&0fAc8Win2cN%RPM@ub0gN}n! zhhICn!aD;+`iFD{PqCtQf2g_-i*c~5c(~Mgfm*jtt6wEl7s(%~*@Y%vi?gi8uhLL) z09@2<(j=xTzmL86v-BYE{!Q$5NlZ$3{amjV#I0{a6&~QIJr?v!et>>WL3;+nh7w)& zkm|>JacF+VzwnER;b=hX$))+3HuxR9pB}g6|1a(2hL3zRT8s~Ke+T*2>m_3uedVpV zbwj| zIW=iCf6=&z?n;~S>il)GYlI%iBQL!4@{42bLT;2EbxhfzWIz5bpZ6-m3wU{bVF8cf zxcmXm-RQ9Rk&=BLf9IndTL`&vx6QyI5$UvXgtO&KQVtiS7ffGs^F_alpGk$TNyJkV27ZnvM?Qw%HVx9%nr1x0NAE z5mtl+VL})X94W)lB+Fio+275`ofsv2C){DAJ!okq!*0qne_YMRXPKQQcVZyvvFOJw%*%uZw%2FN+{0DYMWIU={HWtK8`YYJFr7Wy7Rt%+`x z9<*-p9X-z3opwXIU%KX+>#Z3e_;%M;D9| zjh&e+jY;PNy{#GMm%bP1t*cy47%C^s1J{r{ouwNLi7rKEC9|@ya4y3(i|yXqSeKmh zc+{GSOd}G7zOnGxWsC^e*Ctls?R&s#o*r zz7oF1oYo8rBn>u`?k_!-+q*J3mtn1%-4kZ_vRW9KAj@;pEdT29U>hss z6!91J;UPC#W-tG$8T-*V^lv=>#~}tH{sM?F{_J>7A^_?%OO0j$a6=VzLL8#F3V{TE z9I4Mff7FHG6`a{on*{n@tFNi4;>+us+V}FpGj^w)ev|%$I{E=W-gejD z13rI9s*A?Fp~#7_*nI4y7?7Z9M>sCU0zIeE`w?ATC=&D`qJ2KNtwTn2;h~?+D)u z7B_{x&gY!dPO;)b#ifcZs%BfZSpEQ)OtS)WQ2_!T0~7!N00;nd4Gmp`9`T&v1ONa4 z4gdfT00000000000002(flRXk0CHt>X?kTXXm4|LE@gOSP)h{{000000{{a6t^fc4 JTDSrL007eRm!tpy delta 39 pcmbRDiRX?kTXXm4|LE@gOS?N(n*TU8kU+Foa=-P(k*p~37f=>yZO_LgY|VM!eYmW)58 z1*#KuU3(p!rES_<=yWD558Hz?e=tbg z#4H=A^*iSlXokVX7>o&D@|}Cm_viQhzH`t0(|mLobO6Ai=JNofpo$azUiwjY?7p-E zJ}uP~tl#d0^ny{&NxKhHhg7b~ zX{M{k6UJB4sXw};()6r${#NyL(!lD^eL?YERt|V42zdtaCPFAOEz{XaTvn^X%HxbD zr>zyJ<){`^GpZ3)kIIn>Oii)sm011#jNFA;!Vkh71{y<88yRs?f25IGHXdMpTHJw5 zGOQwt9mH6V=b#n|nq(akWPNfwks|Y-XMQ3(F+t9m2I$L7$PwAC(ky4;mVRKJndy53 ztwy?4Iw*9D@8}86dH@mgWfCJ@LB3Ak&6@@m(I?~;70hU*Z7SJr6`5f;DIRTDv7mLyFGXIU%KDMe{w2Gud! zM;8ndon4tM%}M72y=@s5m%bP1ZKz&O>Z>PB16L6{ouwNL$!4~&SltUvE6$I zdC57CN99apnvpCax5>4+v+O>^(C{S7-}e}97?J6X%E%Hke}}30c`Y^5yNDZ7zLRCu zuGZ6hBYcZBEg6uvSg3Nt0_?B}OL6^4tuozj{2_ z#tJz_{8fE;$PM@GrQZt9)94)jH=h3l#2^Gg5FrjW98;(O-XbfHt0;tVErB1$IE!#W z4Rm4l{hobje+$8DIJ2QO1@s%&SXWoWS2nhE?B#`LYz`a!JmHbbL(z3h{u`;augxVv z{ckx|WE)1?dHec2Dj%Z#Gt?aF6zX- { }; 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 () => {