diff --git a/AGENTS.md b/AGENTS.md index 05c0479..2ad7012 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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//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//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/.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 diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 0b1aaea..29601e6 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -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(text.size()), nullptr, 0); + if (length <= 0) return {}; + std::wstring wide(static_cast(length), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast(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(lParam); + CREATESTRUCTW* pCreate = reinterpret_cast(lParam); Window* window = static_cast(pCreate->lpCreateParams); SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(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(width), static_cast(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(); diff --git a/project.cpp b/project.cpp index 3f26162..2a7c54e 100644 --- a/project.cpp +++ b/project.cpp @@ -337,6 +337,16 @@ extern "C" Configuration CrafterBuildProject(std::span 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 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 diff --git a/tests/PointerWin32/main.cpp b/tests/PointerWin32/main.cpp index bd4b725..5a82824 100644 --- a/tests/PointerWin32/main.cpp +++ b/tests/PointerWin32/main.cpp @@ -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); } } diff --git a/tests/TextWin32/main.cpp b/tests/TextWin32/main.cpp new file mode 100644 index 0000000..2afa605 --- /dev/null +++ b/tests/TextWin32/main.cpp @@ -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 +#include + +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 +std::string Units(std::basic_string_view text) { + std::string out; + for (const Char unit : text) { + out += std::format(" {:0{}X}", static_cast>(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(GetWindowTextLengthW(hwnd)) + 1, L'\0'); + title.resize(static_cast(GetWindowTextW(hwnd, title.data(), static_cast(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(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 typed; + EventListener 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 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; +} diff --git a/tests/TitleWayland/main.cpp b/tests/TitleWayland/main.cpp new file mode 100644 index 0000000..cb7dffc --- /dev/null +++ b/tests/TitleWayland/main.cpp @@ -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 +#include +#include +#include +#include +#include +#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 Sent(wl_display* display, int peer) { + wl_display_flush(display); + std::vector 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 Titles(const std::vector& wire, std::uint32_t object) { + std::vector 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(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(wl_registry_bind(registry, 1, &xdg_toplevel_interface, 1)); + const std::uint32_t id = wl_proxy_get_id(reinterpret_cast(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 sliced = Titles(Sent(display, fds[1]), id); + Check(sliced == std::vector{ "Crafter" }, "a view into a longer string: the view's bytes only"); + if (sliced != std::vector{ "Crafter" }) { + for (const std::string& title : sliced) std::println(" set_title \"{}\"", title); + } + + window.SetTitle(std::string_view{}); + const std::vector empty = Titles(Sent(display, fds[1]), id); + const int error = wl_display_get_error(display); + Check(error == 0 && empty == std::vector{ "" }, "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; +}