From bfc5e108e5e336e89faa4342a838d7967a24dcf0 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 17 Oct 2026 23:48:01 +0200 Subject: [PATCH 01/11] fix(wayland): route keys by keyboard focus, not pointer focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit keyboard_enter and keyboard_leave were empty and keyboard_key dereferenced Device::focusedWindow, which only a pointer-enter sets: the first key event in a window the pointer never entered — every key on a touch phone, or a key typed while the pointer hovers another window — dereferenced a null pointer, and keys typed after the pointer left went nowhere. Keyboard focus is its own thing in Wayland. wl_keyboard.enter now resolves its surface to the Window into Device::keyboardWindow, leave clears it together with the held keys (their releases happen elsewhere) and stops the repeat, and keyboard_key and TickKeyRepeats deliver to that window and drop a key that has none. --- implementations/Crafter.Graphics-Device.cpp | 42 ++++++++++++++------- interfaces/Crafter.Graphics-Device.cppm | 5 +++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index 138107c..d2344ce 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -441,11 +441,23 @@ void Device::keyboard_keymap(void *data, wl_keyboard *keyboard, uint32_t format, } void Device::keyboard_enter(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface, wl_array *keys) { - + for (Window* window : windows) { + if (window->surface == surface) { + keyboardWindow = window; + return; + } + } + keyboardWindow = nullptr; } void Device::keyboard_leave(void *data, wl_keyboard *keyboard, uint32_t serial, wl_surface *surface) { - + // Releases happen elsewhere while the focus is away: nothing we still + // hold is held, and a repeat must not keep firing into a window that + // lost the keyboard. + if (keyboardWindow != nullptr) keyboardWindow->heldKeys.clear(); + keyboardWindow = nullptr; + keyRepeat.active = false; + keyRepeat.utf8.clear(); } void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { @@ -456,12 +468,16 @@ void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, ui KeyCode code = key; xkb_keycode_t xkbKeycode = key + 8; + // No enter for any of our surfaces (a compositor quirk, or a key for a + // window already gone): nowhere to deliver it. + if (keyboardWindow == nullptr) return; + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { - if (focusedWindow->heldKeys.contains(code)) { - focusedWindow->onRawKeyHold.Invoke(code); + if (keyboardWindow->heldKeys.contains(code)) { + keyboardWindow->onRawKeyHold.Invoke(code); } else { - focusedWindow->heldKeys.insert(code); - focusedWindow->onRawKeyDown.Invoke(code); + keyboardWindow->heldKeys.insert(code); + keyboardWindow->onRawKeyDown.Invoke(code); } std::string buf; @@ -471,7 +487,7 @@ void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, ui if (n > 0 && (unsigned char)buf[0] >= 0x20 && buf[0] != 0x7f) { buf.resize(n); utf8 = buf; - focusedWindow->onTextInput.Invoke(utf8); + keyboardWindow->onTextInput.Invoke(utf8); } // Replace the active repeat with this key — most recent press wins, @@ -482,8 +498,8 @@ void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, ui keyRepeat.pressTime = std::chrono::steady_clock::now(); keyRepeat.lastFireTime = keyRepeat.pressTime; } else { - focusedWindow->heldKeys.erase(code); - focusedWindow->onRawKeyUp.Invoke(code); + keyboardWindow->heldKeys.erase(code); + keyboardWindow->onRawKeyUp.Invoke(code); // If the released key was the one repeating, stop. Otherwise leave // the existing repeat alone (user pressed/released a modifier @@ -506,7 +522,7 @@ void Device::keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rat } void Device::TickKeyRepeats() { - if (!keyRepeat.active || !focusedWindow) return; + if (!keyRepeat.active || !keyboardWindow) return; if (keyRepeat.rate <= 0) return; auto now = std::chrono::steady_clock::now(); @@ -521,10 +537,10 @@ void Device::TickKeyRepeats() { // Catch up — emit one event per missed period so a paused frame doesn't // make the repeat permanently lag behind. while (now - keyRepeat.lastFireTime >= period) { - focusedWindow->onRawKeyDown.Invoke(keyRepeat.key); - focusedWindow->onRawKeyHold.Invoke(keyRepeat.key); + keyboardWindow->onRawKeyDown.Invoke(keyRepeat.key); + keyboardWindow->onRawKeyHold.Invoke(keyRepeat.key); if (!keyRepeat.utf8.empty()) { - focusedWindow->onTextInput.Invoke(keyRepeat.utf8); + keyboardWindow->onTextInput.Invoke(keyRepeat.utf8); } keyRepeat.lastFireTime += period; } diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index d461eb6..be42e9e 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -49,6 +49,11 @@ export namespace Crafter { inline static xkb_keymap* xkb_keymap; inline static wl_compositor* compositor = nullptr; inline static Window* focusedWindow = nullptr; + // The window with keyboard focus (wl_keyboard.enter/leave), apart from + // the pointer's: a key arrives without the pointer ever having entered + // (a touch phone, a key typed while the pointer hovers elsewhere), so the + // keyboard handlers route by this and never by focusedWindow. + inline static Window* keyboardWindow = nullptr; inline static wl_shm* shm = nullptr; inline static wp_viewporter* wpViewporter = nullptr; inline static xkb_context* xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); From b412640fac94db502202232f889b0968e57ee347 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 00:40:25 +0200 Subject: [PATCH 02/11] feat(window): a destructor for the native backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Window could be created but never destroyed: the constructor's command buffers, semaphores, fences, swapchain and Vulkan surface, and on Wayland the viewport, fractional scale, decoration, xdg toplevel, xdg surface and wl_surface stayed alive, and Device::windows kept a dangling pointer the pointer listeners still compared against. An app that hides its window and shows it again — a resident messaging app closing to a notification — needs the window to go for real. The destructor waits for the device, frees and destroys the Vulkan objects in dependency order (the swapchain before the surface), releases the Wayland objects from the cursor down to the wl_surface, flushes, and removes the window from Device::windows and the focus pointers. The decoration object the constructor used to leak is a member now. The frame callback member is left alone: wl_surface_frame_done destroys the callback it is handed and re-arms through its own local, so the member may name a freed one. --- implementations/Crafter.Graphics-Window.cpp | 48 ++++++++++++++++++++- interfaces/Crafter.Graphics-Window.cppm | 12 ++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 017121c..dde2e3d 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -299,7 +299,7 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height wl_surface_commit(surface); - zxdg_toplevel_decoration_v1* decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(Device::manager, xdgToplevel); + decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(Device::manager, xdgToplevel); zxdg_toplevel_decoration_v1_set_mode(decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); wpViewport = wp_viewporter_get_viewport(Device::wpViewporter, surface); @@ -515,6 +515,52 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height currentMousePos = {0,0}; } +#ifndef CRAFTER_GRAPHICS_WINDOW_DOM +Window::~Window() { + // Nothing may still run against the semaphores, fences or images below. + if (Device::device != VK_NULL_HANDLE) vkDeviceWaitIdle(Device::device); + if (Device::device != VK_NULL_HANDLE) { + vkFreeCommandBuffers(Device::device, Device::commandPool, numFrames, drawCmdBuffers); + for (std::uint8_t i = 0; i < numFrames; i++) { + if (renderFinishedSemaphores[i]) vkDestroySemaphore(Device::device, renderFinishedSemaphores[i], nullptr); + if (waitFences[i]) vkDestroyFence(Device::device, waitFences[i], nullptr); + } + for (std::uint8_t i = 0; i < numAcquireSemaphores; i++) { + if (imageAcquiredSemaphores[i]) vkDestroySemaphore(Device::device, imageAcquiredSemaphores[i], nullptr); + } + if (swapChain != VK_NULL_HANDLE) vkDestroySwapchainKHR(Device::device, swapChain, nullptr); + swapChain = VK_NULL_HANDLE; + } + if (vulkanSurface != VK_NULL_HANDLE) vkDestroySurfaceKHR(Device::instance, vulkanSurface, nullptr); + vulkanSurface = VK_NULL_HANDLE; + + #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND + SetDefaultCursor(); + // `cb` is not destroyed here: wl_surface_frame_done frees the callback it + // was called with and re-arms through its own local, so the member may + // already name a freed one. + if (wpViewport) wp_viewport_destroy(wpViewport); + if (wp_scale) { + wp_fractional_scale_v1_destroy(wp_scale); + wp_scale = nullptr; + } + if (decoration) zxdg_toplevel_decoration_v1_destroy(decoration); + if (xdgToplevel) xdg_toplevel_destroy(xdgToplevel); + if (xdgSurface) xdg_surface_destroy(xdgSurface); + if (surface) wl_surface_destroy(surface); + wpViewport = nullptr; + decoration = nullptr; + xdgToplevel = nullptr; + xdgSurface = nullptr; + surface = nullptr; + wl_display_flush(Device::display); + std::erase(Device::windows, this); + if (Device::focusedWindow == this) Device::focusedWindow = nullptr; + if (Device::keyboardWindow == this) Device::keyboardWindow = nullptr; + #endif +} +#endif + void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) { // Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland // sometimes echoes the current size in a configure. diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index 37e97f4..c9f7155 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -110,16 +110,19 @@ export namespace Crafter { Window(Window&) = delete; Window(Window&&) = delete; Window& operator=(const Window&) = delete; - #ifdef CRAFTER_GRAPHICS_WINDOW_DOM // DOM mode keeps a process-global pointer to the live Window so // the JS bridge can deliver document/window events back. If the // Window is destroyed (e.g. it was stack-allocated in main and // main returned), the global must be cleared so subsequent // browser callbacks become no-ops instead of dereferencing - // freed memory. Native builds have nothing to clean up at the - // Window level (Vulkan resources teardown is the user's job). + // freed memory. Native builds release what the constructor made: + // after a device wait, the command buffers, semaphores and fences, + // the swapchain and the Vulkan surface, then (Wayland) the cursor, + // viewport, fractional scale, decoration, xdg toplevel, xdg surface + // and wl_surface, and the window leaves Device::windows and the + // focus pointers — so a window can be closed and another created, + // which a resident app does on every hide and show. ~Window(); - #endif // Enter the platform event loop and never return. In DOM mode this // hands the loop to requestAnimationFrame and _Exit(0)s so the wasm @@ -191,6 +194,7 @@ export namespace Crafter { std::int32_t pendingLogicalWidth = 0; std::int32_t pendingLogicalHeight = 0; xdg_toplevel* xdgToplevel = nullptr; + zxdg_toplevel_decoration_v1* decoration = nullptr; wp_viewport* wpViewport = nullptr; wl_surface* surface = nullptr; xdg_surface* xdgSurface = nullptr; From 6f5b192078f692d745347cd42d920c8214c130d6 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 01:12:21 +0200 Subject: [PATCH 03/11] fix(compute): destroy the shader module once its pipeline exists ComputeShader::Load built a VulkanShader on the stack and let it go with its VkShaderModule still alive: nothing in the library destroys one. A process that loads the UI shaders once never noticed; one that creates a window again and again (an app that hides and re-shows its window) leaked five modules per window. Vulkan allows destroying the module as soon as the pipeline is created, so Load does, on success and on failure alike. --- implementations/Crafter.Graphics-ComputeShader.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/implementations/Crafter.Graphics-ComputeShader.cpp b/implementations/Crafter.Graphics-ComputeShader.cpp index 3af554b..f1daa93 100644 --- a/implementations/Crafter.Graphics-ComputeShader.cpp +++ b/implementations/Crafter.Graphics-ComputeShader.cpp @@ -74,8 +74,12 @@ void ComputeShader::Load(const std::filesystem::path& spvPath, DescriptorHeapVul }, .layout = layout, }; - Device::CheckVkResult(vkCreateComputePipelines( - Device::device, Device::pipelineCache, 1, &info, nullptr, &pipeline)); + const VkResult created = vkCreateComputePipelines( + Device::device, Device::pipelineCache, 1, &info, nullptr, &pipeline); + // The module is only an input to pipeline creation; VulkanShader does + // not destroy it, so it is released here either way. + vkDestroyShaderModule(Device::device, shader.shader, nullptr); + Device::CheckVkResult(created); return; } @@ -106,8 +110,10 @@ void ComputeShader::Load(const std::filesystem::path& spvPath, DescriptorHeapVul }, .layout = VK_NULL_HANDLE, }; - Device::CheckVkResult(vkCreateComputePipelines( - Device::device, Device::pipelineCache, 1, &info, nullptr, &pipeline)); + const VkResult created = vkCreateComputePipelines( + Device::device, Device::pipelineCache, 1, &info, nullptr, &pipeline); + vkDestroyShaderModule(Device::device, shader.shader, nullptr); + Device::CheckVkResult(created); } void ComputeShader::Dispatch(VkCommandBuffer cmd, From b16f1e99d5fb7943c528256d2678995e7e30f2f6 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 01:12:21 +0200 Subject: [PATCH 04/11] fix(font-atlas): the atlas destroys its image FontAtlas::Initialize creates an ImageVulkan and nothing ever called its Destroy(): every atlas left its 1024x1024 image, memory and view behind. The atlas now owns the image for its lifetime and is no longer copyable. ImageVulkan's handles start as VK_NULL_HANDLE and Destroy() resets them, so an atlas that was never initialised, or an image destroyed by hand before the atlas goes, is a no-op in the destructor. The owner waits for the GPU before the atlas goes and, in sets mode, destroys the descriptor heap that holds the image's view before the atlas. --- interfaces/Crafter.Graphics-FontAtlas.cppm | 12 ++++++++++++ interfaces/Crafter.Graphics-ImageVulkan.cppm | 9 ++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/interfaces/Crafter.Graphics-FontAtlas.cppm b/interfaces/Crafter.Graphics-FontAtlas.cppm index 4f72522..732455d 100644 --- a/interfaces/Crafter.Graphics-FontAtlas.cppm +++ b/interfaces/Crafter.Graphics-FontAtlas.cppm @@ -96,6 +96,18 @@ export namespace Crafter { bool dirty = false; DirtyRect dirtyRect; +#ifndef CRAFTER_GRAPHICS_WINDOW_DOM + FontAtlas() = default; + FontAtlas(const FontAtlas&) = delete; + FontAtlas& operator=(const FontAtlas&) = delete; + // The image goes with the atlas. The owner waits for the GPU first + // and, in sets mode, destroys the heap holding the image's view before + // the atlas. + ~FontAtlas() { + if (image.image != VK_NULL_HANDLE) image.Destroy(); + } +#endif + void Initialize(GraphicsCommandBuffer cmd); // Returns the row-major byte pointer the CPU writes pixels into. diff --git a/interfaces/Crafter.Graphics-ImageVulkan.cppm b/interfaces/Crafter.Graphics-ImageVulkan.cppm index 3864d60..8ae4df8 100644 --- a/interfaces/Crafter.Graphics-ImageVulkan.cppm +++ b/interfaces/Crafter.Graphics-ImageVulkan.cppm @@ -56,8 +56,8 @@ export namespace Crafter { std::uint16_t width; std::uint16_t height; std::uint8_t mipLevels; - VkImage image; - VkDeviceMemory imageMemory; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory imageMemory = VK_NULL_HANDLE; VulkanBuffer buffer; // Transient host-visible staging for the compressed Update path. Same // lifetime contract as Mesh::compressedStaging: the compressed Update @@ -66,7 +66,7 @@ export namespace Crafter { // submit's frame has cleared instead of pinning it for the image's life. // Between Updates the handle is null; the next Update's Resize re-creates it. VulkanBuffer compressedStaging; - VkImageView imageView; + VkImageView imageView = VK_NULL_HANDLE; VkDescriptorImageInfo descriptor; // Pipeline stage that samples this image after upload. Set once at // Create and reused by every Update so the upload barrier's dst (and @@ -378,6 +378,9 @@ export namespace Crafter { vkDestroyImageView(Device::device, imageView, nullptr); vkDestroyImage(Device::device, image, nullptr); vkFreeMemory(Device::device, imageMemory, nullptr); + imageView = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + imageMemory = VK_NULL_HANDLE; // Free any staging this image still owns: a streamed image's // persistent `buffer`, or either staging buffer on an image // destroyed before its first upload released them (issue #114). From aed6142cce9b26c0aa90910a00194e1f65be6d0d Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 01:48:30 +0200 Subject: [PATCH 05/11] feat(wayland): touch, with the first finger standing in for the pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seat's wl_touch was never bound, so a touchscreen could not drive a window at all. Every finger is now reported through onTouchDown, onTouchMove, onTouchUp and onTouchCancel (TouchPoint: the compositor's id and the position in physical pixels). The first finger down also enters the window — Device::focusedWindow and the pointer serial the clipboard needs — presses the left button, moves it and releases it when it lifts, so an application written for a pointer works on a phone as it is. --- implementations/Crafter.Graphics-Device.cpp | 70 +++++++++++++++++++++ implementations/Crafter.Graphics-Window.cpp | 4 ++ interfaces/Crafter.Graphics-Device.cppm | 15 +++++ interfaces/Crafter.Graphics-Types.cppm | 7 +++ interfaces/Crafter.Graphics-Window.cppm | 8 +++ 5 files changed, 104 insertions(+) diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index d2344ce..21c35bc 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -546,12 +546,82 @@ void Device::TickKeyRepeats() { } } +// The first finger stands in for the pointer: it enters the window (so the +// clipboard has a serial and Device::focusedWindow a window), presses the +// left button, moves it and releases it. Every finger is also reported as +// such, in physical pixels. +void Device::touch_down(void*, wl_touch*, std::uint32_t serial, std::uint32_t, wl_surface* surface, std::int32_t id, wl_fixed_t x, wl_fixed_t y) { + Window* window = nullptr; + for (Window* candidate : windows) { + if (candidate->surface == surface) window = candidate; + } + if (window == nullptr) return; + touchWindow = window; + window->lastPointerSerial_ = serial; + const Vector pos = Vector(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale; + touchPositions[id] = { pos.x, pos.y }; + window->onTouchDown.Invoke({ id, pos }); + if (window->primaryTouchId < 0) { + window->primaryTouchId = id; + focusedWindow = window; + window->currentMousePos = pos; + window->mouseLeftHeld = true; + window->onMouseLeftClick.Invoke(); + } +} + +void Device::touch_up(void*, wl_touch*, std::uint32_t, std::uint32_t, std::int32_t id) { + if (touchWindow == nullptr) return; + Window* window = touchWindow; + const auto at = touchPositions.find(id); + const Vector pos = at == touchPositions.end() ? window->currentMousePos : Vector(at->second.first, at->second.second); + touchPositions.erase(id); + window->onTouchUp.Invoke({ id, pos }); + if (id == window->primaryTouchId) { + window->primaryTouchId = -1; + window->mouseLeftHeld = false; + window->onMouseLeftRelease.Invoke(); + } + if (touchPositions.empty()) touchWindow = nullptr; +} + +void Device::touch_motion(void*, wl_touch*, std::uint32_t, std::int32_t id, wl_fixed_t x, wl_fixed_t y) { + if (touchWindow == nullptr) return; + Window* window = touchWindow; + const Vector pos = Vector(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale; + touchPositions[id] = { pos.x, pos.y }; + window->onTouchMove.Invoke({ id, pos }); + if (id == window->primaryTouchId) { + window->currentMousePos = pos; + window->onMouseMove.Invoke(); + } +} + +void Device::touch_frame(void*, wl_touch*) {} + +void Device::touch_cancel(void*, wl_touch*) { + if (touchWindow == nullptr) return; + Window* window = touchWindow; + touchPositions.clear(); + touchWindow = nullptr; + if (window->primaryTouchId >= 0) { + window->primaryTouchId = -1; + window->mouseLeftHeld = false; + window->onMouseLeftRelease.Invoke(); + } + window->onTouchCancel.Invoke(); +} + void Device::seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities) { seat = seat; if (capabilities & WL_SEAT_CAPABILITY_POINTER) { wl_pointer* pointer = wl_seat_get_pointer(seat); wl_pointer_add_listener(pointer, &pointer_listener, nullptr); } + if ((capabilities & WL_SEAT_CAPABILITY_TOUCH) && wlTouch == nullptr) { + wlTouch = wl_seat_get_touch(seat); + wl_touch_add_listener(wlTouch, &touch_listener, nullptr); + } if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { wl_keyboard* keyboard = wl_seat_get_keyboard(seat); wl_keyboard_add_listener(keyboard, &keyboard_listener, nullptr); diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index dde2e3d..afb1939 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -557,6 +557,10 @@ Window::~Window() { std::erase(Device::windows, this); if (Device::focusedWindow == this) Device::focusedWindow = nullptr; if (Device::keyboardWindow == this) Device::keyboardWindow = nullptr; + if (Device::touchWindow == this) { + Device::touchWindow = nullptr; + Device::touchPositions.clear(); + } #endif } #endif diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index be42e9e..e4b54ab 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -60,6 +60,9 @@ export namespace Crafter { inline static xkb_state* xkb_state; inline static std::vector windows; inline static wl_pointer* wlPointer; + inline static wl_touch* wlTouch = nullptr; + inline static Window* touchWindow = nullptr; // the window under the fingers + inline static std::unordered_map> touchPositions; // per finger, physical px // wl_data_device_manager + wl_data_device drive copy/paste. Bound // lazily in handle_global; the data device is created once both // the manager and the seat are present (registry binding order @@ -90,6 +93,11 @@ export namespace Crafter { static void PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t time, std::uint32_t axis, wl_fixed_t value); static void PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y); static void PointerListenerHandleLeave(void*, wl_pointer*, std::uint32_t, wl_surface*); + static void touch_down(void* data, wl_touch* touch, std::uint32_t serial, std::uint32_t time, wl_surface* surface, std::int32_t id, wl_fixed_t x, wl_fixed_t y); + static void touch_up(void* data, wl_touch* touch, std::uint32_t serial, std::uint32_t time, std::int32_t id); + static void touch_motion(void* data, wl_touch* touch, std::uint32_t time, std::int32_t id, wl_fixed_t x, wl_fixed_t y); + static void touch_frame(void* data, wl_touch* touch); + static void touch_cancel(void* data, wl_touch* touch); constexpr static wl_pointer_listener pointer_listener = { .enter = PointerListenerHandleEnter, @@ -98,6 +106,13 @@ export namespace Crafter { .button = pointer_handle_button, .axis = PointerListenerHandleAxis, }; + constexpr static wl_touch_listener touch_listener = { + .down = touch_down, + .up = touch_up, + .motion = touch_motion, + .frame = touch_frame, + .cancel = touch_cancel, + }; constexpr static wl_keyboard_listener keyboard_listener = { .keymap = keyboard_keymap, .enter = keyboard_enter, diff --git a/interfaces/Crafter.Graphics-Types.cppm b/interfaces/Crafter.Graphics-Types.cppm index a7d5eeb..b13a040 100644 --- a/interfaces/Crafter.Graphics-Types.cppm +++ b/interfaces/Crafter.Graphics-Types.cppm @@ -18,6 +18,13 @@ export namespace Crafter { Vector mouseDelta; }; + // One finger on a touchscreen, in the window's physical pixels; the id is + // the compositor's and stays with the finger until it lifts. + struct TouchPoint { + std::int32_t id; + Vector position; + }; + struct ScaleData2D { Vector position; Vector size; diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index c9f7155..5b1e9dc 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -98,6 +98,14 @@ export namespace Crafter { // touchpads, free-spinning wheels) is accumulated backend-side // until a whole detent is reached. Event onMouseScroll; + // Touch, from the seat's wl_touch. The first finger down also drives + // currentMousePos and the left-button events until it lifts, so a + // pointer-only application works on a touchscreen as it is. + Event onTouchDown; + Event onTouchMove; + Event onTouchUp; + Event onTouchCancel; + std::int32_t primaryTouchId = -1; Vector currentMousePos; Vector lastMousePos; Vector mouseDelta; From 678f38964f56a74a795e91ff54490146134839e0 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 01:50:39 +0200 Subject: [PATCH 06/11] feat(wayland): text input through zwp_text_input_v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A window can now say that a text field has the focus and where its caret is — Window::SetTextInput(active, caret rectangle in physical pixels, purpose) — which is what opens the on-screen keyboard on a phone (Maliit under KWin, squeekboard under phoc). What the input method produces comes back as events: committed text as onTextInput, the composing text as onPreedit (empty clears it), deletions around the cursor as onTextInputDelete; done() applies them in the protocol's order. The state a window wants is remembered and sent again whenever the keyboard focus returns to it. The protocol's client code is generated with wayland-scanner 1.26.0 from wayland-protocols' text-input-unstable-v3.xml, under lib/ like the others. --- implementations/Crafter.Graphics-Device.cpp | 80 ++ implementations/Crafter.Graphics-Window.cpp | 14 + interfaces/Crafter.Graphics-Device.cppm | 31 + interfaces/Crafter.Graphics-Types.cppm | 9 + interfaces/Crafter.Graphics-Window.cppm | 14 + lib/text-input-unstable-v3-client-protocol.h | 1103 ++++++++++++++++++ lib/text-input-unstable-v3-protocol.c | 103 ++ project.cpp | 1 + 8 files changed, 1355 insertions(+) create mode 100644 lib/text-input-unstable-v3-client-protocol.h create mode 100644 lib/text-input-unstable-v3-protocol.c diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index 21c35bc..b8d0f15 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -15,6 +15,7 @@ module; #include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h" #include "../lib/fractional-scale-v1.h" #include "../lib/viewporter.h" +#include "../lib/text-input-unstable-v3-client-protocol.h" #include #include #include @@ -303,6 +304,10 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name if (dataDeviceManager != nullptr && dataDevice == nullptr) { dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat); } + if (textInputManager != nullptr && textInput == nullptr) { + textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat); + zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr); + } } else if (compositor == nullptr && strcmp(interface, wl_compositor_interface.name) == 0) { compositor = reinterpret_cast(wl_registry_bind(registry, name, &wl_compositor_interface, 3)); } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { @@ -324,9 +329,84 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name if (seat != nullptr && dataDevice == nullptr) { dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat); } + } else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) { + textInputManager = reinterpret_cast(wl_registry_bind(registry, name, &zwp_text_input_manager_v3_interface, 1)); + if (seat != nullptr && textInput == nullptr) { + textInput = zwp_text_input_manager_v3_get_text_input(textInputManager, seat); + zwp_text_input_v3_add_listener(textInput, &text_input_listener, nullptr); + } } } +void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) { + textInputWindow = nullptr; + for (Window* window : windows) { + if (window->surface == surface) textInputWindow = window; + } + if (textInputWindow != nullptr && textInputWindow->textInputActive_) ApplyTextInput(textInputWindow); +} + +void Device::text_input_leave(void*, zwp_text_input_v3*, wl_surface*) { + textInputWindow = nullptr; + textInputHasPreedit = textInputHasCommit = textInputHasDelete = false; + textInputPreedit.clear(); + textInputShownPreedit.clear(); +} + +void Device::text_input_preedit(void*, zwp_text_input_v3*, const char* text, std::int32_t, std::int32_t) { + textInputPreedit = text == nullptr ? "" : text; + textInputHasPreedit = true; +} + +void Device::text_input_commit(void*, zwp_text_input_v3*, const char* text) { + textInputCommit = text == nullptr ? "" : text; + textInputHasCommit = true; +} + +void Device::text_input_delete(void*, zwp_text_input_v3*, std::uint32_t before, std::uint32_t after) { + textInputDeleteBefore = before; + textInputDeleteAfter = after; + textInputHasDelete = true; +} + +// The protocol's order: the deletion around the cursor, then the committed +// text at the cursor, then the composing text after it. The pending values +// reset with every done, so a round that set no preedit means an empty one; +// the window hears about it only when that differs from what it shows. +void Device::text_input_done(void*, zwp_text_input_v3*, std::uint32_t) { + Window* window = textInputWindow; + if (!textInputHasPreedit) textInputPreedit.clear(); + if (window != nullptr) { + if (textInputHasDelete && (textInputDeleteBefore > 0 || textInputDeleteAfter > 0)) window->onTextInputDelete.Invoke({ textInputDeleteBefore, textInputDeleteAfter }); + if (textInputHasCommit && !textInputCommit.empty()) window->onTextInput.Invoke(textInputCommit); + if (textInputPreedit != textInputShownPreedit || (textInputHasCommit && !textInputCommit.empty())) { + textInputShownPreedit = textInputPreedit; + window->onPreedit.Invoke(textInputPreedit); + } + } + textInputHasPreedit = textInputHasCommit = textInputHasDelete = false; + textInputCommit.clear(); +} + +void Device::ApplyTextInput(Window* window) { + if (textInput == nullptr || window == nullptr || textInputWindow != window) return; + if (window->textInputActive_) { + zwp_text_input_v3_enable(textInput); + const bool phone = window->textInputPurpose_ == TextInputPurpose::Phone; + zwp_text_input_v3_set_content_type(textInput, + phone ? ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE : (ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION | ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK | ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION), + phone ? ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE : ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL); + const float s = window->scale > 0.0f ? window->scale : 1.0f; + zwp_text_input_v3_set_cursor_rectangle(textInput, + static_cast(window->textInputRect_[0] / s), static_cast(window->textInputRect_[1] / s), + std::max(1, static_cast(window->textInputRect_[2] / s)), std::max(1, static_cast(window->textInputRect_[3] / s))); + } else { + zwp_text_input_v3_disable(textInput); + } + zwp_text_input_v3_commit(textInput); + wl_display_flush(display); +} + void Device::handle_global_remove(void* data, wl_registry* registry, uint32_t name) { } diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index afb1939..5bd2045 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -11,6 +11,7 @@ module; #include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h" #include "../lib/fractional-scale-v1.h" #include "../lib/viewporter.h" +#include "../lib/text-input-unstable-v3-client-protocol.h" #include #include #include @@ -561,6 +562,7 @@ Window::~Window() { Device::touchWindow = nullptr; Device::touchPositions.clear(); } + if (Device::textInputWindow == this) Device::textInputWindow = nullptr; #endif } #endif @@ -618,6 +620,18 @@ void Window::SetTitle(const std::string_view title) { #endif } +void Window::SetTextInput(bool active, std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, TextInputPurpose purpose) { + #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND + textInputActive_ = active; + textInputRect_[0] = x; + textInputRect_[1] = y; + textInputRect_[2] = width; + textInputRect_[3] = height; + textInputPurpose_ = purpose; + Device::ApplyTextInput(this); + #endif +} + void Window::SetCursorImage(std::uint16_t width, std::uint16_t height, std::uint16_t hotspotX, std::uint16_t hotspotY, const std::uint8_t* pixels) { diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index e4b54ab..cc4ea3c 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -12,6 +12,7 @@ module; #include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h" #include "../lib/fractional-scale-v1.h" #include "../lib/viewporter.h" +#include "../lib/text-input-unstable-v3-client-protocol.h" #include #endif export module Crafter.Graphics:Device; @@ -76,6 +77,22 @@ export namespace Crafter { // whole detent yet is carried here so smooth-scroll devices add up // across events. Reset on pointer leave. inline static double scrollDetentRemainder = 0.0; + // zwp_text_input_v3: one object per seat; the compositor says which + // surface it follows (enter/leave), the events are state that done() + // applies as a whole. + inline static zwp_text_input_manager_v3* textInputManager = nullptr; + inline static zwp_text_input_v3* textInput = nullptr; + inline static Window* textInputWindow = nullptr; + inline static std::string textInputPreedit; + inline static std::string textInputShownPreedit; // what the window was last told + inline static std::string textInputCommit; + inline static std::uint32_t textInputDeleteBefore = 0; + inline static std::uint32_t textInputDeleteAfter = 0; + inline static bool textInputHasPreedit = false; + inline static bool textInputHasCommit = false; + inline static bool textInputHasDelete = false; + // Sends the window's wanted text-input state when it has the focus. + static void ApplyTextInput(Window* window); static void seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities); static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale); @@ -98,6 +115,12 @@ export namespace Crafter { static void touch_motion(void* data, wl_touch* touch, std::uint32_t time, std::int32_t id, wl_fixed_t x, wl_fixed_t y); static void touch_frame(void* data, wl_touch* touch); static void touch_cancel(void* data, wl_touch* touch); + static void text_input_enter(void* data, zwp_text_input_v3* textInput, wl_surface* surface); + static void text_input_leave(void* data, zwp_text_input_v3* textInput, wl_surface* surface); + static void text_input_preedit(void* data, zwp_text_input_v3* textInput, const char* text, std::int32_t cursorBegin, std::int32_t cursorEnd); + static void text_input_commit(void* data, zwp_text_input_v3* textInput, const char* text); + static void text_input_delete(void* data, zwp_text_input_v3* textInput, std::uint32_t before, std::uint32_t after); + static void text_input_done(void* data, zwp_text_input_v3* textInput, std::uint32_t serial); constexpr static wl_pointer_listener pointer_listener = { .enter = PointerListenerHandleEnter, @@ -113,6 +136,14 @@ export namespace Crafter { .frame = touch_frame, .cancel = touch_cancel, }; + constexpr static zwp_text_input_v3_listener text_input_listener = { + .enter = text_input_enter, + .leave = text_input_leave, + .preedit_string = text_input_preedit, + .commit_string = text_input_commit, + .delete_surrounding_text = text_input_delete, + .done = text_input_done, + }; constexpr static wl_keyboard_listener keyboard_listener = { .keymap = keyboard_keymap, .enter = keyboard_enter, diff --git a/interfaces/Crafter.Graphics-Types.cppm b/interfaces/Crafter.Graphics-Types.cppm index b13a040..1cab5c8 100644 --- a/interfaces/Crafter.Graphics-Types.cppm +++ b/interfaces/Crafter.Graphics-Types.cppm @@ -25,6 +25,15 @@ export namespace Crafter { Vector position; }; + // The input method asks for bytes around the cursor to go (text-input-v3 + // counts UTF-8 bytes). + struct TextDelete { + std::uint32_t beforeBytes; + std::uint32_t afterBytes; + }; + + enum class TextInputPurpose : std::uint8_t { Normal, Phone }; + struct ScaleData2D { Vector position; Vector size; diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index 5b1e9dc..7fd4276 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -13,6 +13,7 @@ module; #include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h" #include "../lib/fractional-scale-v1.h" #include "../lib/viewporter.h" +#include "../lib/text-input-unstable-v3-client-protocol.h" #include #include #include @@ -106,6 +107,11 @@ export namespace Crafter { Event onTouchUp; Event onTouchCancel; std::int32_t primaryTouchId = -1; + // Text input (zwp_text_input_v3): the input method's composing text + // (an empty string clears it) and its deletions around the cursor; + // the text it commits arrives as onTextInput, like a key's. + Event onPreedit; + Event onTextInputDelete; Vector currentMousePos; Vector lastMousePos; Vector mouseDelta; @@ -151,6 +157,11 @@ export namespace Crafter { void StartUpdate(); void StopUpdate(); void SetTitle(const std::string_view title); + // Tells the compositor's input method that a text field has focus + // (an on-screen keyboard opens) and where its caret is, in physical + // pixels, or that none has. Remembered: it is applied again whenever + // the keyboard focus comes back to this window. + void SetTextInput(bool active, std::int32_t x = 0, std::int32_t y = 0, std::int32_t width = 0, std::int32_t height = 0, TextInputPurpose purpose = TextInputPurpose::Normal); void Resize(std::uint32_t width, std::uint32_t height); void Render(); void Update(); @@ -219,6 +230,9 @@ export namespace Crafter { // Needed so `SetCursorImage` can re-issue `wl_pointer_set_cursor` // mid-session (the hotspot only updates when set_cursor is recalled). std::uint32_t lastPointerSerial_ = 0; + bool textInputActive_ = false; + std::int32_t textInputRect_[4] = { 0, 0, 0, 0 }; + TextInputPurpose textInputPurpose_ = TextInputPurpose::Normal; static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale); static void wl_surface_frame_done(void *data, wl_callback *cb, uint32_t time); diff --git a/lib/text-input-unstable-v3-client-protocol.h b/lib/text-input-unstable-v3-client-protocol.h new file mode 100644 index 0000000..b2d3624 --- /dev/null +++ b/lib/text-input-unstable-v3-client-protocol.h @@ -0,0 +1,1103 @@ +/* Generated by wayland-scanner 1.26.0 */ + +#ifndef TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H +#define TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol + * Protocol for composing text + * + * @section page_desc_text_input_unstable_v3 Description + * + * This protocol allows compositors to act as input methods and to send text + * to applications. A text input object is used to manage state of what are + * typically text entry fields in the application. + * + * This document adheres to the RFC 2119 when using words like "must", + * "should", "may", etc. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * + * @section page_ifaces_text_input_unstable_v3 Interfaces + * - @subpage page_iface_zwp_text_input_v3 - text input + * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager + * @section page_copyright_text_input_unstable_v3 Copyright + *
+ *
+ * Copyright © 2012, 2013 Intel Corporation
+ * Copyright © 2015, 2016 Jan Arne Petersen
+ * Copyright © 2017, 2018 Red Hat, Inc.
+ * Copyright © 2018       Purism SPC
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission.  The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ * 
+ */ +struct wl_seat; +struct wl_surface; +struct zwp_text_input_manager_v3; +struct zwp_text_input_v3; + +#ifndef ZWP_TEXT_INPUT_V3_INTERFACE +#define ZWP_TEXT_INPUT_V3_INTERFACE +/** + * @page page_iface_zwp_text_input_v3 zwp_text_input_v3 + * @section page_iface_zwp_text_input_v3_desc Description + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + * @section page_iface_zwp_text_input_v3_api API + * See @ref iface_zwp_text_input_v3. + */ +/** + * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + */ +extern const struct wl_interface zwp_text_input_v3_interface; +#endif +#ifndef ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE +#define ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE +/** + * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3 + * @section page_iface_zwp_text_input_manager_v3_desc Description + * + * A factory for text-input objects. This object is a global singleton. + * @section page_iface_zwp_text_input_manager_v3_api API + * See @ref iface_zwp_text_input_manager_v3. + */ +/** + * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface + * + * A factory for text-input objects. This object is a global singleton. + */ +extern const struct wl_interface zwp_text_input_manager_v3_interface; +#endif + +#ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +#define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * text change reason + * + * Reason for the change of surrounding text or cursor posision. + */ +enum zwp_text_input_v3_change_cause { + /** + * input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0, + /** + * something else than the input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1, +}; +#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content hint + * + * Content hint is a bitmask to allow to modify the behavior of the text + * input. + */ +enum zwp_text_input_v3_content_hint { + /** + * no special behavior + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0, + /** + * suggest word completions + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1, + /** + * suggest word corrections + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2, + /** + * switch to uppercase letters at the start of a sentence + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4, + /** + * prefer lowercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8, + /** + * prefer uppercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10, + /** + * prefer casing for titles and headings (can be language dependent) + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20, + /** + * characters should be hidden + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40, + /** + * typed text should not be stored + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80, + /** + * just Latin characters should be entered + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100, + /** + * the text input is multiline + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200, + /** + * an on-screen way to fill in the input is already provided by the client + * @since 2 + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_ON_SCREEN_INPUT_PROVIDED = 0x400, + /** + * prefer not offering emoji support + * @since 2 + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NO_EMOJI = 0x800, + /** + * the text input will display preedit text in place + * @since 2 + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN = 0x1000, +}; +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ON_SCREEN_INPUT_PROVIDED_SINCE_VERSION 2 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_NO_EMOJI_SINCE_VERSION 2 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN_SINCE_VERSION 2 +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content purpose + * + * The content purpose allows to specify the primary purpose of a text + * input. + * + * This allows an input method to show special purpose input panels with + * extra characters or to disallow some characters. + */ +enum zwp_text_input_v3_content_purpose { + /** + * default input, allowing all characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0, + /** + * allow only alphabetic characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1, + /** + * allow only digits + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2, + /** + * input a number (including decimal separator and sign) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3, + /** + * input a phone number + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4, + /** + * input an URL + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5, + /** + * input an email address + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6, + /** + * input a name of a person + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7, + /** + * input a password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8, + /** + * input is a numeric password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9, + /** + * input a date + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10, + /** + * input a time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11, + /** + * input a date and time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12, + /** + * input for a terminal + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13, +}; +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_ERROR_ENUM +#define ZWP_TEXT_INPUT_V3_ERROR_ENUM +enum zwp_text_input_v3_error { + /** + * an invalid or duplicate action was specified + */ + ZWP_TEXT_INPUT_V3_ERROR_INVALID_ACTION = 0, +}; +#endif /* ZWP_TEXT_INPUT_V3_ERROR_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_ACTION_ENUM +#define ZWP_TEXT_INPUT_V3_ACTION_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * action + * + * A possible action to perform on a text input. + * + * The submit action is intended for input entries that expect some sort of + * activation after user interaction, e.g. the URL entry in a browser. + */ +enum zwp_text_input_v3_action { + /** + * no action + */ + ZWP_TEXT_INPUT_V3_ACTION_NONE = 0, + /** + * the action is submitted + */ + ZWP_TEXT_INPUT_V3_ACTION_SUBMIT = 1, +}; +#endif /* ZWP_TEXT_INPUT_V3_ACTION_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM +#define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * preedit style hint + * + * Style hints for the preedit string. + */ +enum zwp_text_input_v3_preedit_hint { + /** + * simple pre-edit text style, typically underlined + */ + ZWP_TEXT_INPUT_V3_PREEDIT_HINT_WHOLE = 1, + /** + * hint for a selected piece of text, e.g. per-character navigation and composition + */ + ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SELECTION = 2, + /** + * predicted text, not typed by the user + */ + ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREDICTION = 3, + /** + * prefixed text not being currently edited, e.g. prior to a 'selection' section + */ + ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREFIX = 4, + /** + * suffixed text not being currently edited, e.g. after a 'selection' section + */ + ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SUFFIX = 5, + /** + * spelling error + */ + ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SPELLING_ERROR = 6, + /** + * wrong composition, e.g. user input that can not be transliterated + */ + ZWP_TEXT_INPUT_V3_PREEDIT_HINT_COMPOSE_ERROR = 7, +}; +#endif /* ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM */ + +/** + * @ingroup iface_zwp_text_input_v3 + * @struct zwp_text_input_v3_listener + */ +struct zwp_text_input_v3_listener { + /** + * enter event + * + * Notification that this seat's text-input focus is on a certain + * surface. + * + * If client has created multiple text input objects, compositor + * must send this event to all of them. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. This event sets the current surface + * for the text-input object. + */ + void (*enter)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * leave event + * + * Notification that this seat's text-input focus is no longer on + * a certain surface. The client should reset any preedit string + * previously set. + * + * The leave notification clears the current surface. It is sent + * before the enter notification for the new focus. After leave + * event, compositor must ignore requests from any text input + * instances until next enter event. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. + */ + void (*leave)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * pre-edit + * + * Notify when a new composing text (pre-edit) should be set at + * the current cursor position. Any previously set composing text + * must be removed. Any previously existing selected text must be + * removed. + * + * The argument text contains the pre-edit string buffer. + * + * The parameters cursor_begin and cursor_end are counted in bytes + * relative to the beginning of the submitted text buffer. Cursor + * should be hidden when both are equal to -1. + * + * They could be represented by the client as a line if both values + * are the same, or as a text highlight otherwise. + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string, and cursor_begin, + * cursor_end and cursor_hidden are all 0. + */ + void (*preedit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text, + int32_t cursor_begin, + int32_t cursor_end); + /** + * text commit + * + * Notify when text should be inserted into the editor widget. + * The text to commit could be either just a single character after + * a key press or the result of some composing (pre-edit). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string. + */ + void (*commit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text); + /** + * delete surrounding text + * + * Notify when the text around the current cursor position should + * be deleted. + * + * Before_length and after_length are the number of bytes before + * and after the current cursor index (excluding the selection) to + * delete. + * + * If a preedit text is present, in effect before_length is counted + * from the beginning of it, and after_length from its end (see + * done event sequence). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial values of both before_length and after_length are 0. + * @param before_length length of text before current cursor position + * @param after_length length of text after current cursor position + */ + void (*delete_surrounding_text)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t before_length, + uint32_t after_length); + /** + * apply changes + * + * Instruct the application to apply changes to state requested + * by the preedit_string, commit_string delete_surrounding_text, + * and action events. + * + * The state relating to these events is double-buffered, and each + * one modifies the pending state. This event replaces the current + * state with the pending state. + * + * The application must proceed by evaluating the changes in the + * following order: + * + * 1. Replace existing preedit string with the cursor. 2. Delete + * requested surrounding text. 3. Insert commit string with the + * cursor at its end. 4. Calculate surrounding text to send. 5. + * Insert new preedit text in cursor position. 6. Place cursor + * inside preedit text. 7. Perform the requested action. + * + * The serial number reflects the last state of the + * zwp_text_input_v3 object known to the compositor. The value of + * the serial argument must be equal to the number of commit + * requests already issued on that object. + * + * When the client receives a done event with a serial different + * than the number of past commit requests, it must proceed with + * evaluating and applying the changes as normal, except it should + * not change the current state of the zwp_text_input_v3 object. + * All pending state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) on the + * zwp_text_input_v3 object should be sent and committed after + * receiving a zwp_text_input_v3.done event with a matching serial. + */ + void (*done)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t serial); + /** + * action performed + * + * An action was performed on this text input. + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of action is none. + * @param action action performed + * @param serial serial number of the action event + * @since 2 + */ + void (*action)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t action, + uint32_t serial); + /** + * notify of language selection + * + * Notify the application of language used by the input method. + * + * This event will be sent on creation if known and for all + * subsequent changes. + * + * The language should be specified as an IETF BCP 47 tag. Setting + * an empty string will reset any known language back to the + * default unknown state. + * @param language new language set by IME + * @since 2 + */ + void (*language)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *language); + /** + * pre-edit + * + * Notify of contextual hints for the pre-edit string. This event + * is always sent together with a zwp_text_input_v3.preedit_string + * event. + * + * The parameters start and end are counted in bytes relative to + * the beginning of the text buffer submitted through + * zwp_text_input_v3.preedit_string, and represent the substring in + * the pre-edit text affected by the hint. + * + * Multiple events may be submitted if the preedit string has + * different sections. The extent of hints may overlap. The parts + * of the preedit string that are not covered by any + * zwp_text_input_v3.preedit_hint event, the text will be + * considered unhinted. This is also the case if no preedit_hint + * event is sent. + * + * Clients should provide recognizable visuals to these hints. if + * they are unable to comply with this requisition, it may be + * preferable for them keep the preedit_shown content hint + * disabled. + * + * Values set with this event are double-buffered. They must be + * applied and reset on the next zwp_text_input_v3.done event. + * @param start starting point of the affected substring + * @param end end point of the affected substring + * @param hint hint to apply + * @since 2 + */ + void (*preedit_hint)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t start, + uint32_t end, + uint32_t hint); +}; + +/** + * @ingroup iface_zwp_text_input_v3 + */ +static inline int +zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *zwp_text_input_v3, + const struct zwp_text_input_v3_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwp_text_input_v3, + (void (**)(void)) listener, data); +} + +#define ZWP_TEXT_INPUT_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_V3_ENABLE 1 +#define ZWP_TEXT_INPUT_V3_DISABLE 2 +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT 3 +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE 4 +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE 5 +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE 6 +#define ZWP_TEXT_INPUT_V3_COMMIT 7 +#define ZWP_TEXT_INPUT_V3_SET_AVAILABLE_ACTIONS 8 +#define ZWP_TEXT_INPUT_V3_SHOW_INPUT_PANEL 9 +#define ZWP_TEXT_INPUT_V3_HIDE_INPUT_PANEL 10 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ACTION_SINCE_VERSION 2 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_LANGUAGE_SINCE_VERSION 2 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SINCE_VERSION 2 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_AVAILABLE_ACTIONS_SINCE_VERSION 2 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SHOW_INPUT_PANEL_SINCE_VERSION 2 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_HIDE_INPUT_PANEL_SINCE_VERSION 2 + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void +zwp_text_input_v3_set_user_data(struct zwp_text_input_v3 *zwp_text_input_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void * +zwp_text_input_v3_get_user_data(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_v3); +} + +static inline uint32_t +zwp_text_input_v3_get_version(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Destroy the wp_text_input object. Also disables all surfaces enabled + * through this wp_text_input object. + */ +static inline void +zwp_text_input_v3_destroy(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Requests text input on the surface previously obtained from the enter + * event. + * + * This request must be issued every time the focused text input changes + * to a new one, including within the current surface. Use + * zwp_text_input_v3.disable when there is no longer any input focus on + * the current surface. + * + * Clients must not enable more than one text input on the single seat + * and should disable the current text input before enabling the new one. + * Requests to enable a text input when another text input is enabled + * on the same seat must be ignored by compositor. + * + * This request resets all state associated with previous enable, disable, + * set_surrounding_text, set_text_change_cause, set_content_type, and + * set_cursor_rectangle requests, as well as the state associated with + * preedit_string, commit_string, and delete_surrounding_text events. + * + * The set_surrounding_text, set_content_type and set_cursor_rectangle + * requests must follow if the text input supports the necessary + * functionality. + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The changes must be applied by the compositor after issuing a + * zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_enable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_ENABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Explicitly disable text input on the current surface (typically when + * there is no focus on any text entry inside the surface). + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_disable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DISABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the surrounding plain text around the input, excluding the preedit + * text. + * + * The client should notify the compositor of any changes in any of the + * values carried with this request, including changes caused by handling + * incoming text-input events as well as changes caused by other + * mechanisms like keyboard typing. + * + * If the client is unaware of the text around the cursor, it should not + * issue this request, to signify lack of support to the compositor. + * + * Text is UTF-8 encoded, and should include the cursor position, the + * complete selection and additional characters before and after them. + * There is a maximum length of wayland messages, so text can not be + * longer than 4000 bytes. + * + * Cursor is the byte offset of the cursor within text buffer. + * + * Anchor is the byte offset of the selection anchor within text buffer. + * If there is no selected text, anchor is the same as cursor. + * + * If any preedit text is present, it is replaced with a cursor for the + * purpose of this event. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial state for affected fields is empty, meaning that the text + * input does not support sending surrounding text. If the empty values + * get applied, subsequent attempts to change them may have no effect. + */ +static inline void +zwp_text_input_v3_set_surrounding_text(struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor, int32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, text, cursor, anchor); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Tells the compositor why the text surrounding the cursor changed. + * + * Whenever the client detects an external change in text, cursor, or + * anchor posision, it must issue this request to the compositor. This + * request is intended to give the input method a chance to update the + * preedit text in an appropriate way, e.g. by removing it when the user + * starts typing with a keyboard. + * + * cause describes the source of the change. + * + * The value set with this request is double-buffered. It must be applied + * and reset to initial at the next zwp_text_input_v3.commit request. + * + * The initial value of cause is input_method. + */ +static inline void +zwp_text_input_v3_set_text_change_cause(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t cause) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, cause); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the content purpose and content hint. While the purpose is the + * basic purpose of an input field, the hint flags allow to modify some of + * the behavior. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request. + * Subsequent attempts to update them may have no effect. The values + * remain valid until the next committed enable or disable request. + * + * The initial value for hint is none, and the initial value for purpose + * is normal. + */ +static inline void +zwp_text_input_v3_set_content_type(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t hint, uint32_t purpose) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, hint, purpose); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Marks an area around the cursor as a x, y, width, height rectangle in + * surface local coordinates. + * + * Allows the compositor to put a window with word suggestions near the + * cursor, without obstructing the text being input. + * + * If the client is unaware of the position of edited text, it should not + * issue this request, to signify lack of support to the compositor. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial values describing a cursor rectangle are empty. That means + * the text input does not support describing the cursor area. If the + * empty values get applied, subsequent attempts to change them may have + * no effect. + * + * As of version 2, the zwp_text_input_v3.commit request does not apply + * values sent with this request. Instead, it stores them in a separate + * "committed" area. The committed values, if still valid, get applied on + * the next wl_surface.commit request on the surface with text-input focus. + * Both committed and applied values get invalidated on: + * + * - the next committed enable or disable request, or + * - a change of the focused surface of the text-input (leave or enter events). + * + * This double stage application allows the compositor to position + * the input method popup in the same frame as the contents + * of the text on the surface are updated. + */ +static inline void +zwp_text_input_v3_set_cursor_rectangle(struct zwp_text_input_v3 *zwp_text_input_v3, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, x, y, width, height); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Atomically applies state changes recently sent to the compositor. + * + * The commit request establishes and updates the state of the client, and + * must be issued after any changes to apply them. + * + * Text input state (enabled status, content purpose, content hint, + * surrounding text and change cause, cursor rectangle) is conceptually + * double-buffered within the context of a text input, i.e. between a + * committed enable request and the following committed enable or disable + * request. + * + * Protocol requests modify the pending state, as opposed to the current + * state in use by the input method. A commit request atomically applies + * all pending state, replacing the current state. After commit, the new + * pending state is as documented for each related request. + * + * Requests are applied in the order of arrival. + * + * Neither current nor pending state are modified unless noted otherwise. + * + * The compositor must count the number of commit requests coming from + * each zwp_text_input_v3 object and use the count as the serial in done + * events. + */ +static inline void +zwp_text_input_v3_commit(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Set the actions available for this text input. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request. + * + * If the available_actions array contains the none action, or contains the + * same action multiple times, the compositor must raise the invalid_action + * protocol error. + * + * Initially, no actions are available. + */ +static inline void +zwp_text_input_v3_set_available_actions(struct zwp_text_input_v3 *zwp_text_input_v3, struct wl_array *available_actions) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_AVAILABLE_ACTIONS, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, available_actions); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Requests an input panel to be shown (e.g. a on-screen keyboard). + * + * This request only hints the desired interaction pattern from the + * client side, and its effect may be ignored by compositors given + * other environmental factors. Repeated calls will be ignored. + */ +static inline void +zwp_text_input_v3_show_input_panel(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SHOW_INPUT_PANEL, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Requests an input panel to be hidden. + * + * This request only hints the desired interaction pattern from the + * client side, and its effect may be ignored by compositors given + * other environmental factors. Repeated calls will be ignored. + */ +static inline void +zwp_text_input_v3_hide_input_panel(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_HIDE_INPUT_PANEL, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0); +} + +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT 1 + + +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1 + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void +zwp_text_input_manager_v3_set_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_manager_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void * +zwp_text_input_manager_v3_get_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_manager_v3); +} + +static inline uint32_t +zwp_text_input_manager_v3_get_version(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Destroy the wp_text_input_manager object. + */ +static inline void +zwp_text_input_manager_v3_destroy(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Creates a new text-input object for a given seat. + */ +static inline struct zwp_text_input_v3 * +zwp_text_input_manager_v3_get_text_input(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT, &zwp_text_input_v3_interface, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), 0, NULL, seat); + + return (struct zwp_text_input_v3 *) id; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lib/text-input-unstable-v3-protocol.c b/lib/text-input-unstable-v3-protocol.c new file mode 100644 index 0000000..053ebf3 --- /dev/null +++ b/lib/text-input-unstable-v3-protocol.c @@ -0,0 +1,103 @@ +/* Generated by wayland-scanner 1.26.0 */ + +/* + * Copyright © 2012, 2013 Intel Corporation + * Copyright © 2015, 2016 Jan Arne Petersen + * Copyright © 2017, 2018 Red Hat, Inc. + * Copyright © 2018 Purism SPC + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface zwp_text_input_v3_interface; + +static const struct wl_interface *text_input_unstable_v3_types[] = { + NULL, + NULL, + NULL, + NULL, + &wl_surface_interface, + &wl_surface_interface, + &zwp_text_input_v3_interface, + &wl_seat_interface, +}; + +static const struct wl_message zwp_text_input_v3_requests[] = { + { "destroy", "", text_input_unstable_v3_types + 0 }, + { "enable", "", text_input_unstable_v3_types + 0 }, + { "disable", "", text_input_unstable_v3_types + 0 }, + { "set_surrounding_text", "sii", text_input_unstable_v3_types + 0 }, + { "set_text_change_cause", "u", text_input_unstable_v3_types + 0 }, + { "set_content_type", "uu", text_input_unstable_v3_types + 0 }, + { "set_cursor_rectangle", "iiii", text_input_unstable_v3_types + 0 }, + { "commit", "", text_input_unstable_v3_types + 0 }, + { "set_available_actions", "2a", text_input_unstable_v3_types + 0 }, + { "show_input_panel", "2", text_input_unstable_v3_types + 0 }, + { "hide_input_panel", "2", text_input_unstable_v3_types + 0 }, +}; + +static const struct wl_message zwp_text_input_v3_events[] = { + { "enter", "o", text_input_unstable_v3_types + 4 }, + { "leave", "o", text_input_unstable_v3_types + 5 }, + { "preedit_string", "?sii", text_input_unstable_v3_types + 0 }, + { "commit_string", "?s", text_input_unstable_v3_types + 0 }, + { "delete_surrounding_text", "uu", text_input_unstable_v3_types + 0 }, + { "done", "u", text_input_unstable_v3_types + 0 }, + { "action", "2uu", text_input_unstable_v3_types + 0 }, + { "language", "2s", text_input_unstable_v3_types + 0 }, + { "preedit_hint", "2uuu", text_input_unstable_v3_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_v3_interface = { + "zwp_text_input_v3", 2, + 11, zwp_text_input_v3_requests, + 9, zwp_text_input_v3_events, +}; + +static const struct wl_message zwp_text_input_manager_v3_requests[] = { + { "destroy", "", text_input_unstable_v3_types + 0 }, + { "get_text_input", "no", text_input_unstable_v3_types + 6 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_manager_v3_interface = { + "zwp_text_input_manager_v3", 2, + 2, zwp_text_input_manager_v3_requests, + 0, NULL, +}; + diff --git a/project.cpp b/project.cpp index 92643c0..643a44e 100644 --- a/project.cpp +++ b/project.cpp @@ -89,6 +89,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.cFiles.push_back("lib/wayland-xdg-decoration-unstable-v1-client-protocol"); cfg.cFiles.push_back("lib/fractional-scale-v1"); cfg.cFiles.push_back("lib/viewporter"); + cfg.cFiles.push_back("lib/text-input-unstable-v3-protocol"); } // Vulkan is the only renderer on native targets. Software fallback is From 0d8287c1228c188f772fd1874c6af8ecd27c71bd Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 01:51:20 +0200 Subject: [PATCH 07/11] feat(wayland): Window::SetAppId xdg_toplevel.set_app_id, so the compositor can match the window to its .desktop file: the icon, the task switcher, activation. --- implementations/Crafter.Graphics-Window.cpp | 6 ++++++ interfaces/Crafter.Graphics-Window.cppm | 3 +++ 2 files changed, 9 insertions(+) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 5bd2045..0e3fdf5 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -620,6 +620,12 @@ void Window::SetTitle(const std::string_view title) { #endif } +void Window::SetAppId(const std::string_view id) { + #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND + xdg_toplevel_set_app_id(xdgToplevel, std::string(id).c_str()); + #endif +} + void Window::SetTextInput(bool active, std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, TextInputPurpose purpose) { #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND textInputActive_ = active; diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index 7fd4276..b4e4176 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -157,6 +157,9 @@ export namespace Crafter { void StartUpdate(); void StopUpdate(); void SetTitle(const std::string_view title); + // The xdg app id, which the compositor matches to a .desktop file + // for the icon, the task switcher and activation. + void SetAppId(const std::string_view id); // Tells the compositor's input method that a text field has focus // (an on-screen keyboard opens) and where its caret is, in physical // pixels, or that none has. Remembered: it is applied again whenever From cc04d4e1211087eb300a97309477caa81d0f2797 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 02:06:03 +0200 Subject: [PATCH 08/11] fix(wayland): constant-initialised statics in Device, for clang 23.1 on aarch64-musl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the touch positions in an inline static std::unordered_map and the text input's strings in inline static std::strings, clang 23.1 crashed compiling Device.cpp for aarch64-alpine-linux-musl against that sysroot's libc++ 22 — in LazyCallGraph at -O3, in the ThinLTO bitcode writer at -O0 (a dangling global); the native build was fine. The fingers now live in a constant-initialised array of slots and the strings in function-local statics of the implementation unit; nothing else changes. --- implementations/Crafter.Graphics-Device.cpp | 85 ++++++++++++++++----- implementations/Crafter.Graphics-Window.cpp | 2 +- interfaces/Crafter.Graphics-Device.cppm | 16 ++-- 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index b8d0f15..8258218 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -338,6 +338,44 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name } } +namespace { + // A finger's slot by id, or a free one for a new finger; nullptr with more + // than ten fingers down. + Crafter::Device::TouchSlot* SlotFor(std::int32_t id) { + Crafter::Device::TouchSlot* free = nullptr; + for (Crafter::Device::TouchSlot& slot : Crafter::Device::touchSlots) { + if (slot.used && slot.id == id) return &slot; + if (!slot.used && free == nullptr) free = &slot; + } + if (free != nullptr) { + free->used = true; + free->id = id; + } + return free; + } + + bool AnyTouch() { + for (const Crafter::Device::TouchSlot& slot : Crafter::Device::touchSlots) { + if (slot.used) return true; + } + return false; + } + + // The text input's pending strings live here, as function-local statics: + // an inline static std::string in the interface made clang 23.1 crash + // in ThinLTO for aarch64-alpine-linux-musl. + struct TextInputStrings { + std::string preedit; + std::string shown; // what the window was last told + std::string commit; + }; + + TextInputStrings& TextStrings() { + static TextInputStrings strings; + return strings; + } +} + void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) { textInputWindow = nullptr; for (Window* window : windows) { @@ -349,17 +387,17 @@ void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) { void Device::text_input_leave(void*, zwp_text_input_v3*, wl_surface*) { textInputWindow = nullptr; textInputHasPreedit = textInputHasCommit = textInputHasDelete = false; - textInputPreedit.clear(); - textInputShownPreedit.clear(); + TextStrings().preedit.clear(); + TextStrings().shown.clear(); } void Device::text_input_preedit(void*, zwp_text_input_v3*, const char* text, std::int32_t, std::int32_t) { - textInputPreedit = text == nullptr ? "" : text; + TextStrings().preedit = text == nullptr ? "" : text; textInputHasPreedit = true; } void Device::text_input_commit(void*, zwp_text_input_v3*, const char* text) { - textInputCommit = text == nullptr ? "" : text; + TextStrings().commit = text == nullptr ? "" : text; textInputHasCommit = true; } @@ -375,17 +413,18 @@ void Device::text_input_delete(void*, zwp_text_input_v3*, std::uint32_t before, // the window hears about it only when that differs from what it shows. void Device::text_input_done(void*, zwp_text_input_v3*, std::uint32_t) { Window* window = textInputWindow; - if (!textInputHasPreedit) textInputPreedit.clear(); + TextInputStrings& strings = TextStrings(); + if (!textInputHasPreedit) strings.preedit.clear(); if (window != nullptr) { if (textInputHasDelete && (textInputDeleteBefore > 0 || textInputDeleteAfter > 0)) window->onTextInputDelete.Invoke({ textInputDeleteBefore, textInputDeleteAfter }); - if (textInputHasCommit && !textInputCommit.empty()) window->onTextInput.Invoke(textInputCommit); - if (textInputPreedit != textInputShownPreedit || (textInputHasCommit && !textInputCommit.empty())) { - textInputShownPreedit = textInputPreedit; - window->onPreedit.Invoke(textInputPreedit); + if (textInputHasCommit && !strings.commit.empty()) window->onTextInput.Invoke(strings.commit); + if (strings.preedit != strings.shown || (textInputHasCommit && !strings.commit.empty())) { + strings.shown = strings.preedit; + window->onPreedit.Invoke(strings.preedit); } } textInputHasPreedit = textInputHasCommit = textInputHasDelete = false; - textInputCommit.clear(); + strings.commit.clear(); } void Device::ApplyTextInput(Window* window) { @@ -626,6 +665,10 @@ void Device::TickKeyRepeats() { } } +void Device::ForgetTouches() { + for (TouchSlot& slot : touchSlots) slot = TouchSlot{}; +} + // The first finger stands in for the pointer: it enters the window (so the // clipboard has a serial and Device::focusedWindow a window), presses the // left button, moves it and releases it. Every finger is also reported as @@ -639,7 +682,10 @@ void Device::touch_down(void*, wl_touch*, std::uint32_t serial, std::uint32_t, w touchWindow = window; window->lastPointerSerial_ = serial; const Vector pos = Vector(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale; - touchPositions[id] = { pos.x, pos.y }; + if (TouchSlot* slot = SlotFor(id)) { + slot->x = pos.x; + slot->y = pos.y; + } window->onTouchDown.Invoke({ id, pos }); if (window->primaryTouchId < 0) { window->primaryTouchId = id; @@ -653,23 +699,28 @@ void Device::touch_down(void*, wl_touch*, std::uint32_t serial, std::uint32_t, w void Device::touch_up(void*, wl_touch*, std::uint32_t, std::uint32_t, std::int32_t id) { if (touchWindow == nullptr) return; Window* window = touchWindow; - const auto at = touchPositions.find(id); - const Vector pos = at == touchPositions.end() ? window->currentMousePos : Vector(at->second.first, at->second.second); - touchPositions.erase(id); + Vector pos = window->currentMousePos; + if (TouchSlot* slot = SlotFor(id)) { + pos = Vector(slot->x, slot->y); + *slot = TouchSlot{}; + } window->onTouchUp.Invoke({ id, pos }); if (id == window->primaryTouchId) { window->primaryTouchId = -1; window->mouseLeftHeld = false; window->onMouseLeftRelease.Invoke(); } - if (touchPositions.empty()) touchWindow = nullptr; + if (!AnyTouch()) touchWindow = nullptr; } void Device::touch_motion(void*, wl_touch*, std::uint32_t, std::int32_t id, wl_fixed_t x, wl_fixed_t y) { if (touchWindow == nullptr) return; Window* window = touchWindow; const Vector pos = Vector(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale; - touchPositions[id] = { pos.x, pos.y }; + if (TouchSlot* slot = SlotFor(id)) { + slot->x = pos.x; + slot->y = pos.y; + } window->onTouchMove.Invoke({ id, pos }); if (id == window->primaryTouchId) { window->currentMousePos = pos; @@ -682,7 +733,7 @@ void Device::touch_frame(void*, wl_touch*) {} void Device::touch_cancel(void*, wl_touch*) { if (touchWindow == nullptr) return; Window* window = touchWindow; - touchPositions.clear(); + ForgetTouches(); touchWindow = nullptr; if (window->primaryTouchId >= 0) { window->primaryTouchId = -1; diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 0e3fdf5..483d9ee 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -560,7 +560,7 @@ Window::~Window() { if (Device::keyboardWindow == this) Device::keyboardWindow = nullptr; if (Device::touchWindow == this) { Device::touchWindow = nullptr; - Device::touchPositions.clear(); + Device::ForgetTouches(); } if (Device::textInputWindow == this) Device::textInputWindow = nullptr; #endif diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index cc4ea3c..7fbad00 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -62,8 +62,17 @@ export namespace Crafter { inline static std::vector windows; inline static wl_pointer* wlPointer; inline static wl_touch* wlTouch = nullptr; - inline static Window* touchWindow = nullptr; // the window under the fingers - inline static std::unordered_map> touchPositions; // per finger, physical px + inline static Window* touchWindow = nullptr; // the window under the fingers + // The fingers down, by the compositor's id, in physical pixels. Plain + // data: the statics here are constant-initialised on purpose. + struct TouchSlot { + std::int32_t id; + float x; + float y; + bool used; + }; + inline static std::array touchSlots{}; + static void ForgetTouches(); // wl_data_device_manager + wl_data_device drive copy/paste. Bound // lazily in handle_global; the data device is created once both // the manager and the seat are present (registry binding order @@ -83,9 +92,6 @@ export namespace Crafter { inline static zwp_text_input_manager_v3* textInputManager = nullptr; inline static zwp_text_input_v3* textInput = nullptr; inline static Window* textInputWindow = nullptr; - inline static std::string textInputPreedit; - inline static std::string textInputShownPreedit; // what the window was last told - inline static std::string textInputCommit; inline static std::uint32_t textInputDeleteBefore = 0; inline static std::uint32_t textInputDeleteAfter = 0; inline static bool textInputHasPreedit = false; From c8b5739e1a6c258f74a899dbee0edc7aa072fcc0 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 03:27:50 +0200 Subject: [PATCH 09/11] fix(window): the destructor cancels the frame callback; a window without a display or swapchain wl_surface_frame_done re-armed the frame callback through its local and left the member stale, so the destructor could not cancel it and a done already queued would dispatch into the freed window; the member now names the live callback and the destructor destroys it. The destructor also flushed a null display for a window created without Device::Initialize (tests do that) and freed never-created command buffers (a default-constructed Window after Initialize); both guarded. And wp_scale was one inline static for every window, which the destructor destroyed on behalf of all of them: it is a member now, like the viewport. --- implementations/Crafter.Graphics-Window.cpp | 18 ++++++++++-------- interfaces/Crafter.Graphics-Window.cppm | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 483d9ee..9603065 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -521,7 +521,7 @@ Window::~Window() { // Nothing may still run against the semaphores, fences or images below. if (Device::device != VK_NULL_HANDLE) vkDeviceWaitIdle(Device::device); if (Device::device != VK_NULL_HANDLE) { - vkFreeCommandBuffers(Device::device, Device::commandPool, numFrames, drawCmdBuffers); + if (drawCmdBuffers[0] != VK_NULL_HANDLE) vkFreeCommandBuffers(Device::device, Device::commandPool, numFrames, drawCmdBuffers); for (std::uint8_t i = 0; i < numFrames; i++) { if (renderFinishedSemaphores[i]) vkDestroySemaphore(Device::device, renderFinishedSemaphores[i], nullptr); if (waitFences[i]) vkDestroyFence(Device::device, waitFences[i], nullptr); @@ -537,9 +537,10 @@ Window::~Window() { #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND SetDefaultCursor(); - // `cb` is not destroyed here: wl_surface_frame_done frees the callback it - // was called with and re-arms through its own local, so the member may - // already name a freed one. + // A pending frame callback would still dispatch into this window; + // destroying the proxy makes libwayland drop its event. + if (cb) wl_callback_destroy(cb); + cb = nullptr; if (wpViewport) wp_viewport_destroy(wpViewport); if (wp_scale) { wp_fractional_scale_v1_destroy(wp_scale); @@ -554,7 +555,7 @@ Window::~Window() { xdgToplevel = nullptr; xdgSurface = nullptr; surface = nullptr; - wl_display_flush(Device::display); + if (Device::display) wl_display_flush(Device::display); std::erase(Device::windows, this); if (Device::focusedWindow == this) Device::focusedWindow = nullptr; if (Device::keyboardWindow == this) Device::keyboardWindow = nullptr; @@ -1299,12 +1300,13 @@ void Window::wl_surface_frame_done(void* data, struct wl_callback *cb, uint32_t wl_callback_destroy(cb); Window* window = reinterpret_cast(data); + // The member names the live callback, so the destructor can cancel it. if(window->updating) { - cb = wl_surface_frame(window->surface); - wl_callback_add_listener(cb, &Window::wl_callback_listener, window); + window->cb = wl_surface_frame(window->surface); + wl_callback_add_listener(window->cb, &Window::wl_callback_listener, window); window->Update(); } else { - cb = nullptr; + window->cb = nullptr; } } diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index b4e4176..03a5710 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -256,7 +256,7 @@ export namespace Crafter { constexpr static wp_fractional_scale_v1_listener wp_fractional_scale_v1_listener = { .preferred_scale = xdg_surface_handle_preferred_scale, }; - inline static wp_fractional_scale_v1* wp_scale = nullptr; + wp_fractional_scale_v1* wp_scale = nullptr; #endif #ifndef CRAFTER_GRAPHICS_WINDOW_DOM @@ -298,7 +298,7 @@ export namespace Crafter { // lazily in Render() (post-acquire) rather than up front. std::array imageInitialised{}; std::thread thread; - VkCommandBuffer drawCmdBuffers[numFrames]; + VkCommandBuffer drawCmdBuffers[numFrames] = {}; // Per-frame Vulkan info structs whose contents are almost entirely // invariant. Initialised once in the constructor; Render() patches only // the few fields that change each frame (the two barriers' image and From 1498a5bf4b75846698b4b959ce69dd8a1383b113 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 03:28:02 +0200 Subject: [PATCH 10/11] fix(wayland): text input enables once per focus; touches route by their finger's window enable resets the input method's state, so sending it with every caret move (an application keeps the cursor rectangle current while typing) dropped the composition at each keystroke: enable now goes once per focus and a moved caret sends its rectangle alone; disable only takes back an enable. The compositor's leave ends both, and a window still showing composing text hears onPreedit("") then. A finger's up and motion go to the window it went down on, not the window of the latest down. The seat creates its pointer and keyboard once, as it already did the touch: capabilities come again when a device appears or goes, and a second keyboard proxy delivered every key twice. --- implementations/Crafter.Graphics-Device.cpp | 38 +++++++++++++++------ interfaces/Crafter.Graphics-Device.cppm | 3 ++ interfaces/Crafter.Graphics-Window.cppm | 1 + 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/implementations/Crafter.Graphics-Device.cpp b/implementations/Crafter.Graphics-Device.cpp index 8258218..f9414c6 100644 --- a/implementations/Crafter.Graphics-Device.cpp +++ b/implementations/Crafter.Graphics-Device.cpp @@ -350,6 +350,7 @@ namespace { if (free != nullptr) { free->used = true; free->id = id; + free->window = nullptr; } return free; } @@ -384,10 +385,17 @@ void Device::text_input_enter(void*, zwp_text_input_v3*, wl_surface* surface) { if (textInputWindow != nullptr && textInputWindow->textInputActive_) ApplyTextInput(textInputWindow); } +// The compositor's leave ends the enable and the composition: the window +// hears that its composing text is gone, and the next enter enables anew. void Device::text_input_leave(void*, zwp_text_input_v3*, wl_surface*) { + Window* window = textInputWindow; textInputWindow = nullptr; textInputHasPreedit = textInputHasCommit = textInputHasDelete = false; TextStrings().preedit.clear(); + if (window != nullptr) { + window->textInputEnabled_ = false; + if (!TextStrings().shown.empty()) window->onPreedit.Invoke(std::string_view()); + } TextStrings().shown.clear(); } @@ -427,10 +435,13 @@ void Device::text_input_done(void*, zwp_text_input_v3*, std::uint32_t) { strings.commit.clear(); } +// enable resets the input method's state, so it goes once per focus; a +// caret that moved sends its rectangle alone. void Device::ApplyTextInput(Window* window) { if (textInput == nullptr || window == nullptr || textInputWindow != window) return; if (window->textInputActive_) { - zwp_text_input_v3_enable(textInput); + if (!window->textInputEnabled_) zwp_text_input_v3_enable(textInput); + window->textInputEnabled_ = true; const bool phone = window->textInputPurpose_ == TextInputPurpose::Phone; zwp_text_input_v3_set_content_type(textInput, phone ? ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE : (ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION | ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK | ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION), @@ -440,6 +451,8 @@ void Device::ApplyTextInput(Window* window) { static_cast(window->textInputRect_[0] / s), static_cast(window->textInputRect_[1] / s), std::max(1, static_cast(window->textInputRect_[2] / s)), std::max(1, static_cast(window->textInputRect_[3] / s))); } else { + if (!window->textInputEnabled_) return; + window->textInputEnabled_ = false; zwp_text_input_v3_disable(textInput); } zwp_text_input_v3_commit(textInput); @@ -685,6 +698,7 @@ void Device::touch_down(void*, wl_touch*, std::uint32_t serial, std::uint32_t, w if (TouchSlot* slot = SlotFor(id)) { slot->x = pos.x; slot->y = pos.y; + slot->window = window; } window->onTouchDown.Invoke({ id, pos }); if (window->primaryTouchId < 0) { @@ -701,6 +715,7 @@ void Device::touch_up(void*, wl_touch*, std::uint32_t, std::uint32_t, std::int32 Window* window = touchWindow; Vector pos = window->currentMousePos; if (TouchSlot* slot = SlotFor(id)) { + if (slot->window != nullptr) window = slot->window; pos = Vector(slot->x, slot->y); *slot = TouchSlot{}; } @@ -716,8 +731,10 @@ void Device::touch_up(void*, wl_touch*, std::uint32_t, std::uint32_t, std::int32 void Device::touch_motion(void*, wl_touch*, std::uint32_t, std::int32_t id, wl_fixed_t x, wl_fixed_t y) { if (touchWindow == nullptr) return; Window* window = touchWindow; + TouchSlot* slot = SlotFor(id); + if (slot != nullptr && slot->window != nullptr) window = slot->window; const Vector pos = Vector(wl_fixed_to_double(x), wl_fixed_to_double(y)) * window->scale; - if (TouchSlot* slot = SlotFor(id)) { + if (slot != nullptr) { slot->x = pos.x; slot->y = pos.y; } @@ -743,19 +760,20 @@ void Device::touch_cancel(void*, wl_touch*) { window->onTouchCancel.Invoke(); } -void Device::seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities) { - seat = seat; - if (capabilities & WL_SEAT_CAPABILITY_POINTER) { - wl_pointer* pointer = wl_seat_get_pointer(seat); - wl_pointer_add_listener(pointer, &pointer_listener, nullptr); +// Capabilities come again when a device appears or goes; each object is +// created once, or every event would arrive once per copy. +void Device::seat_handle_capabilities(void*, wl_seat* seat, uint32_t capabilities) { + if ((capabilities & WL_SEAT_CAPABILITY_POINTER) && seatPointer == nullptr) { + seatPointer = wl_seat_get_pointer(seat); + wl_pointer_add_listener(seatPointer, &pointer_listener, nullptr); } if ((capabilities & WL_SEAT_CAPABILITY_TOUCH) && wlTouch == nullptr) { wlTouch = wl_seat_get_touch(seat); wl_touch_add_listener(wlTouch, &touch_listener, nullptr); } - if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { - wl_keyboard* keyboard = wl_seat_get_keyboard(seat); - wl_keyboard_add_listener(keyboard, &keyboard_listener, nullptr); + if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) && seatKeyboard == nullptr) { + seatKeyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(seatKeyboard, &keyboard_listener, nullptr); } } diff --git a/interfaces/Crafter.Graphics-Device.cppm b/interfaces/Crafter.Graphics-Device.cppm index 7fbad00..30e7959 100644 --- a/interfaces/Crafter.Graphics-Device.cppm +++ b/interfaces/Crafter.Graphics-Device.cppm @@ -61,6 +61,8 @@ export namespace Crafter { inline static xkb_state* xkb_state; inline static std::vector windows; inline static wl_pointer* wlPointer; + inline static wl_pointer* seatPointer = nullptr; + inline static wl_keyboard* seatKeyboard = nullptr; inline static wl_touch* wlTouch = nullptr; inline static Window* touchWindow = nullptr; // the window under the fingers // The fingers down, by the compositor's id, in physical pixels. Plain @@ -70,6 +72,7 @@ export namespace Crafter { float x; float y; bool used; + Window* window; }; inline static std::array touchSlots{}; static void ForgetTouches(); diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index 03a5710..1426cd0 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -234,6 +234,7 @@ export namespace Crafter { // mid-session (the hotspot only updates when set_cursor is recalled). std::uint32_t lastPointerSerial_ = 0; bool textInputActive_ = false; + bool textInputEnabled_ = false; // enable sent and not taken back since std::int32_t textInputRect_[4] = { 0, 0, 0, 0 }; TextInputPurpose textInputPurpose_ = TextInputPurpose::Normal; From 1c30b1bbf7319cbe2bcc163c7cfab61be0425931 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 18 Oct 2026 03:28:11 +0200 Subject: [PATCH 11/11] fix(font-atlas): wait for the GPU before the image goes; DOM stubs for SetAppId and SetTextInput ~FontAtlas destroyed the image while frames that sample it could still run (HelloUI declares the atlas after the window, so it goes first); it waits on the queue now, as the descriptor heap does. The DOM build got the two new Window methods as no-ops, like the other backend-specific ones. --- implementations/Crafter.Graphics-Window.cpp | 4 ++++ interfaces/Crafter.Graphics-FontAtlas.cppm | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 9603065..e26022c 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -1640,6 +1640,10 @@ void Window::SetTitle(const std::string_view title) { Crafter::DomEnv::domSetTitle(title.data(), static_cast(title.size())); } +void Window::SetAppId(const std::string_view) {} + +void Window::SetTextInput(bool, std::int32_t, std::int32_t, std::int32_t, std::int32_t, TextInputPurpose) {} + void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) { if (newWidth == 0 || newHeight == 0) return; if (newWidth == width && newHeight == height) return; diff --git a/interfaces/Crafter.Graphics-FontAtlas.cppm b/interfaces/Crafter.Graphics-FontAtlas.cppm index 732455d..f5f2ea1 100644 --- a/interfaces/Crafter.Graphics-FontAtlas.cppm +++ b/interfaces/Crafter.Graphics-FontAtlas.cppm @@ -100,11 +100,13 @@ export namespace Crafter { FontAtlas() = default; FontAtlas(const FontAtlas&) = delete; FontAtlas& operator=(const FontAtlas&) = delete; - // The image goes with the atlas. The owner waits for the GPU first - // and, in sets mode, destroys the heap holding the image's view before - // the atlas. + // The image goes with the atlas, once the GPU is done with it; in + // sets mode the owner destroys the heap holding the image's view + // before the atlas. ~FontAtlas() { - if (image.image != VK_NULL_HANDLE) image.Destroy(); + if (image.image == VK_NULL_HANDLE) return; + if (Device::queue != VK_NULL_HANDLE) vkQueueWaitIdle(Device::queue); + image.Destroy(); } #endif