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] 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;