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:
Jorijn van der Graaf 2026-10-31 18:27:24 +01:00
commit 578a3c6dc4
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
6 changed files with 292 additions and 19 deletions

View file

@ -66,11 +66,11 @@ crafter-build clean
## Testing instructions
Tests are declared in `project.cpp`, not discovered, through the `LibraryTest(name)` lambda: it copies the library's settings field by field (`Configuration` is not copyable) and compiles the library's sources plus `tests/<name>/main.cpp` into one executable, so a test reaches non-exported internals the way the library does. A new test is a directory, a `cfg.tests.push_back(LibraryTest("Name"));` and a comment above it saying what it covers and what it needs (a GPU, ray tracing, a compositor, a tool). A test that needs shaders adds them to `test.config.shaders` with the same `ShaderType` lines the examples use; a font or asset goes in `test.config.files`; a tool it runs goes in `requires_` (`{"tool:glslang", "tool:spirv-val"}`), which turns the test into a skip when the tool is missing. The declarations sit in the native branch of `project.cpp`: wasm runs no tests. Wayland-only tests sit in `if (!windows)`; `PointerWin32` is the one Win32 test, with `requires_ = {"tool:wine"}` when built on Linux.
Tests are declared in `project.cpp`, not discovered, through the `LibraryTest(name)` lambda: it copies the library's settings field by field (`Configuration` is not copyable) and compiles the library's sources plus `tests/<name>/main.cpp` into one executable, so a test reaches non-exported internals the way the library does. A new test is a directory, a `cfg.tests.push_back(LibraryTest("Name"));` and a comment above it saying what it covers and what it needs (a GPU, ray tracing, a compositor, a tool). A test that needs shaders adds them to `test.config.shaders` with the same `ShaderType` lines the examples use; a font or asset goes in `test.config.files`; a tool it runs goes in `requires_` (`{"tool:glslang", "tool:spirv-val"}`), which turns the test into a skip when the tool is missing. The declarations sit in the native branch of `project.cpp`: wasm runs no tests. Wayland-only tests sit in `if (!windows)`; the Win32 tests, `PointerWin32` and `TextWin32`, sit in `if (windows)`, with `requires_ = {"tool:wine"}` when built on Linux.
A test is an `int main()`: exit 0 passes, nonzero fails, 77 skips (for a missing capability, never for a failed check). Each runs in its own process, in parallel, with a 60 s default timeout, and the output of a failed, crashed or timed-out test is in `build/test-logs/<name>.log`; a later pass does not delete the log, so check its timestamp. What the tests need, in three groups:
- Pure logic, no GPU: they stage `Device`'s statics or feed synthetic data to the decision functions that were factored out for them (`Device::ResolveFeatures`, `ChooseDevice`, `CheckShaderCapabilities`, `GetMemoryType`, `PreferDirectDeviceWrite`, `PipelineCacheDataCompatible`, `AlignMappedFlushRange`, `BuildMipChainFinalBarriers`, `SwapchainStageUnion`, `BuildTlasWriteBarrier`, `UIRenderer::ClipFlags`, `Window::SwapchainMinImageCount`, `WorkaroundNvidiaAS::Patch`, `DescriptorHeapVulkan::ComputeLayout`, the font and input-field helpers). `PushConstantRewrite` and `UIFusedShader` compile GLSL with glslang and check the SPIR-V with spirv-val at runtime, and `ExampleShaders` compiles every GLSL shader under `examples/` the way crafter-build would, the descriptor-set build included, so a shader contract change that breaks an example fails here without building the example; `MouseScroll` and `PointerEnterLeave` drive the Wayland listener callbacks directly.
- Pure logic, no GPU: they stage `Device`'s statics or feed synthetic data to the decision functions that were factored out for them (`Device::ResolveFeatures`, `ChooseDevice`, `CheckShaderCapabilities`, `GetMemoryType`, `PreferDirectDeviceWrite`, `PipelineCacheDataCompatible`, `AlignMappedFlushRange`, `BuildMipChainFinalBarriers`, `SwapchainStageUnion`, `BuildTlasWriteBarrier`, `UIRenderer::ClipFlags`, `Window::SwapchainMinImageCount`, `WorkaroundNvidiaAS::Patch`, `DescriptorHeapVulkan::ComputeLayout`, the font and input-field helpers). `PushConstantRewrite` and `UIFusedShader` compile GLSL with glslang and check the SPIR-V with spirv-val at runtime, and `ExampleShaders` compiles every GLSL shader under `examples/` the way crafter-build would, the descriptor-set build included, so a shader contract change that breaks an example fails here without building the example; `MouseScroll` and `PointerEnterLeave` drive the Wayland listener callbacks directly, and `TitleWayland` reads the requests a window sends off a `wl_display` connected to a socketpair.
- A headless device: `Device::Initialize()` and no `Window` (`HeapSlotReuse`, `DeferredDeletion`, `MeshLifetime`, `ImageStagingRelease`, `BLASBuildOptions`, the `TLAS*` tests, ...). Headless still means a Wayland display on Linux. Most of the RT ones check `Device::rayTracingSupported` and return 77 without it; `BLASBuildOptions`, `TLASInstanceDirtyTracking` and `MeshDecompressStagingRelease` do not, and crash on a GPU without ray tracing (`Device::RequireRayTracing` throws out of `main`). `DeviceSelection` skips with one GPU.
- A window and a live compositor: `FrameLoopSync`, `SetupCmdBufferReuse`, `UIFallbackParity`, `SwapchainTransferDst`, `WindowUpdateClock`, `HeapASRead`, `HeapDynamicIndexHit`, `RTPassPushData`, `WindowOptionalGlobals` (hides xdg-decoration, viewporter and fractional scale from the window in turn), `SwapchainImageCount` (every present mode, and swapchains with more images than frames in flight, in child processes on llvmpipe where the host's GPU never makes one), and `FeatureRequests` for its retry case. They render, read the frame back with `Window::CaptureNextFrame` (a PNG, decoded with a private `STB_IMAGE_STATIC` copy of stb_image) or a transfer pass of their own, and check pixels.
@ -85,7 +85,7 @@ Conventions the existing tests follow:
A bug fix ships with a test that fails before the fix and passes after it. A change to a UI kernel keeps `UIFallbackParity` green: the heap and descriptor-set builds of one source must stay pixel-identical. A category drawn fused must also match the same category drawn standalone, but no test compares those two (`UIFusedShader` only pins the push-constant layout): check it by eye, or add the comparison.
Cross targets: `crafter-build test --target=x86_64-w64-mingw32` needs `wine` on the sandbox's `PATH`, which the Flatpak does not have, and a MinGW import library for `vulkan-1`, which the toolchain does not ship; the Win32 test has been run by building an import library from the `vk*` imports with `llvm-dlltool` and running the exe under the host's wine in a scratch `WINEPREFIX`. For an aarch64 device, `crafter-build test --target=...` tries qemu, which has no Vulkan: build with the sysroot, copy the test's whole bin directory (binary, `*.spv`, `*.sets.spv`, fonts) to the device and run it there, unlocked and with the window visible, since a locked phone's compositor sends no frame callbacks and the FIFO present then blocks forever.
Cross targets: `crafter-build test --target=x86_64-w64-mingw32` needs `wine` on the sandbox's `PATH`, which the Flatpak does not have; a MinGW import library for `vulkan-1`, which the toolchain does not ship; `-luuid`, which the Win32 link flags lack (#184); and a definition of `__libc_single_threaded`, which the MinGW build of Crafter.Asset references. The Win32 tests have been run with an import library made by `llvm-dlltool` from the `vk*` symbols `llvm-nm -u` lists for the MinGW `libCrafter.Graphics.a`, and an archive defining `char __libc_single_threaded = 0;`, both under the gitignored `build/`. A temporary edit of `project.cpp` added `-L` to that directory, both libraries and `-luuid` to the Win32 tests' link flags and cleared their `requires_`; the tests were built with `--runner=cmd:true`, and each exe ran from its bin directory under the host's wine in a scratch `WINEPREFIX`. For an aarch64 device, `crafter-build test --target=...` tries qemu, which has no Vulkan: build with the sysroot, copy the test's whole bin directory (binary, `*.spv`, `*.sets.spv`, fonts) to the device and run it there, unlocked and with the window visible, since a locked phone's compositor sends no frame callbacks and the FIFO present then blocks forever.
## Code style

View file

@ -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();

View file

@ -337,6 +337,16 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
win32PointerTest.requires_ = { "tool:wine" };
#endif
cfg.tests.push_back(std::move(win32PointerTest));
// The Win32 backend's text, UTF-8 to the library and UTF-16 to
// Windows: the title from the constructor and SetTitle, read back
// from the HWND, and the characters WM_CHAR brings. Needs what
// PointerWin32 needs.
Test win32TextTest = LibraryTest("TextWin32");
#ifndef _WIN32
win32TextTest.requires_ = { "tool:wine" };
#endif
cfg.tests.push_back(std::move(win32TextTest));
}
// Regression test for issue #32: the Wayland scroll-wheel chain
@ -351,6 +361,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
// Drives the wl_pointer callbacks directly, as MouseScroll does.
cfg.tests.push_back(LibraryTest("PointerEnterLeave"));
// SetTitle's xdg_toplevel.set_title, read off a wl_display on a
// socketpair: the view's bytes only, and an empty view as an
// empty title, not a null string. No compositor.
cfg.tests.push_back(LibraryTest("TitleWayland"));
// Issue #40: multi-frame-in-flight frame pacing (per-frame fences
// and acquire semaphores, per-image render-finished semaphores, no
// steady-state wait-idle). Drives the

View file

@ -43,9 +43,9 @@ void Check(bool ok, std::string_view what) {
// One pass of StartSync's pump.
void Pump() {
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
DispatchMessageW(&msg);
}
}

114
tests/TextWin32/main.cpp Normal file
View file

@ -0,0 +1,114 @@
//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;
}

120
tests/TitleWayland/main.cpp Normal file
View file

@ -0,0 +1,120 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SetTitle on the Wayland backend sends xdg_toplevel.set_title with exactly
// the view's bytes. It handed title.data() on, which set_title reads up to
// a NUL: a view into a longer string sent the whole string, and an empty
// view, whose data() is null, sent a null string, which libwayland refuses
// with a fatal error on the display.
//
// The toplevel is a proxy on a wl_display connected to one end of a
// socketpair, and the test reads the requests off the other end, so no
// compositor is needed. Needs the Wayland backend, hence Linux only.
#include <wayland-client.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include "../../lib/xdg-shell-client-protocol.h"
import Crafter.Graphics;
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;
}
// What the display has written so far.
std::vector<std::uint8_t> Sent(wl_display* display, int peer) {
wl_display_flush(display);
std::vector<std::uint8_t> wire;
std::uint8_t chunk[4096];
for (ssize_t n; (n = recv(peer, chunk, sizeof(chunk), MSG_DONTWAIT)) > 0;) {
wire.insert(wire.end(), chunk, chunk + n);
}
return wire;
}
// The titles of the set_title requests on `object` in `wire`. A request is
// its object id, then its size and opcode in one word, then its arguments;
// set_title's one argument is a string, its length with the NUL, then its
// bytes padded to a word.
std::vector<std::string> Titles(const std::vector<std::uint8_t>& wire, std::uint32_t object) {
std::vector<std::string> titles;
for (std::size_t at = 0; at + 8 <= wire.size();) {
std::uint32_t header[2];
std::memcpy(header, wire.data() + at, sizeof(header));
const std::uint32_t size = header[1] >> 16;
if (size < 8 || at + size > wire.size()) break;
if (header[0] == object && (header[1] & 0xFFFF) == XDG_TOPLEVEL_SET_TITLE && size >= 12) {
std::uint32_t length;
std::memcpy(&length, wire.data() + at + 8, sizeof(length));
if (length > 0 && 12 + length <= size) {
titles.emplace_back(reinterpret_cast<const char*>(wire.data() + at + 12), length - 1);
}
}
at += size;
}
return titles;
}
} // namespace
int main() {
int fds[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds) != 0) {
std::println("FAIL socketpair: {}", std::strerror(errno));
return EXIT_FAILURE;
}
// The display owns fds[0] from here.
wl_display* display = wl_display_connect_to_fd(fds[0]);
if (display == nullptr) {
std::println("FAIL wl_display_connect_to_fd: {}", std::strerror(errno));
return EXIT_FAILURE;
}
// libwayland makes whatever proxy it is asked to bind; nothing answers.
wl_registry* registry = wl_display_get_registry(display);
auto* toplevel = static_cast<xdg_toplevel*>(wl_registry_bind(registry, 1, &xdg_toplevel_interface, 1));
const std::uint32_t id = wl_proxy_get_id(reinterpret_cast<wl_proxy*>(toplevel));
Sent(display, fds[1]);
{
// A default-constructed window, which ~Window can tear down without
// a display, holding the proxy as its toplevel.
Window window;
window.xdgToplevel = toplevel;
const std::string longer = "Crafter.Graphics";
window.SetTitle(std::string_view(longer).substr(0, 7));
const std::vector<std::string> sliced = Titles(Sent(display, fds[1]), id);
Check(sliced == std::vector<std::string>{ "Crafter" }, "a view into a longer string: the view's bytes only");
if (sliced != std::vector<std::string>{ "Crafter" }) {
for (const std::string& title : sliced) std::println(" set_title \"{}\"", title);
}
window.SetTitle(std::string_view{});
const std::vector<std::string> empty = Titles(Sent(display, fds[1]), id);
const int error = wl_display_get_error(display);
Check(error == 0 && empty == std::vector<std::string>{ "" }, "an empty view: an empty title, not a null string");
if (error != 0) std::println(" the display failed: {}", std::strerror(error));
// ~Window destroys the toplevel.
}
wl_registry_destroy(registry);
wl_display_disconnect(display);
close(fds[1]);
if (failures != 0) {
std::println("{} check(s) failed", failures);
return EXIT_FAILURE;
}
std::println("all checks passed");
return EXIT_SUCCESS;
}