From 4a09a957cfb8bfeb17036ac5f8ebbebc06c937e4 Mon Sep 17 00:00:00 2001 From: tkaluza Date: Wed, 16 Sep 2026 13:22:31 +0200 Subject: [PATCH] win32 host: APPWINDOW client size used the system DPI, never read back create()'s width/height specify the CLIENT area, and Win32 takes an OUTER size, so widget_show grows the request by the non-client frame via AdjustWindowRectExForDpi. That conversion is DPI-dependent twice over - the client scales, and so do the border and title-bar metrics - but the host has to choose a DPI before the HWND exists, and it used GetDpiForSystem(). Nothing corrected it afterwards. When the estimate is wrong the client comes out wrong by the ratio of the two DPIs and stays wrong: WM_DPICHANGED fires when a window MOVES between monitors, not when it is born on one that disagrees with the system DPI. With GetDpiForSystem() = 144 and the frame landing on a 96 DPI monitor, a 940x400 request produced a 1416x617 client - the outer size computed for 144 (1432x656) minus the 96 DPI non-client frame. Not an exotic setup: a 150% primary with a 100% secondary hits it the moment a window opens on the secondary, and so does a remote session whose desktop DPI differs from the console's. Read GetDpiForWindow back once the HWND exists and, if it disagrees with the estimate, redo the client -> outer conversion at the real DPI. Before ShowWindow, so the correction is never visible as a resize flash. This is the same read-back the crossplatform host already does at the end of create_native_window - the native host simply never had it (#29 hardened the xpl side only). Found via an editor embedding neui: the shell's own APPWINDOW goes through this host (neui_get_api(nullptr) returns the native one when it is linked) while the embedded editor frame goes through the xpl host, so the editor was laid out correctly inside a window a third too large. Verified on a machine where GetDpiForSystem()=144 and the primary monitor is 96: the app's client area goes from 1416x617 to 940x400 (outer 1432x656 -> 956x439) and the UI fills the window. No regression test: this path needs a real frame from the native host, which only comes up under the WinMain that host provides, so a console harness like tests/embed_smoke_win32.cpp (xpl host, no WinMain needed) cannot reach it. Worth a WIN32-subsystem test harness as a follow-up. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XLLVDvxV66uC4GiMtb7tHH --- hosts/win32/widgets.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/hosts/win32/widgets.cpp b/hosts/win32/widgets.cpp index a5b916f..e793c01 100644 --- a/hosts/win32/widgets.cpp +++ b/hosts/win32/widgets.cpp @@ -2929,6 +2929,34 @@ namespace win32_host ); // w.hwnd is set by AppWindowProc WM_NCCREATE; children created in WM_CREATE if (w.hwnd) { + // The outer size above came from GetDpiForSystem(), which is only an + // estimate of where the frame will land: it may open on a monitor at a + // different scale (a 150% primary with a 100% secondary, or a remote + // session whose desktop DPI differs from the console's). Read the real + // per-monitor DPI back now that the HWND exists and, if it disagrees, + // redo the client -> outer conversion at that DPI. + // + // Without this the client area is wrong by the ratio of the two: a + // 940x400 request lands as a 1416x617 client when the system DPI is 144 + // and the monitor is 96. WM_DPICHANGED does not cover it - that fires + // when a window MOVES between monitors, not when it is born on one that + // disagrees with the system DPI. Before ShowWindow so the correction is + // not visible as a resize flash. The crossplatform host does the same + // read-back in create_native_window. + UINT actual_dpi = GetDpiForWindow(w.hwnd); + if (actual_dpi && actual_dpi != initial_dpi) { + bool menu_row = false; + for (uint32_t ci = _widgets.child(index); ci != 0; ci = _widgets.next(ci)) + if (_widgets.exists(ci) && _widgets[ci].hmenu != nullptr) { menu_row = true; break; } + RECT fix = { 0, 0, + LogicalToPhysical(w.width, actual_dpi), + LogicalToPhysical(w.height, actual_dpi) }; + AdjustWindowRectExForDpi(&fix, style, menu_row ? TRUE : FALSE, ex_style, actual_dpi); + SetWindowPos(w.hwnd, nullptr, 0, 0, + fix.right - fix.left, fix.bottom - fix.top, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } + if (!is_plug) ApplyText(w.hwnd, w.text); // Apply any icon attribute set before show. if (w.attrs) {