Crafter.Graphics/tests/TextWin32/main.cpp

114 lines
4.8 KiB
C++
Raw Normal View History

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>
2026-10-31 18:27:24 +01:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The Win32 backend's text, through a real HWND. The library speaks UTF-8
// and Windows UTF-16, and the window class decides what a window procedure
// hears: one registered with RegisterClassA gets text in the process code
// page, which is CP1252 on a Western system.
//
// - the title given to the constructor is the window's: SetTitle had no
// Win32 body, so every Win32 window was untitled
// - SetTitle replaces it, and characters outside the code page survive.
// On an ANSI class they would not: WM_SETTEXT would reach
// DefWindowProcA in the code page, and 日本 would come back as ??
// - SetTitle takes only the view's bytes, though the string goes on past
// them, and an empty title clears the old one
// - StartSync's loop delivers WM_CHAR as UTF-16 code units, as the handler
// always assumed. On the ANSI class € arrived as CP1252's 0x80, taken
// for U+0080, and 日 and both halves of a surrogate pair as ?. A loop
// that pumped with PeekMessageA would still turn 日 and the surrogates
// into ?, whatever the class
//
// The window is disabled, so no real keystroke reaches the WM_CHAR check,
// while the messages posted to it still arrive. Needs a Vulkan device, for
// the Window's swapchain. Built on Linux, it runs under Wine:
// crafter-build test --target=x86_64-w64-mingw32 TextWin32
#include <windows.h>
#include <cstdlib>
import Crafter.Graphics;
import Crafter.Event;
import std;
using namespace Crafter;
namespace {
int failures = 0;
void Check(bool ok, std::string_view what) {
std::println("{} {}", ok ? "PASS" : "FAIL", what);
if (!ok) ++failures;
}
// A string's code units in hex, for a failed check to say what it got.
template <typename Char>
std::string Units(std::basic_string_view<Char> text) {
std::string out;
for (const Char unit : text) {
out += std::format(" {:0{}X}", static_cast<std::make_unsigned_t<Char>>(unit), sizeof(Char) * 2);
}
return out;
}
// The title as Windows has it, which is what the title bar, the taskbar
// and Alt+Tab show.
void CheckTitle(HWND hwnd, std::wstring_view expected, std::string_view what) {
std::wstring title(static_cast<std::size_t>(GetWindowTextLengthW(hwnd)) + 1, L'\0');
title.resize(static_cast<std::size_t>(GetWindowTextW(hwnd, title.data(), static_cast<int>(title.size()))));
Check(title == expected, what);
if (title != expected) std::println(" the window has [{} ]", Units(std::wstring_view(title)));
}
} // namespace
int main() {
Device::Initialize();
auto window = std::make_unique<Window>(320, 240, "Crafter");
const HWND hwnd = window->hwnd;
// Windows activates the window when it is shown, so keys typed now
// would reach it. A disabled window gets no keyboard input, and what
// was typed before that is dropped.
EnableWindow(hwnd, FALSE);
MSG msg;
while (PeekMessageW(&msg, hwnd, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE)) {}
CheckTitle(hwnd, L"Crafter", "the constructor's title");
// é and € are in CP1252, 日本 and 😀 in no Western code page.
window->SetTitle("café € 日本 \U0001F600");
CheckTitle(hwnd, L"café € 日本 \U0001F600", "SetTitle, with characters outside the code page");
const std::string longer = "Crafter.Graphics";
window->SetTitle(std::string_view(longer).substr(0, 7));
CheckTitle(hwnd, L"Crafter", "SetTitle takes the view's bytes, not the string's up to its NUL");
window->SetTitle("");
CheckTitle(hwnd, L"", "an empty title clears the old one");
std::vector<std::string> typed;
EventListener<const std::string_view> textSub(&window->onTextInput, [&](const std::string_view text) {
typed.emplace_back(text);
});
// As TranslateMessage posts them, a UTF-16 code unit each: é is 0xE9 in
// CP1252 and Unicode alike, € is 0x80 in CP1252, 日 is in no Western
// code page, and 😀 is a surrogate pair. StartSync's own loop delivers
// them, and returns once the WM_CLOSE after them has cleared `open`.
for (const wchar_t unit : std::wstring_view(L"é€日\U0001F600")) {
PostMessageW(hwnd, WM_CHAR, unit, 1);
}
PostMessageW(hwnd, WM_CLOSE, 0, 0);
window->StartSync();
const std::vector<std::string> expected{ "é", "€", "日", "\U0001F600" };
Check(typed == expected, "WM_CHAR through StartSync: each character as itself in UTF-8, the surrogate pair as one");
if (typed != expected) {
for (const std::string& text : typed) std::println(" onTextInput had [{} ]", Units(std::string_view(text)));
}
if (failures != 0) {
std::println("{} check(s) failed", failures);
return EXIT_FAILURE;
}
std::println("all checks passed");
return EXIT_SUCCESS;
}