Skip to content

xpl host (win32): first RESIZE after frame creation reported physical px (#29) - #30

Merged
defiantnerd merged 1 commit into
mainfrom
fix/xpl-win32-first-resize-dpi
Aug 25, 2026
Merged

defiantnerd merged 1 commit into
mainfrom
fix/xpl-win32-first-resize-dpi

Conversation

@defiantnerd

Copy link
Copy Markdown
Owner

Fixes #29.

Cause

CreateWindowExW sends WM_SIZE before it returns - and for a WS_CHILD
embedded PLUGWINDOW it always does - but create_native_window only assigned
wd.dpi after the call returned. The WM_SIZE handler converts the physical
client size to logical with that dpi, so at its 96 default the conversion was an
identity and the client's first NEUI_EVENT_RESIZE carried physical pixels:
a frame created 940x400 reported 1410x600 at 150%. A client laying out from the
resize event then lays out 1.5x too big, which in an embedded editor pushes the
whole UI off the visible area.

The win32 native host does not have the bug because it seeds wd->dpi in its
WM_CREATE (hosts/win32/window.cpp), one message earlier. The xpl host now
does the same - that is the whole fix; the rest is hardening in the same area.

One correction to the issue text: on a single-monitor 150% setup a plain
NEUI_W_APPWINDOW does not reproduce. Instrumenting the message order shows
both WM_SIZEs for a WS_OVERLAPPEDWINDOW arrive after CreateWindowExW
returns (native_handle set, dpi already read back), so the pre-fix build
reported the correct logical size there. The creation-time WM_SIZE is specific
to the embedded WS_CHILD path.

Changes

  • WM_CREATE: seed wd->dpi from GetDpiForWindow(hwnd) before the
    creation-time WM_SIZE can read it. Chosen over an early-out on
    !native_handle (suggestion 1 in the issue) so the initial RESIZE still
    fires - with correct units - rather than being dropped at matching DPI while
    still firing in the mismatch case.
  • create_native_window: seed wd.dpi before CreateWindowExW from the
    best pre-creation estimate (embed parent / dialog owner DPI, else system DPI)
    and size the outer window from it. WM_GETMINMAXINFO arrives before
    WM_NCCREATE, so it too was scaling NEUI_ATTR_MIN_/MAX_ tracking sizes as
    if the frame were at 96 DPI; an embedded frame also lands at its parent's real
    scale on the first try now, instead of via the post-creation SetWindowPos
    correction.
  • WM_SIZE: convert with MulDiv (round-to-nearest, the exact inverse of
    the MulDiv used when sizing the window) instead of a truncating float
    divide, which lost a pixel at 125% (logical 941 -> 1176 phys -> 940).
  • Division-by-zero hardening (the issue's aside - GetDpiForWindow returns
    0 on failure): phys_to_log, platform_get_scale_factor, the post-creation
    read-back, Session::on_dpi_changed, and d2d_create_context (which was
    storing 0 and would later call SetDpi(0, 0)).

No public API or event-contract change.

Verification

Windows 10 19045, MSVC, display at 150% (system DPI 144), neui-xplhost +
neui-backend-d2d.

New tests/embed_smoke_win32.cpp - a fake-DAW harness mirroring the existing
Linux / macOS embed smokes (built, not ctest-registered: it needs a GUI session
and only differs from an identity check on a scaled display). It embeds a
PLUGWINDOW in a foreign HWND, drives it from the fake DAW's own message pump,
and asserts the client-area contract plus a logical-px first RESIZE.

Before:

fake DAW parent HWND=..., dpi=144 (150% scaling)
  RESIZE #1: 1410 x 600
PASS: embedded child HWND=... under the DAW parent
PASS: child client is 1410x600 physical = 940x400 logical at 144 dpi
FAIL: first RESIZE reported 1410x600, expected 940x400 logical - those are PHYSICAL pixels (issue #29)

After:

  RESIZE #1: 940 x 400
PASS: embedded child HWND=... under the DAW parent
PASS: child client is 1410x600 physical = 940x400 logical at 144 dpi
PASS: first RESIZE reported 940x400 logical

Also: neui_example (APPWINDOW + menubar, xpl host) still comes up with a
client of exactly 1010x645 logical / 1515x968 physical, so the sys_dpi ->
create_dpi swap is a no-op on a single-DPI setup; warning-clean full build;
ctest green.

Not measured: the mixed-DPI multi-monitor dialog path (no second monitor here).
It is a no-op by construction on one monitor - GetDpiForWindow(owner) == GetDpiForSystem() - and only diverges, in the correct direction, when owner and
primary differ.

🤖 Generated with Claude Code

… px (#29)

CreateWindowExW sends WM_SIZE before it returns (for a WS_CHILD embedded
PLUGWINDOW it always does), but the xpl host only assigned wd.dpi *after*
the call returned. The WM_SIZE handler converts the physical client size to
logical with that dpi, so at its 96 default the conversion was an identity
and the client's first NEUI_EVENT_RESIZE carried PHYSICAL pixels: a frame
created 940x400 reported 1410x600 at 150%. A client laying out from the
resize event - the documented way to react to a size change - then laid out
1.5x too big, which in an embedded editor pushes the whole UI off-screen.
The win32 native host does not have the bug because it seeds wd->dpi in its
WM_CREATE, one message earlier; the xpl host now does the same.

- WM_CREATE: seed wd->dpi from GetDpiForWindow(hwnd) before the
  creation-time WM_SIZE can read it.
- create_native_window: seed wd.dpi before CreateWindowExW from the best
  pre-creation estimate (embed parent / dialog owner DPI, else system DPI)
  and size the outer window from it. WM_GETMINMAXINFO arrives before
  WM_NCCREATE, so it too was scaling MIN_/MAX_ tracking sizes as if the
  frame were at 96 DPI; an embedded frame now also lands at the parent's
  real scale on the first try instead of via the correction round-trip.
- WM_SIZE: convert with MulDiv (round-to-nearest, the exact inverse of the
  MulDiv used when sizing the window) instead of a truncating float divide,
  which lost a pixel at 125% (logical 941 -> 1176 phys -> 940).
- Harden the division-by-zero paths GetDpiForWindow's 0-on-failure return
  feeds (the issue's aside): phys_to_log, platform_get_scale_factor, the
  post-creation read-back, Session::on_dpi_changed, d2d_create_context.

Adds tests/embed_smoke_win32.cpp, a fake-DAW harness mirroring the existing
Linux / macOS embed smokes (built, not ctest-registered): it embeds a
PLUGWINDOW in a foreign HWND, drives it from the fake DAW's own pump, and
asserts the client-area contract plus a logical-px first RESIZE. On this
150% display it fails before the fix (first RESIZE 1410x600) and passes
after (940x400).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@defiantnerd
defiantnerd merged commit 05a2133 into main Aug 25, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

xpl host (win32): first RESIZE after frame creation reports physical pixels as logical

1 participant