114 lines
4.8 KiB
C++
114 lines
4.8 KiB
C++
|
|
//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;
|
||
|
|
}
|