fix(window): set the title on Win32 through a Unicode window class, and send only the view's bytes on Wayland
Win32 - SetTitle had a body only for Wayland, so the title passed to Window's constructor and every later SetTitle left a Win32 window untitled. It converts the title from UTF-8 and hands it to SetWindowTextW. - The window class is registered with RegisterClassW, the window made with CreateWindowExW, and WndProc hands what it does not handle to DefWindowProcW. An ANSI class has the text of every message converted to the process code page on its way to WndProc, so SetWindowTextW alone would not do: under Wine, in CP1252, 日本 in a title came back as ??. WM_CHAR, whose handler takes wParam for a UTF-16 code unit, got € as its CP1252 byte 0x80 and passed it on as U+0080, and 日 and both halves of a surrogate pair as ?. - Both message pumps use PeekMessageW and DispatchMessageW. PeekMessageA converts WM_CHAR to the code page whatever the window's class: on the Unicode class, an ANSI pump still turned 日 and the surrogates into ?. Wayland - SetTitle handed title.data() to xdg_toplevel_set_title, which reads up to a NUL: a view into a longer string sent the whole string, and an empty view's null data() made libwayland fail the display with EINVAL. It copies the view into a std::string, as SetAppId does. Tests - TextWin32: the constructor's title, SetTitle with characters outside the code page, with a view into a longer string and with an empty title, read back with GetWindowTextW; and é, €, 日 and a surrogate pair posted as WM_CHAR and delivered by StartSync's own loop, which returns on the WM_CLOSE posted after them, so a StartSync pumping with the A functions fails it. The window is disabled, so a real keystroke can't reach the check. Without the fix every check but the empty title fails. Like PointerWin32, built on Linux it runs under Wine. - TitleWayland: SetTitle's set_title requests, read off a wl_display connected to a socketpair, for a view into a longer string and for an empty view. Both checks fail without the fix. No compositor. - PointerWin32 pumps with the W functions, as StartSync now does. Docs - AGENTS.md names TextWin32 beside PointerWin32 as the Win32 tests, says what their Wine runs needed (the vulkan-1 import library, -luuid, which the link flags lack, #184, and a __libc_single_threaded definition), and lists TitleWayland among the tests that need no GPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
234fcce099
commit
578a3c6dc4
6 changed files with 292 additions and 19 deletions
|
|
@ -124,8 +124,19 @@ static inline KeyCode KeyCodeFromLParam(LPARAM lParam) {
|
|||
| (((lParam >> 24) & 1u) << 8);
|
||||
}
|
||||
|
||||
// The library's strings are UTF-8, and the W functions take UTF-16.
|
||||
// Malformed sequences come out as U+FFFD.
|
||||
static std::wstring Utf16FromUtf8(std::string_view text) {
|
||||
if (text.empty()) return {};
|
||||
const int length = MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast<int>(text.size()), nullptr, 0);
|
||||
if (length <= 0) return {};
|
||||
std::wstring wide(static_cast<std::size_t>(length), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast<int>(text.size()), wide.data(), length);
|
||||
return wide;
|
||||
}
|
||||
|
||||
// Define a window class name
|
||||
const char g_szClassName[] = "myWindowClass";
|
||||
const wchar_t g_szClassName[] = L"myWindowClass";
|
||||
|
||||
// The Window an HWND belongs to, or nullptr once ~Window has detached it.
|
||||
// Looked up again after an Invoke that more work follows: a listener may
|
||||
|
|
@ -139,7 +150,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||
|
||||
if (msg == WM_NCCREATE)
|
||||
{
|
||||
CREATESTRUCT* pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
|
||||
CREATESTRUCTW* pCreate = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
Window* window = static_cast<Window*>(pCreate->lpCreateParams);
|
||||
|
||||
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window));
|
||||
|
|
@ -151,7 +162,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||
// ~Window, have no Window to go to.
|
||||
Window* window = WindowOf(hwnd);
|
||||
if (window == nullptr) {
|
||||
return DefWindowProc(hwnd, msg, wParam, lParam);
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
switch (msg) {
|
||||
|
|
@ -336,7 +347,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||
break;
|
||||
}
|
||||
|
||||
default: return DefWindowProc(hwnd, msg, wParam, lParam);
|
||||
default: return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -399,25 +410,31 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
#endif
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
// Initialize the window class
|
||||
WNDCLASS wc = {0};
|
||||
// A Unicode class, so the text in WndProc's messages is UTF-16: WM_CHAR
|
||||
// brings UTF-16 code units, and SetTitle's WM_SETTEXT reaches
|
||||
// DefWindowProcW as it was sent. A class from RegisterClassA gets its
|
||||
// text in the process code page, CP1252 on a Western system, where a
|
||||
// typed € would arrive as 0x80 and a 日, typed or in a title, as ?. The
|
||||
// pumps use PeekMessageW and DispatchMessageW too: PeekMessageA hands
|
||||
// WM_CHAR over in the code page whatever the window's class.
|
||||
WNDCLASSW wc = {0};
|
||||
wc.lpfnWndProc = WndProc; // Set window procedure
|
||||
wc.hInstance = GetModuleHandle(NULL); // Get instance handle
|
||||
wc.lpszClassName = g_szClassName;
|
||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
|
||||
// The first window registers the class; every later one finds it there.
|
||||
if (!RegisterClass(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
||||
if (!RegisterClassW(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
||||
MessageBox(NULL, "Window Class Registration Failed!", "Error", MB_ICONERROR);
|
||||
}
|
||||
|
||||
RECT rc = {0, 0, static_cast<LONG>(width), static_cast<LONG>(height)};
|
||||
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
|
||||
hwnd = CreateWindowEx(
|
||||
hwnd = CreateWindowExW(
|
||||
0,
|
||||
g_szClassName,
|
||||
"",
|
||||
L"",
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
rc.right - rc.left,
|
||||
|
|
@ -439,9 +456,9 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
// when a window appears under the cursor would make an onMouseEnter
|
||||
// nobody hears, and later an onMouseLeave without one.
|
||||
MSG msg;
|
||||
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_QS_POSTMESSAGE | PM_QS_PAINT | PM_QS_SENDMESSAGE)) {
|
||||
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE | PM_QS_POSTMESSAGE | PM_QS_PAINT | PM_QS_SENDMESSAGE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
VkWin32SurfaceCreateInfoKHR createInfo = {};
|
||||
|
|
@ -802,7 +819,12 @@ void Window::SetPresentMode(PresentMode mode) {
|
|||
|
||||
void Window::SetTitle(const std::string_view title) {
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
xdg_toplevel_set_title(xdgToplevel, title.data());
|
||||
// A copy: set_title reads up to a NUL, which a view need not have, and
|
||||
// an empty view's null data() is a fatal error on the display.
|
||||
xdg_toplevel_set_title(xdgToplevel, std::string(title).c_str());
|
||||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
SetWindowTextW(hwnd, Utf16FromUtf8(title).c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -1058,10 +1080,12 @@ void Window::StartSync() {
|
|||
QS_ALLINPUT, MWMO_INPUTAVAILABLE);
|
||||
}
|
||||
}
|
||||
// W, as the window class is: PeekMessageA would hand WM_CHAR over
|
||||
// in the code page (see the constructor).
|
||||
MSG msg;
|
||||
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
|
||||
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
Gamepad::Tick();
|
||||
onBeforeUpdate.Invoke();
|
||||
|
|
|
|||
Loading…
Reference in a new issue