From 1116bd21567314a31c98f55b0cc2b02e36b3735d Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:04:47 +0200 Subject: [PATCH 01/17] copy from wayland branch --- Engine/gapi/vulkan/vswapchain.cpp | 37 +- Engine/gapi/vulkanapi.cpp | 37 +- Engine/system/api/waylandapi.cpp | 1313 +++++++++++++++++++++++++++++ Engine/system/api/waylandapi.h | 52 ++ Engine/system/systemapi.cpp | 14 +- 5 files changed, 1433 insertions(+), 20 deletions(-) create mode 100644 Engine/system/api/waylandapi.cpp create mode 100644 Engine/system/api/waylandapi.h diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index 1edfe1aa..01f259aa 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -10,6 +10,7 @@ #if defined(__UNIX__) #include "system/api/x11api.h" +#include "system/api/waylandapi.h" #endif #if defined(__WINDOWS__) @@ -20,6 +21,10 @@ # include # include #elif defined(__UNIX__) +# if defined(TEMPEST_BUILD_WAYLAND) +# define VK_USE_PLATFORM_WAYLAND_KHR +# include +# endif # define VK_USE_PLATFORM_XLIB_KHR # include # include @@ -241,13 +246,17 @@ bool VSwapchain::checkPresentationSupport(VkPhysicalDevice device, uint32_t queu (void)queueFamilyIndex; return true; #elif defined(__UNIX__) - bool presentSupport = false; +#if defined(TEMPEST_BUILD_WAYLAND) + if(auto dpy = WaylandApi::display()){ + return vkGetPhysicalDeviceWaylandPresentationSupportKHR(device,queueFamilyIndex,dpy)!=VK_FALSE; + } +#endif if(auto dpy = reinterpret_cast(X11Api::display())){ auto screen = DefaultScreen(dpy); auto visualId = XVisualIDFromVisual(DefaultVisual(dpy,screen)); - presentSupport = vkGetPhysicalDeviceXlibPresentationSupportKHR(device,queueFamilyIndex,dpy,visualId)!=VK_FALSE; + return vkGetPhysicalDeviceXlibPresentationSupportKHR(device,queueFamilyIndex,dpy,visualId)!=VK_FALSE; } - return presentSupport; + return false; #else # warning "wsi for vulkan not implemented on this platform" return false; @@ -267,11 +276,23 @@ VkResult VSwapchain::createSurface(VkInstance instance, void* hwnd, VkSurfaceKHR createInfo.window = reinterpret_cast(hwnd); return vkCreateAndroidSurfaceKHR(instance,&createInfo,nullptr,pSurface); #elif defined(__UNIX__) - VkXlibSurfaceCreateInfoKHR createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; - createInfo.dpy = reinterpret_cast(X11Api::display()); - createInfo.window = ::Window(hwnd); - return vkCreateXlibSurfaceKHR(instance, &createInfo, nullptr, pSurface); +#if defined(TEMPEST_BUILD_WAYLAND) + if(auto dpy = WaylandApi::display()) { + VkWaylandSurfaceCreateInfoKHR createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR; + createInfo.display = dpy; + createInfo.surface = WaylandApi::surface(reinterpret_cast(hwnd)); + return vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, pSurface); + } +#endif + if(auto dpy = reinterpret_cast(X11Api::display())) { + VkXlibSurfaceCreateInfoKHR createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR; + createInfo.dpy = dpy; + createInfo.window = ::Window(hwnd); + return vkCreateXlibSurfaceKHR(instance, &createInfo, nullptr, pSurface); + } + return VK_ERROR_UNKNOWN; #else # warning "wsi for vulkan not implemented on this platform" return VK_ERROR_UNKNOWN; diff --git a/Engine/gapi/vulkanapi.cpp b/Engine/gapi/vulkanapi.cpp index 5edcd9d6..682476cf 100644 --- a/Engine/gapi/vulkanapi.cpp +++ b/Engine/gapi/vulkanapi.cpp @@ -19,21 +19,18 @@ #include +#if defined(__UNIX__) +#include "system/api/waylandapi.h" +#endif + using namespace Tempest; using namespace Tempest::Detail; -#define VK_KHR_WIN32_SURFACE_EXTENSION_NAME "VK_KHR_win32_surface" -#define VK_KHR_XLIB_SURFACE_EXTENSION_NAME "VK_KHR_xlib_surface" +#define VK_KHR_WIN32_SURFACE_EXTENSION_NAME "VK_KHR_win32_surface" +#define VK_KHR_XLIB_SURFACE_EXTENSION_NAME "VK_KHR_xlib_surface" +#define VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME "VK_KHR_wayland_surface" #define VK_KHR_ANDROID_SURFACE_EXTENSION_NAME "VK_KHR_android_surface" -#if defined(__WINDOWS__) -#define SURFACE_EXTENSION_NAME VK_KHR_WIN32_SURFACE_EXTENSION_NAME -#elif defined(__ANDROID__) -#define SURFACE_EXTENSION_NAME VK_KHR_ANDROID_SURFACE_EXTENSION_NAME -#elif defined(__UNIX__) -#define SURFACE_EXTENSION_NAME VK_KHR_XLIB_SURFACE_EXTENSION_NAME -#endif - static const std::initializer_list validationLayersKHR = { "VK_LAYER_KHRONOS_validation" }; @@ -90,6 +87,24 @@ static std::vector instExtensionsList() { return ext; } +// surface extension of the window system +// on linux: wayland unless TEMPEST_DISABLE_WAYLAND=1, the same decision as SystemApi::inst() +static const char* surfaceExtensionName() { +#if defined(__WINDOWS__) + return VK_KHR_WIN32_SURFACE_EXTENSION_NAME; +#elif defined(__ANDROID__) + return VK_KHR_ANDROID_SURFACE_EXTENSION_NAME; +#elif defined(__UNIX__) +#if defined(TEMPEST_BUILD_WAYLAND) + if(!WaylandApi::isDisabled()) + return VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME; +#endif + return VK_KHR_XLIB_SURFACE_EXTENSION_NAME; +#else +#error "WSI is not implemented on this platform" +#endif + } + struct Tempest::VulkanApi::Impl { Impl(bool validation) @@ -121,7 +136,7 @@ struct Tempest::VulkanApi::Impl { std::vector rqExt = { VK_EXT_DEBUG_REPORT_EXTENSION_NAME, VK_KHR_SURFACE_EXTENSION_NAME, - SURFACE_EXTENSION_NAME, + surfaceExtensionName(), }; auto ext = instExtensionsList(); diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp new file mode 100644 index 00000000..bb1e1d5b --- /dev/null +++ b/Engine/system/api/waylandapi.cpp @@ -0,0 +1,1313 @@ +#include "waylandapi.h" + +#if defined(TEMPEST_BUILD_WAYLAND) +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "xdg-shell-client-protocol.h" +#include "xdg-decoration-unstable-v1-client-protocol.h" +#include "viewporter-client-protocol.h" +#include "fractional-scale-v1-client-protocol.h" +#include "cursor-shape-v1-client-protocol.h" +#include "pointer-constraints-unstable-v1-client-protocol.h" +#include "relative-pointer-unstable-v1-client-protocol.h" + +using namespace Tempest; + +static wl_display* dpy = nullptr; // non-null while Wayland is the active backend, written only by constructor/destructor +static std::atomic_bool isExit{0}; + +//// +/// WINDOW STATE +// + +// Per-window state. The SystemApi::Window* handed to Tempest points to this struct. +struct WWindow { + Tempest::Window* owner = nullptr; + + wl_surface* surface = nullptr; + xdg_surface* xdgSurface = nullptr; + xdg_toplevel* toplevel = nullptr; + zxdg_toplevel_decoration_v1* decoration = nullptr; + wp_viewport* viewport = nullptr; + wp_fractional_scale_v1* fractionalScale = nullptr; + + // Announced by xdg_toplevel.configure, becomes current on xdg_surface.configure + struct Pending { + int32_t width = 0; // logical; 0 = no size from the compositor yet + int32_t height = 0; + bool fullscreen = false; + bool suspended = false; + } pending; + + // Current state + int32_t width = 0; // logical + int32_t height = 0; + uint32_t preferredScale = 120; // wp_fractional_scale_v1: scale * 120 + bool fullscreen = false; + bool suspended = false; // compositor doesn't repaint the window (e.g. minimized): pause rendering + bool configured = false; // first configure acked; no buffer (= Vulkan present) before that + + int32_t appRequestedWidth = 0; // physical; used until the compositor sends a size + int32_t appRequestedHeight = 0; + + // size the app last saw; updateState() dispatches a resize when it differs. + int32_t dispatchedWidth = 0; // physical + int32_t dispatchedHeight = 0; + + // Pointer lock for mouse-look, requested while the window's cursor is hidden (see Private::setPointerLock). + zwp_locked_pointer_v1* lockedPointer = nullptr; + bool pointerLocked = false; + + // Cursor position. Button and axis events carry none. While pointer is locked, this is a + // virtual cursor moved by relative motion. + double cursorX = 0; // physical + double cursorY = 0; + + bool readyToRender() const { return owner!=nullptr && configured && !suspended; } + + float scale() const { return float(preferredScale)/120.f; } + // Physical size, as Tempest sees it (client rect, resize events, swapchain extent). Rounded half up as fractional-scale + // requires. In integers, since scales like 1.15 aren't exact as float. + int32_t physicalWidth() const { return int32_t((int64_t(width) *preferredScale + 60)/120); } + int32_t physicalHeight() const { return int32_t((int64_t(height)*preferredScale + 60)/120); } + }; + +static WWindow* toWWindow(SystemApi::Window* w) { + return reinterpret_cast(w); + } + +//// +/// SESSION STATE +// + +struct WaylandApi::Private { + wl_display* display = nullptr; + wl_registry* registry = nullptr; + + // Globals + wl_compositor* compositor = nullptr; + wl_seat* seat = nullptr; + xdg_wm_base* wmBase = nullptr; + zxdg_decoration_manager_v1* decorationManager = nullptr; + wp_viewporter* viewporter = nullptr; + wp_fractional_scale_manager_v1* fractionalScaleManager = nullptr; + wp_cursor_shape_manager_v1* cursorShapeManager = nullptr; + zwp_pointer_constraints_v1* pointerConstraints = nullptr; + zwp_relative_pointer_manager_v1* relativePointerManager = nullptr; + + // Exact versions we need to bind successfully: listeners and requests are written for these + static constexpr uint32_t wlCompositorVersion = 6; + static constexpr uint32_t wlSeatVersion = 8; // v8: wl_pointer.axis_value120 (wheel in 1/120 notches) + static constexpr uint32_t xdgWmBaseVersion = 6; // v6: xdg_toplevel state 'suspended' + static constexpr uint32_t zxdgDecorationManagerV1Version = 1; + static constexpr uint32_t wpViewporterVersion = 1; + static constexpr uint32_t wpFractionalScaleManagerV1Version = 1; + static constexpr uint32_t wpCursorShapeManagerV1Version = 1; + static constexpr uint32_t zwpPointerConstraintsV1Version = 1; + static constexpr uint32_t zwpRelativePointerManagerV1Version = 1; + + // Input devices, non-null while seat has the capability (can change at runtime) + wl_pointer* pointer = nullptr; + wp_cursor_shape_device_v1* cursorShapeDevice = nullptr; // exists together with 'pointer' + zwp_relative_pointer_v1* relativePointer = nullptr; // exists together with 'pointer' + wl_keyboard* keyboard = nullptr; + + // Pointer state between wl_pointer.enter and leave. Position is in WWindow::cursorX/Y + WWindow* pointerFocus = nullptr; // window under pointer + uint32_t pointerEnterSerial = 0; // serial of latest enter event + + // Keymap from wl_keyboard.keymap, state follows wl_keyboard.modifiers; both null until first keymap. + xkb_context* xkbContext = nullptr; + xkb_keymap* xkbKeymap = nullptr; + xkb_state* xkbState = nullptr; + WWindow* keyboardFocus = nullptr; // window with keyboard focus (wl_keyboard.enter/leave) + + // Keys (xkb keycodes) and buttons (evdev codes) whose press reached the app. Only their releases are dispatched, + // and they're released on focus or device loss (see releaseHeldKeys / releaseHeldButtons). + std::vector heldKeys; + std::vector heldButtons; + + // Key repeat: Wayland sends none. Manual implementation from wl_keyboard.repeat_info by sending the held key's KeyDown again, + // which Tempest's dispatcher turns into KeyRepeat (like on Windows). + int32_t repeatRate = 0; // repeats per second; 0 = disabled + int32_t repeatDelay = 0; // ms until the first repeat trigger + uint32_t repeatKeycode = 0; // xkb keycode of the repeating key; 0 = none. + // Non-zero only while keyboardFocus is set and repeatRate>0. + std::chrono::steady_clock::time_point repeatNext; // when the next repeat is due + + // WWindow* if a window gained focus during active readEvents() batch, otherwise nullptr (for focus click filter) + WWindow* focusGainedInBatch = nullptr; + + std::vector> windows; // owns the windows; each WWindow keeps its address (handle, listener data) + + // Session listeners (globals and seat devices). 'data' is Private*. + static void onWlRegistryGlobal(void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version); + static void onXdgWmBasePing(void* data, xdg_wm_base* wmBase, uint32_t serial); + static void onWlSeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities); + static void onWlPointerEnter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, wl_fixed_t x, wl_fixed_t y); + static void onWlPointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface); + static void onWlPointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y); + static void onWlPointerButton(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state); + static void onWlPointerAxisValue120(void* data, wl_pointer* pointer, uint32_t axis, int32_t value120); + static void onZwpRelativePointerV1RelativeMotion(void* data, zwp_relative_pointer_v1* relativePointer, uint32_t utimeHi, + uint32_t utimeLo, wl_fixed_t dx, wl_fixed_t dy, + wl_fixed_t dxUnaccel, wl_fixed_t dyUnaccel); + static void onWlKeyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size); + static void onWlKeyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface, wl_array* keys); + static void onWlKeyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface); + static void onWlKeyboardKey(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state); + static void onWlKeyboardModifiers(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t depressed, + uint32_t latched, uint32_t locked, uint32_t group); + static void onWlKeyboardRepeatInfo(void* data, wl_keyboard* keyboard, int32_t rate, int32_t delay); + + // Window specific listeners. 'data' is WWindow*. + static void onXdgSurfaceConfigure(void* data, xdg_surface* xdgSurface, uint32_t serial); + static void onXdgToplevelConfigure(void* data, xdg_toplevel* toplevel, int32_t width, int32_t height, wl_array* states); + static void onXdgToplevelClose (void* data, xdg_toplevel* toplevel); + static void onZxdgToplevelDecorationV1Configure(void* data, zxdg_toplevel_decoration_v1* decoration, uint32_t mode); + static void onWpFractionalScaleV1PreferredScale(void* data, wp_fractional_scale_v1* fractionalScale, uint32_t scale); + static void onZwpLockedPointerV1Locked (void* data, zwp_locked_pointer_v1* lockedPointer); + static void onZwpLockedPointerV1Unlocked(void* data, zwp_locked_pointer_v1* lockedPointer); + + // Listener tables (session) + static const wl_registry_listener wlRegistryListener; + static const xdg_wm_base_listener xdgWmBaseListener; + static const wl_seat_listener wlSeatListener; + static const wl_pointer_listener wlPointerListener; + static const zwp_relative_pointer_v1_listener zwpRelativePointerV1Listener; + static const wl_keyboard_listener wlKeyboardListener; + + // Listener tables (window specific) + static const wl_surface_listener wlSurfaceListener; + static const xdg_surface_listener xdgSurfaceListener; + static const xdg_toplevel_listener xdgToplevelListener; + static const zxdg_toplevel_decoration_v1_listener zxdgToplevelDecorationV1Listener; + static const wp_fractional_scale_v1_listener wpFractionalScaleV1Listener; + static const zwp_locked_pointer_v1_listener zwpLockedPointerV1Listener; + + // Helpers + + static constexpr auto ioIgnore = [](auto...) {}; // useful to silence Wayland listener stubs + template static void ioDestroy(T*& obj, void (*destroyFn)(T*)); + void logDisplayError() const; + static Event::MouseButton toButton(uint32_t button); + static wp_cursor_shape_device_v1_shape toCursorShape(CursorShape shape); + static MouseEvent pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type); + KeyEvent keyEvent(uint32_t keycode, Event::Type type) const; + bool isWindowFocused(const WWindow* w) const; + void setPointerLock(WWindow& w, bool lock); + void releaseHeldKeys(WWindow& w); + void releaseHeldButtons(WWindow& w); + void releasePointer(); + void releaseKeyboard(); + void startKeyRepeat(uint32_t keycode); + void processKeyRepeat(); + + // Connection and windows + bool hasRequiredGlobals() const; + bool connect(); + void disconnect(); + WWindow* createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm); + void destroyWindow(WWindow* w); + + // Event loop + void handleConnectionError(); + int pollTimeout() const; + void readEvents(); + static void updateState(WWindow& w); + void renderWindows(); + }; + +//// +/// LISTENERS +// + +/// Session listeners +// +void WaylandApi::Private::onWlRegistryGlobal(void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { + // Binds exactly the *Version constants; older globals are skipped. + WaylandApi::Private* self = static_cast(data); + if(std::strcmp(interface, wl_compositor_interface.name)==0 && version>=wlCompositorVersion) { + self->compositor = static_cast(wl_registry_bind(registry, name, &wl_compositor_interface, wlCompositorVersion)); + } + if(std::strcmp(interface, xdg_wm_base_interface.name)==0 && version>=xdgWmBaseVersion) { + self->wmBase = static_cast(wl_registry_bind(registry, name, &xdg_wm_base_interface, xdgWmBaseVersion)); + xdg_wm_base_add_listener(self->wmBase, &xdgWmBaseListener, self); + } + if(std::strcmp(interface, zxdg_decoration_manager_v1_interface.name)==0 && version>=zxdgDecorationManagerV1Version) { + self->decorationManager = static_cast( + wl_registry_bind(registry, name, &zxdg_decoration_manager_v1_interface, zxdgDecorationManagerV1Version)); + } + if(std::strcmp(interface, wp_viewporter_interface.name)==0 && version>=wpViewporterVersion) { + self->viewporter = static_cast(wl_registry_bind(registry, name, &wp_viewporter_interface, wpViewporterVersion)); + } + if(std::strcmp(interface, wp_fractional_scale_manager_v1_interface.name)==0 && version>=wpFractionalScaleManagerV1Version) { + self->fractionalScaleManager = static_cast( + wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, wpFractionalScaleManagerV1Version)); + } + if(std::strcmp(interface, wp_cursor_shape_manager_v1_interface.name)==0 && version>=wpCursorShapeManagerV1Version) { + self->cursorShapeManager = static_cast( + wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version)); + } + if(std::strcmp(interface, zwp_pointer_constraints_v1_interface.name)==0 && version>=zwpPointerConstraintsV1Version) { + self->pointerConstraints = static_cast( + wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version)); + } + if(std::strcmp(interface, zwp_relative_pointer_manager_v1_interface.name)==0 && version>=zwpRelativePointerManagerV1Version) { + self->relativePointerManager = static_cast( + wl_registry_bind(registry, name, &zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version)); + } + if(std::strcmp(interface, wl_seat_interface.name)==0 && version>=wlSeatVersion && self->seat==nullptr) { + // Only the first seat: multi-seat setups are rare, and Tempest has a single pointer/keyboard. + static_assert(wlSeatVersionseat = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, wlSeatVersion)); + wl_seat_add_listener(self->seat, &wlSeatListener, self); + } + } + +void WaylandApi::Private::onXdgWmBasePing([[maybe_unused]] void* data, xdg_wm_base* wmBase, uint32_t serial) { + xdg_wm_base_pong(wmBase, serial); + } + +void WaylandApi::Private::onWlSeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities) { + // Sent after binding and whenever devices come or go: get a device when its capability appears, release it when it goes. + WaylandApi::Private* self = static_cast(data); + + // The managers are bound: capabilities arrive after connect()'s roundtrip. Pointer locks follow on the next cursor + // change (always on enter). + const bool hasPointer = (capabilities & WL_SEAT_CAPABILITY_POINTER)!=0; + if(hasPointer && self->pointer==nullptr) { + self->pointer = wl_seat_get_pointer(seat); + wl_pointer_add_listener(self->pointer, &wlPointerListener, self); + self->cursorShapeDevice = wp_cursor_shape_manager_v1_get_pointer(self->cursorShapeManager, self->pointer); + self->relativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer(self->relativePointerManager, self->pointer); + zwp_relative_pointer_v1_add_listener(self->relativePointer, &zwpRelativePointerV1Listener, self); + } + else if(!hasPointer && self->pointer!=nullptr) { + self->releasePointer(); + } + + const bool hasKeyboard = (capabilities & WL_SEAT_CAPABILITY_KEYBOARD)!=0; + if(hasKeyboard && self->keyboard==nullptr) { + self->keyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(self->keyboard, &wlKeyboardListener, self); + } + else if(!hasKeyboard && self->keyboard!=nullptr) { + self->releaseKeyboard(); + } + } + +void WaylandApi::Private::onWlPointerEnter(void* data, [[maybe_unused]] wl_pointer* pointer, uint32_t serial, + wl_surface* surface, wl_fixed_t x, wl_fixed_t y) { + // Like X11's EnterNotify: a move to the entry position, then re-apply the window's cursor + auto self = static_cast(data); + if(surface==nullptr) + return; // surface was destroyed after compositor sent the event + auto w = static_cast(wl_surface_get_user_data(surface)); // listener data of our wl_surface + self->pointerFocus = w; + self->pointerEnterSerial = serial; + // logical -> physical + w->cursorX = wl_fixed_to_double(x)*w->scale(); + w->cursorY = wl_fixed_to_double(y)*w->scale(); + if(w->owner==nullptr) + return; + if(self->isWindowFocused(w)) { + MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, e); + } + // Note: Wayland always requires cursor shape on enter (see implShowCursor) + SystemApi::showCursor(reinterpret_cast(w), SystemApi::cursorShape(*w->owner)); + } + +void WaylandApi::Private::onWlPointerLeave(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] wl_surface* surface) { + // 'surface' may be null if it was destroyed meanwhile. + static_cast(data)->pointerFocus = nullptr; + } + +void WaylandApi::Private::onWlPointerMotion(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t time, + wl_fixed_t x, wl_fixed_t y) { + // While a button is held, coordinates can be outside the window (implicit grab), like on X11. Not sent while locked. + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr) + return; + w->cursorX = wl_fixed_to_double(x)*w->scale(); + w->cursorY = wl_fixed_to_double(y)*w->scale(); + if(w->owner==nullptr || !self->isWindowFocused(w)) + return; + MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, e); + } + +void WaylandApi::Private::onWlPointerButton(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] uint32_t time, uint32_t button, uint32_t state) { + // Unknown buttons become ButtonNone, like X11. Releases only for presses the app got (see heldButtons). + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr || w->owner==nullptr) + return; + + if(state==WL_POINTER_BUTTON_STATE_PRESSED) { + if(!self->isWindowFocused(w)) + return; + // Focus click filter: the click that only focuses the window must not reach the app. KWin sends + // wl_keyboard.enter and that press together, so a press in the same readEvents() batch as the enter is that click. + if(self->focusGainedInBatch==w) + return; + if(std::find(self->heldButtons.begin(), self->heldButtons.end(), button)==self->heldButtons.end()) + self->heldButtons.push_back(button); + MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseDown); + SystemApi::dispatchMouseDown(*w->owner, e); + } else { + if(std::erase(self->heldButtons, button)==0) + return; + MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseUp); + SystemApi::dispatchMouseUp(*w->owner, e); + } + } + +void WaylandApi::Private::onWlPointerAxisValue120(void* data, [[maybe_unused]] wl_pointer* pointer, uint32_t axis, int32_t value120) { + // 120 per notch, like Tempest's delta on Windows; Wayland's sign is inverted (positive = down). + // Horizontal scrolling is ignored, like on X11. + if(axis!=WL_POINTER_AXIS_VERTICAL_SCROLL) + return; + + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr || w->owner==nullptr || !self->isWindowFocused(w)) + return; + MouseEvent e = pointerEvent(*w, Event::ButtonNone, -value120, Event::MouseWheel); + SystemApi::dispatchMouseWheel(*w->owner, e); + } + +void WaylandApi::Private::onZwpRelativePointerV1RelativeMotion(void* data, [[maybe_unused]] zwp_relative_pointer_v1* relativePointer, + [[maybe_unused]] uint32_t utimeHi, [[maybe_unused]] uint32_t utimeLo, + [[maybe_unused]] wl_fixed_t dx, [[maybe_unused]] wl_fixed_t dy, + wl_fixed_t dxUnaccel, wl_fixed_t dyUnaccel) { + // Moves virtual cursor while pointer is locked (otherwise wl_pointer.motion has the position). Raw input deltas + // clamped to the window like a real cursor. + // Note: compositor only activates the lock while focused + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr || !w->pointerLocked || w->owner==nullptr) + return; + const double s = w->scale(); + w->cursorX = std::clamp(w->cursorX + wl_fixed_to_double(dxUnaccel)*s, 0.0, double(std::max(w->physicalWidth() -1, 0))); + w->cursorY = std::clamp(w->cursorY + wl_fixed_to_double(dyUnaccel)*s, 0.0, double(std::max(w->physicalHeight()-1, 0))); + MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, e); + } + +void WaylandApi::Private::onWlKeyboardKeymap(void* data, [[maybe_unused]] wl_keyboard* keyboard, + uint32_t format, int32_t fd, uint32_t size) { + // Sent on creation and when the keymap changes. On failure the previous keymap stays. + // Note: need to close fd manually inside function! + auto self = static_cast(data); + if(format!=WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + Log::e("WaylandApi: unsupported keymap format ", format); + close(fd); + return; + } + + // MAP_PRIVATE: required since wl_keyboard v7 + void* map = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if(map==MAP_FAILED) { + Log::e("WaylandApi: can't map the keymap: ", std::strerror(errno)); + return; + } + // The text is null-terminated within 'size'; strnlen makes sure we never read beyond the mapping. + const char* text = static_cast(map); + xkb_keymap* keymap = xkb_keymap_new_from_buffer(self->xkbContext, text, strnlen(text, size), + XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(map, size); + if(keymap==nullptr) { + Log::e("WaylandApi: can't compile the keymap"); + return; + } + xkb_state* state = xkb_state_new(keymap); + if(state==nullptr) { + Log::e("WaylandApi: can't create the keyboard state"); + xkb_keymap_unref(keymap); + return; + } + + ioDestroy(self->xkbState, xkb_state_unref); + ioDestroy(self->xkbKeymap, xkb_keymap_unref); + self->xkbKeymap = keymap; + self->xkbState = state; + self->repeatKeycode = 0; // keycodes may mean something else in the new keymap + + const char* layout = xkb_keymap_layout_get_name(keymap, 0); + Log::i("WaylandApi: keymap loaded, ", xkb_keymap_num_layouts(keymap), " layout(s), first: ", (layout!=nullptr ? layout : "unnamed")); + } + +void WaylandApi::Private::onWlKeyboardEnter(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + wl_surface* surface, [[maybe_unused]] wl_array* keys) { + // Like X11's FocusIn. Keys already held ('keys') are ignored: they aren't in heldKeys, so their releases are dropped. + auto self = static_cast(data); + if(surface==nullptr) + return; // the surface was destroyed after the compositor sent the event + auto w = static_cast(wl_surface_get_user_data(surface)); + self->keyboardFocus = w; + self->focusGainedInBatch = w; + if(w->owner==nullptr) + return; + FocusEvent e(true, Event::UnknownReason); + SystemApi::dispatchFocus(*w->owner, e); + // Pointer already over the window: like an enter, a move to its position and the app's cursor. + if(self->pointerFocus==w) { + MouseEvent m = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, m); + SystemApi::showCursor(reinterpret_cast(w), SystemApi::cursorShape(*w->owner)); + } + } + +void WaylandApi::Private::onWlKeyboardLeave(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] wl_surface* surface) { + // Like X11's FocusOut, but held keys and buttons are released first. 'surface' may be null, so the window comes from + // keyboardFocus. + auto self = static_cast(data); + WWindow* w = self->keyboardFocus; + if(w==nullptr) + return; + // On focus loss the real releases never reach the app, so keys and buttons would stay held (e.g. Alt after Alt+Tab). + self->releaseHeldKeys(*w); + self->releaseHeldButtons(*w); + self->keyboardFocus = nullptr; + if(w->owner==nullptr) + return; + FocusEvent e(false, Event::UnknownReason); + SystemApi::dispatchFocus(*w->owner, e); + if(self->pointerFocus==w) + SystemApi::showCursor(reinterpret_cast(w), SystemApi::cursorShape(*w->owner)); + } + +void WaylandApi::Private::onWlKeyboardKey(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] uint32_t time, uint32_t key, uint32_t state) { + // xkb keycodes are evdev + 8 (X11's keycodes); also the scancode for the dispatcher. + auto self = static_cast(data); + if(self->keyboardFocus==nullptr || self->keyboardFocus->owner==nullptr) + return; + if(self->xkbState==nullptr) + return; // no keymap compiled + // Only releases of keys the app got a press for are dispatched (see heldKeys). + const uint32_t keycode = key + 8; + if(state==WL_KEYBOARD_KEY_STATE_PRESSED) { + if(std::find(self->heldKeys.begin(), self->heldKeys.end(), keycode)==self->heldKeys.end()) + self->heldKeys.push_back(keycode); + KeyEvent e = self->keyEvent(keycode, Event::KeyDown); + SystemApi::dispatchKeyDown(*self->keyboardFocus->owner, e, keycode); + self->startKeyRepeat(keycode); + } else { + if(std::erase(self->heldKeys, keycode)==0) + return; + if(keycode==self->repeatKeycode) + self->repeatKeycode = 0; + KeyEvent e = self->keyEvent(keycode, Event::KeyUp); + SystemApi::dispatchKeyUp(*self->keyboardFocus->owner, e, keycode); + } + } + +void WaylandApi::Private::onWlKeyboardModifiers(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group) { + // Not dispatched: Tempest tracks Shift/Ctrl/Alt from the key events itself. xkb needs them for the text of a key + // (Shift, CapsLock) and for the active layout ('group'). + auto self = static_cast(data); + if(self->xkbState==nullptr) + return; // no keymap compiled + xkb_state_update_mask(self->xkbState, depressed, latched, locked, 0, 0, group); + } + +void WaylandApi::Private::onWlKeyboardRepeatInfo(void* data, [[maybe_unused]] wl_keyboard* keyboard, int32_t rate, int32_t delay) { + // The desktop setting; rate 0 = repeat disabled. + auto self = static_cast(data); + self->repeatRate = rate; + self->repeatDelay = delay; + if(rate<=0) + self->repeatKeycode = 0; + } + +/// Window specific listeners +// +void WaylandApi::Private::onXdgSurfaceConfigure(void* data, [[maybe_unused]] xdg_surface* xdgSurface, uint32_t serial) { + // End of configure sequence (after xdg_toplevel.configure etc.): pending becomes current and is acked + auto w = static_cast(data); + if(w->pending.width==0 || w->pending.height==0) { + // pending is 0 only if the compositor never sent a size (first configure) + // client decides: app requested size needs conversion to logical (KWin sends preferred_scale before the first configure). + w->pending.width = std::max(1, int32_t(std::lround(float(w->appRequestedWidth) /w->scale()))); + w->pending.height = std::max(1, int32_t(std::lround(float(w->appRequestedHeight)/w->scale()))); + } + w->width = w->pending.width; + w->height = w->pending.height; + w->fullscreen = w->pending.fullscreen; + w->suspended = w->pending.suspended; + + // Viewport state is double-buffered: it applies with the next commit, which Vulkan does in vkQueuePresent. + wp_viewport_set_destination(w->viewport, w->width, w->height); + + xdg_surface_ack_configure(w->xdgSurface, serial); + w->configured = true; + } + +void WaylandApi::Private::onXdgToplevelConfigure(void* data, [[maybe_unused]] xdg_toplevel* toplevel, int32_t width, int32_t height, wl_array* states) { + // Only fills w->pending; it becomes current in onXdgSurfaceConfigure. + auto w = static_cast(data); + // Logical size; 0 = client decides, keep the previous size + if(width>0 && height>0) { + w->pending.width = width; + w->pending.height = height; + } + // 'states': the complete list of xdg_toplevel_state values + w->pending.fullscreen = false; + w->pending.suspended = false; + auto state = static_cast(states->data); + for(size_t i=0; isize/sizeof(uint32_t); ++i) { + if(state[i]==XDG_TOPLEVEL_STATE_FULLSCREEN) + w->pending.fullscreen = true; + static_assert(xdgWmBaseVersion>=XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION); + if(state[i]==XDG_TOPLEVEL_STATE_SUSPENDED) + w->pending.suspended = true; + } + } + +void WaylandApi::Private::onXdgToplevelClose(void* data, [[maybe_unused]] xdg_toplevel* toplevel) { + // Only a request. Like the Windows backend: the app or an overlay may accept the CloseEvent; otherwise exit. + auto w = static_cast(data); + if(w->owner==nullptr) + return; + CloseEvent e; + SystemApi::dispatchClose(*w->owner, e); + if(!e.isAccepted()) + SystemApi::exit(); + } + +void WaylandApi::Private::onZxdgToplevelDecorationV1Configure([[maybe_unused]] void* data, [[maybe_unused]] zxdg_toplevel_decoration_v1* decoration, uint32_t mode) { + if(mode==ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE) + Log::i("WaylandApi: compositor chose client-side decorations; the window is undecorated"); + } + +void WaylandApi::Private::onWpFractionalScaleV1PreferredScale(void* data, [[maybe_unused]] wp_fractional_scale_v1* fractionalScale, uint32_t scale) { + auto w = static_cast(data); + w->preferredScale = scale; + } + +void WaylandApi::Private::onZwpLockedPointerV1Locked(void* data, [[maybe_unused]] zwp_locked_pointer_v1* lockedPointer) { + static_cast(data)->pointerLocked = true; + } + +void WaylandApi::Private::onZwpLockedPointerV1Unlocked(void* data, [[maybe_unused]] zwp_locked_pointer_v1* lockedPointer) { + // E.g. focus lost; the persistent lock may become active again later. + static_cast(data)->pointerLocked = false; + } + +//// +/// LISTENER TABLES +// >> Note: Every event of the bound interface version needs a handler: libwayland aborts on null entry. + +/// Session listener tables +// +const wl_registry_listener WaylandApi::Private::wlRegistryListener = { + .global = onWlRegistryGlobal, + .global_remove = ioIgnore, + }; +const xdg_wm_base_listener WaylandApi::Private::xdgWmBaseListener = { + .ping = onXdgWmBasePing, + }; +const wl_seat_listener WaylandApi::Private::wlSeatListener = { + .capabilities = onWlSeatCapabilities, + .name = ioIgnore, + }; +const wl_pointer_listener WaylandApi::Private::wlPointerListener = { + .enter = onWlPointerEnter, + .leave = onWlPointerLeave, + .motion = onWlPointerMotion, + .button = onWlPointerButton, + // TODO(wayland): touchpad scrolling: continuous axis events (axis_source finger) come without axis_value120 + .axis = ioIgnore, + .frame = ioIgnore, + .axis_source = ioIgnore, + .axis_stop = ioIgnore, + .axis_discrete = ioIgnore, // not sent since v8 (replaced by axis_value120) + .axis_value120 = onWlPointerAxisValue120, + // axis_relative_direction (only v9+) + }; +const zwp_relative_pointer_v1_listener WaylandApi::Private::zwpRelativePointerV1Listener = { + .relative_motion = onZwpRelativePointerV1RelativeMotion, + }; +const wl_keyboard_listener WaylandApi::Private::wlKeyboardListener = { + .keymap = onWlKeyboardKeymap, + .enter = onWlKeyboardEnter, + .leave = onWlKeyboardLeave, + .key = onWlKeyboardKey, + .modifiers = onWlKeyboardModifiers, + .repeat_info = onWlKeyboardRepeatInfo, + }; + +/// Window specific listener tables +// +const wl_surface_listener WaylandApi::Private::wlSurfaceListener = { + // Outputs and buffer scale hints; the scale comes from wp_fractional_scale_v1. + .enter = ioIgnore, + .leave = ioIgnore, + .preferred_buffer_scale = ioIgnore, + .preferred_buffer_transform = ioIgnore, + }; +const xdg_surface_listener WaylandApi::Private::xdgSurfaceListener = { + .configure = onXdgSurfaceConfigure, + }; +const xdg_toplevel_listener WaylandApi::Private::xdgToplevelListener = { + .configure = onXdgToplevelConfigure, + .close = onXdgToplevelClose, + .configure_bounds = ioIgnore, + .wm_capabilities = ioIgnore, + }; +const zxdg_toplevel_decoration_v1_listener WaylandApi::Private::zxdgToplevelDecorationV1Listener = { + .configure = onZxdgToplevelDecorationV1Configure, + }; +const wp_fractional_scale_v1_listener WaylandApi::Private::wpFractionalScaleV1Listener = { + .preferred_scale = onWpFractionalScaleV1PreferredScale, + }; +const zwp_locked_pointer_v1_listener WaylandApi::Private::zwpLockedPointerV1Listener = { + .locked = onZwpLockedPointerV1Locked, + .unlocked = onZwpLockedPointerV1Unlocked, + }; + +//// +/// HELPERS +// + +template +void WaylandApi::Private::ioDestroy(T*& obj, void (*destroyFn)(T*)) { + // Destroys a Wayland/xkb object if it exists and clears the pointer: safe on half-built state and when called twice. + if(obj==nullptr) + return; + destroyFn(obj); + obj = nullptr; + } + +void WaylandApi::Private::logDisplayError() const { + const int err = wl_display_get_error(display); + if(err==EPROTO) { + const wl_interface* iface = nullptr; + uint32_t id = 0; + const uint32_t code = wl_display_get_protocol_error(display, &iface, &id); + Log::e("WaylandApi: protocol error ", code, " on ", (iface!=nullptr ? iface->name : "unknown"), "@", id); + } else { + Log::e("WaylandApi: connection error: ", std::strerror(err)); + } + } + +Event::MouseButton WaylandApi::Private::toButton(uint32_t button) { + // wl_pointer.button carries evdev codes. + switch(button) { + case BTN_LEFT: return Event::ButtonLeft; + case BTN_RIGHT: return Event::ButtonRight; + case BTN_MIDDLE: return Event::ButtonMid; + case BTN_SIDE: return Event::ButtonBack; + case BTN_EXTRA: return Event::ButtonForward; + } + return Event::ButtonNone; + } + +wp_cursor_shape_device_v1_shape WaylandApi::Private::toCursorShape(CursorShape shape) { + // Hidden is handled by the caller. + switch(shape) { + case CursorShape::Arrow: + case CursorShape::Hidden: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT; + case CursorShape::IBeam: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT; + case CursorShape::SizeVer: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE; + case CursorShape::SizeHor: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE; + case CursorShape::SizeBDiag: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE; // '/' + case CursorShape::SizeFDiag: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE; // '\' + case CursorShape::SizeAll: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL; // four arrows; all_resize needs v2 + } + return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT; + } + +MouseEvent WaylandApi::Private::pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type) { + // floor: the physical pixel the cursor is in. + const int x = int(std::floor(w.cursorX)); + const int y = int(std::floor(w.cursorY)); + return MouseEvent(x, y, button, Event::M_NoModifier, delta, 0, type); + } + +KeyEvent WaylandApi::Private::keyEvent(uint32_t keycode, Event::Type type) const { + // keycode: xkb keycode (evdev + 8). key: level-0 keysym of the active layout, so Shift+W stays K_W (like Windows' + // virtual keys). code: the typed character, 0 if none (e.g. arrows, dead keys). + const xkb_layout_index_t layout = xkb_state_key_get_layout(xkbState, keycode); + const xkb_keysym_t* syms = nullptr; + const int count = xkb_keymap_key_get_syms_by_level(xkbKeymap, keycode, layout, 0, &syms); + const xkb_keysym_t sym = count>0 ? syms[0] : XKB_KEY_NoSymbol; // choose first available + const auto key = Event::KeyType(SystemApi::translateKey(sym)); + + // Only characters up to U+FFFF (Basic Multilingual Plane): Tempest's text input can't hold more. + // Beyond it, nothing is typed (X11 and Windows deliver half a UTF-16 surrogate pair there). + const uint32_t utf32 = xkb_state_key_get_utf32(xkbState, keycode); + const uint32_t code = utf32<=0xFFFF ? utf32 : 0; + return KeyEvent(key, code, Event::M_NoModifier, type); + } + +bool WaylandApi::Private::isWindowFocused(const WWindow* w) const { + // Window focus = keyboard focus: mouse moves, wheel and presses only reach the focused window. + // Without a keyboard every window counts as focused (pointer events only come for the one under the pointer). + return keyboard==nullptr || keyboardFocus==w; + } + +void WaylandApi::Private::setPointerLock(WWindow& w, bool lock) { + // Mouse-look: Wayland apps can't move the pointer, so while the cursor is hidden it's locked in place and the app + // gets relative motion instead (Tempest has no relative-mouse API). Called on every cursor change. + // The lock stays requested: the compositor pauses it while the window is unfocused and resumes it by itself. + if(lock && w.lockedPointer==nullptr && pointer!=nullptr) { + w.lockedPointer = zwp_pointer_constraints_v1_lock_pointer(pointerConstraints, w.surface, pointer, nullptr, + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + zwp_locked_pointer_v1_add_listener(w.lockedPointer, &zwpLockedPointerV1Listener, &w); + } + else if(!lock && w.lockedPointer!=nullptr) { + ioDestroy(w.lockedPointer, zwp_locked_pointer_v1_destroy); + w.pointerLocked = false; + } + } + +void WaylandApi::Private::releaseHeldKeys(WWindow& w) { + repeatKeycode = 0; + auto keys = std::move(heldKeys); // save from manipulation + heldKeys.clear(); + if(w.owner==nullptr) + return; + for(uint32_t keycode:keys) { + KeyEvent e = keyEvent(keycode, Event::KeyUp); + SystemApi::dispatchKeyUp(*w.owner, e, keycode); + } + } + +void WaylandApi::Private::releaseHeldButtons(WWindow& w) { + auto buttons = std::move(heldButtons); // save from manipulation + heldButtons.clear(); + if(w.owner==nullptr) + return; + for(uint32_t button:buttons) { + MouseEvent e = pointerEvent(w, toButton(button), 0, Event::MouseUp); + SystemApi::dispatchMouseUp(*w.owner, e); + } + } + +void WaylandApi::Private::releasePointer() { + // release what the app saw pressed while pointer still exists + if(pointerFocus!=nullptr) + releaseHeldButtons(*pointerFocus); + // Objects created from the pointer first. + for(auto& w:windows) + setPointerLock(*w, false); + ioDestroy(relativePointer, zwp_relative_pointer_v1_destroy); + ioDestroy(cursorShapeDevice, wp_cursor_shape_device_v1_destroy); + static_assert(wlSeatVersion>=WL_POINTER_RELEASE_SINCE_VERSION); + ioDestroy(pointer, wl_pointer_release); + pointerFocus = nullptr; + heldButtons.clear(); + } + +void WaylandApi::Private::releaseKeyboard() { + // release what the app saw pressed while keyboard still exists + if(keyboardFocus!=nullptr) + releaseHeldKeys(*keyboardFocus); + static_assert(wlSeatVersion>=WL_KEYBOARD_RELEASE_SINCE_VERSION); + ioDestroy(keyboard, wl_keyboard_release); + keyboardFocus = nullptr; + repeatKeycode = 0; + heldKeys.clear(); + } + +void WaylandApi::Private::startKeyRepeat(uint32_t keycode) { + // Non-repeating keys (modifiers) leave a running repeat alone; a repeating key takes over. + if(repeatRate<=0 || !xkb_keymap_key_repeats(xkbKeymap, keycode)) + return; + repeatKeycode = keycode; + repeatNext = std::chrono::steady_clock::now() + std::chrono::milliseconds(repeatDelay); + } + +void WaylandApi::Private::processKeyRepeat() { + if(repeatKeycode==0) + return; + const auto now = std::chrono::steady_clock::now(); + const auto interval = std::chrono::microseconds(std::chrono::seconds(1))/repeatRate; + if(nowowner, e, repeatKeycode); + } + +//// +/// CONNECTION AND WINDOWS +// + +bool WaylandApi::Private::hasRequiredGlobals() const { + // No fallbacks yet without fractional-scale, cursor-shape or server-side decorations (e.g. GNOME doesn't work). + // Pointer constraints + relative pointer: mouse-look, since Wayland clients can't warp the pointer. + struct Required { + const void* object; + const wl_interface& interface; + uint32_t version; + }; + const Required required[] = { + {compositor, wl_compositor_interface, wlCompositorVersion }, + {wmBase, xdg_wm_base_interface, xdgWmBaseVersion }, + {decorationManager, zxdg_decoration_manager_v1_interface, zxdgDecorationManagerV1Version }, + {viewporter, wp_viewporter_interface, wpViewporterVersion }, + {fractionalScaleManager, wp_fractional_scale_manager_v1_interface, wpFractionalScaleManagerV1Version }, + {seat, wl_seat_interface, wlSeatVersion }, + {cursorShapeManager, wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version }, + {pointerConstraints, zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version }, + {relativePointerManager, zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version}, + }; + bool hasAll = true; + for(auto& r:required) { + if(r.object!=nullptr) + continue; + Log::e("WaylandApi: compositor doesn't offer ", r.interface.name, " version ", r.version, " or newer"); + hasAll = false; + } + return hasAll; + } + +bool WaylandApi::Private::connect() { + // True only if Wayland is usable (connected + all required globals). Otherwise logs error + // Note: ::dpy set in constructor/destructor only! + + // TODO(wayland): WAYLAND_DISPLAY check: wl_display_connect(nullptr) falls back to "wayland-0", + // which may belong to another session of the same user (e.g. X11 session here, Wayland session + // on another Virtual Terminal). + + display = wl_display_connect(nullptr); + if(!display) { + const int err = errno; + const char* name = std::getenv("WAYLAND_DISPLAY"); + Log::e("WaylandApi: can't connect to a compositor (WAYLAND_DISPLAY=", (name!=nullptr ? name : "not set"), "): ", + std::strerror(err)); + return false; + } + + // Needed to compile keymap: without it there's no keyboard input + xkbContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if(!xkbContext) { + Log::e("WaylandApi: xkb_context_new failed"); + return false; + } + + registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, &wlRegistryListener, this); + + if(wl_display_roundtrip(display)<0) { + logDisplayError(); + return false; + } + return hasRequiredGlobals(); + } + +void WaylandApi::Private::disconnect() { + // Handles partial state and is safe to call twice (constructor on failure, destructor). + // Connection errors are ignored while tearing down. + if(!display) + return; + + // Windows first: xdg_wm_base must not be destroyed while xdg_surfaces exist + while(!windows.empty()) + destroyWindow(windows.back().get()); + + ioDestroy(relativePointerManager, zwp_relative_pointer_manager_v1_destroy); + ioDestroy(pointerConstraints, zwp_pointer_constraints_v1_destroy); + ioDestroy(cursorShapeManager, wp_cursor_shape_manager_v1_destroy); + ioDestroy(fractionalScaleManager, wp_fractional_scale_manager_v1_destroy); + ioDestroy(viewporter, wp_viewporter_destroy); + ioDestroy(decorationManager, zxdg_decoration_manager_v1_destroy); + ioDestroy(wmBase, xdg_wm_base_destroy); + + // Seat devices before the seat + releaseKeyboard(); + releasePointer(); + + ioDestroy(xkbState, xkb_state_unref); + ioDestroy(xkbKeymap, xkb_keymap_unref); + ioDestroy(xkbContext, xkb_context_unref); + + static_assert(wlSeatVersion>=WL_SEAT_RELEASE_SINCE_VERSION); + ioDestroy(seat, wl_seat_release); + ioDestroy(compositor, wl_compositor_destroy); + ioDestroy(registry, wl_registry_destroy); + + wl_display_flush(display); + wl_display_disconnect(display); + display = nullptr; + } + +WWindow* WaylandApi::Private::createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm) { + // all roles and state requests needed before the first commit + windows.emplace_back(std::make_unique()); + WWindow* win = windows.back().get(); + win->owner = owner; + win->appRequestedWidth = int32_t(width); + win->appRequestedHeight = int32_t(height); + + win->surface = wl_compositor_create_surface(compositor); + wl_surface_add_listener(win->surface, &wlSurfaceListener, win); + + win->xdgSurface = xdg_wm_base_get_xdg_surface(wmBase, win->surface); + xdg_surface_add_listener(win->xdgSurface, &xdgSurfaceListener, win); + win->toplevel = xdg_surface_get_toplevel(win->xdgSurface); + xdg_toplevel_add_listener(win->toplevel, &xdgToplevelListener, win); + + win->decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(decorationManager, win->toplevel); + zxdg_toplevel_decoration_v1_set_mode(win->decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); + zxdg_toplevel_decoration_v1_add_listener(win->decoration, &zxdgToplevelDecorationV1Listener, win); + + win->viewport = wp_viewporter_get_viewport(viewporter, win->surface); + + win->fractionalScale = wp_fractional_scale_manager_v1_get_fractional_scale(fractionalScaleManager, win->surface); + wp_fractional_scale_v1_add_listener(win->fractionalScale, &wpFractionalScaleV1Listener, win); + + // Requested before the first commit, so the first configure already carries the maximized/fullscreen size. + if(sm==SystemApi::Maximized) + xdg_toplevel_set_maximized(win->toplevel); + if(sm==SystemApi::FullScreen) + xdg_toplevel_set_fullscreen(win->toplevel, nullptr); + // Only a request (xdg-shell doesn't say whether it works before mapping; KWin honors it). + if(sm==SystemApi::Minimized) + xdg_toplevel_set_minimized(win->toplevel); + + wl_surface_commit(win->surface); + + // Wait for the first configure: Window's constructor reads the client rect right after this returns, + // and no buffer (= Vulkan present) may be attached before the first ack_configure. + while(!win->configured) { + if(wl_display_dispatch(display)<0) { + logDisplayError(); + destroyWindow(win); + return nullptr; + } + } + + // Window's constructor reads the client rect itself after this returns: no SizeEvent for this size. + win->dispatchedWidth = win->physicalWidth(); + win->dispatchedHeight = win->physicalHeight(); + return win; + } + +void WaylandApi::Private::destroyWindow(WWindow* w) { + // Reverse order of creation; also used for a half-created window. + // The VkSurfaceKHR must be gone before the wl_surface: Tempest destroys the swapchain before the window. + if(w==nullptr) + return; + + // The compositor sends wl_pointer/wl_keyboard.leave for a destroyed surface, but only after we've deleted w. + if(pointerFocus==w) { + pointerFocus = nullptr; + heldButtons.clear(); + } + if(keyboardFocus==w) { + keyboardFocus = nullptr; + repeatKeycode = 0; + heldKeys.clear(); + heldButtons.clear(); + } + if(focusGainedInBatch==w) + focusGainedInBatch = nullptr; + + setPointerLock(*w, false); // created for the wl_surface + ioDestroy(w->fractionalScale, wp_fractional_scale_v1_destroy); + ioDestroy(w->viewport, wp_viewport_destroy); + ioDestroy(w->decoration, zxdg_toplevel_decoration_v1_destroy); // before toplevel + ioDestroy(w->toplevel, xdg_toplevel_destroy); + ioDestroy(w->xdgSurface, xdg_surface_destroy); + ioDestroy(w->surface, wl_surface_destroy); + + // After deletion events still queued for its objects are dropped by libwayland + std::erase_if(windows, [w](const std::unique_ptr& p) { return p.get()==w; }); + } + +//// +/// EVENT LOOP +// + +void WaylandApi::Private::handleConnectionError() { + // connection dead, nothing works anymore, end the app + logDisplayError(); + SystemApi::exit(); + } + +int WaylandApi::Private::pollTimeout() const { + // 0 if a window may render; otherwise wait for the compositor (e.g. a configure that ends 'suspended'), + // at most idlePollTimeoutMs, and not beyond the next scheduled key repeat + + // Longest sleep in poll() while no window may render; short enough to keep Tempest's timers accurate. + constexpr int idlePollTimeoutMs = 5; + const bool canRender = std::any_of(windows.begin(), windows.end(), + [](const std::unique_ptr& w) { return w->readyToRender(); }); + int timeout = canRender ? 0 : idlePollTimeoutMs; + if(repeatKeycode!=0) { + const auto untilRepeat = std::chrono::ceil(repeatNext - std::chrono::steady_clock::now()); + timeout = std::clamp(int(untilRepeat.count()), 0, timeout); + } + return timeout; + } + +void WaylandApi::Private::readEvents() { + // Doesn't block beyond pollTimeout(): Tempest's loop must keep running (timers, rendering). + // The Vulkan driver reads the same connection on its own queue; prepare_read -> read_events/cancel_read + // is how several readers share the socket without stealing or blocking each other. + focusGainedInBatch = nullptr; // a new batch (see onWlPointerButton) + while(wl_display_prepare_read(display)!=0) { + // Our queue already holds events (e.g. read by the driver): dispatch them before reading more. + if(wl_display_dispatch_pending(display)<0) { + handleConnectionError(); + return; + } + } + + // Send our requests even when no frame is presented. If the socket is full (EAGAIN), the rest goes out next time. + wl_display_flush(display); + + pollfd fd = {}; + fd.fd = wl_display_get_fd(display); + fd.events = POLLIN; + // Timeout computed after dispatching the queued events: a configure among them may let a window render now. + if(poll(&fd, 1, pollTimeout())>0) { + if(wl_display_read_events(display)<0) { + handleConnectionError(); + return; + } + } else { + wl_display_cancel_read(display); + } + + if(wl_display_dispatch_pending(display)<0) { + handleConnectionError(); + return; + } + + processKeyRepeat(); // synthetic key events + } + +void WaylandApi::Private::updateState(WWindow& w) { + // Update things that should change only once per draw, not on every triggered callback (e.g. swapchain resize) + const int32_t pw = w.physicalWidth(); + const int32_t ph = w.physicalHeight(); + if(pw!=w.dispatchedWidth || ph!=w.dispatchedHeight) { + w.dispatchedWidth = pw; + w.dispatchedHeight = ph; + SizeEvent e(pw, ph); + SystemApi::dispatchResize(*w.owner, e); + } + } + +void WaylandApi::Private::renderWindows() { + // Skips suspended windows: nothing is drawn while the compositor doesn't repaint them (e.g. minimized). + for(auto& w:windows) { + if(!w->readyToRender()) + continue; + updateState(*w); + SystemApi::dispatchRender(*w->owner); + } + } + +//// +/// WAYLAND API +// + +WaylandApi::WaylandApi() { + // Same table as X11: XKB keysyms have the values of X11's keysyms. Letters, digits and F-keys are ranges + // (see SystemApi::translateKey). + static const TranslateKeyPair k[] = { + { XKB_KEY_Control_L, Event::K_LControl }, + { XKB_KEY_Control_R, Event::K_RControl }, + + { XKB_KEY_Shift_L, Event::K_LShift }, + { XKB_KEY_Shift_R, Event::K_RShift }, + + { XKB_KEY_Alt_L, Event::K_LAlt }, + { XKB_KEY_Alt_R, Event::K_RAlt }, + + { XKB_KEY_Insert, Event::K_Insert }, + { XKB_KEY_Delete, Event::K_Delete }, + { XKB_KEY_Home, Event::K_Home }, + { XKB_KEY_End, Event::K_End }, + { XKB_KEY_Page_Up, Event::K_PageUp }, + { XKB_KEY_Page_Down, Event::K_PageDown }, + + { XKB_KEY_Left, Event::K_Left }, + { XKB_KEY_Right, Event::K_Right }, + { XKB_KEY_Up, Event::K_Up }, + { XKB_KEY_Down, Event::K_Down }, + + { XKB_KEY_Escape, Event::K_ESCAPE }, + { XKB_KEY_Tab, Event::K_Tab }, + { XKB_KEY_BackSpace, Event::K_Back }, + { XKB_KEY_Pause, Event::K_Pause }, + { XKB_KEY_Return, Event::K_Return }, + { XKB_KEY_space, Event::K_Space }, + { XKB_KEY_Caps_Lock, Event::K_CapsLock }, + + { XKB_KEY_F1, Event::K_F1 }, + { XKB_KEY_0, Event::K_0 }, + { XKB_KEY_a, Event::K_A }, + + { 0, Event::K_NoKey } + }; + setupKeyTranslate(k,24); + + impl.reset(new Private()); + if(impl->connect()) { + dpy = impl->display; // display() non-null only while connected + } else { + impl->disconnect(); // leave nothing half-connected + Log::e("WaylandApi: Wayland is not usable (use TEMPEST_DISABLE_WAYLAND=1 for X11)"); + } + } + +WaylandApi::~WaylandApi() { + dpy = nullptr; + impl->disconnect(); + impl.reset(); + } + +bool WaylandApi::isDisabled() { + // Manual override, e.g. to compare with the X11 backend: TEMPEST_DISABLE_WAYLAND=1 + const char* disable = std::getenv("TEMPEST_DISABLE_WAYLAND"); + return disable!=nullptr && std::strcmp(disable,"1")==0; + } + +wl_display* WaylandApi::display() { + SystemApi::inst(); + return dpy; + } + +wl_surface* WaylandApi::surface(SystemApi::Window* w) { + if(dpy==nullptr || w==nullptr) + return nullptr; + return toWWindow(w)->surface; + } + +SystemApi::Window* WaylandApi::implCreateWindow(Tempest::Window* owner, uint32_t width, uint32_t height) { + if(dpy==nullptr) + return nullptr; + return reinterpret_cast(impl->createWindow(owner,width,height,SystemApi::Normal)); + } + +SystemApi::Window* WaylandApi::implCreateWindow(Tempest::Window* owner, ShowMode sm) { + // 800x600 like the other backends; Maximized/FullScreen get their size from the first configure. + // Hidden treated as Normal. + if(dpy==nullptr) + return nullptr; + return reinterpret_cast(impl->createWindow(owner,800,600,sm)); + } + +void WaylandApi::implDestroyWindow(SystemApi::Window* w) { + impl->destroyWindow(toWWindow(w)); + } + +void WaylandApi::implExit() { + isExit.store(true); + } + +Rect WaylandApi::implWindowClientRect(SystemApi::Window* w) { + // Wayland has no global window position. + auto ww = toWWindow(w); + return Rect(0, 0, ww->physicalWidth(), ww->physicalHeight()); + } + +bool WaylandApi::implSetAsFullscreen(SystemApi::Window* w, bool fullScreen) { + // Only a request: implIsFullscreen() and the size follow with the next configure. + auto ww = toWWindow(w); + if(fullScreen) + xdg_toplevel_set_fullscreen(ww->toplevel, nullptr); // nullptr: the compositor picks the output + else + xdg_toplevel_unset_fullscreen(ww->toplevel); + wl_display_flush(impl->display); + return true; + } + +bool WaylandApi::implIsFullscreen(SystemApi::Window* w) { + return toWWindow(w)->fullscreen; + } + +float WaylandApi::implUiScale(SystemApi::Window* w) { + return toWWindow(w)->scale(); + } + +void WaylandApi::implSetWindowTitle(SystemApi::Window* w, const char* utf8) { + xdg_toplevel_set_title(toWWindow(w)->toplevel, utf8); + } + +void WaylandApi::implSetCursorPosition(SystemApi::Window* w, int x, int y) { + // Wayland clients can't move the pointer. While it's locked, the virtual cursor is all the app sees, so setting it is + // enough (no MouseMove for the jump). + auto ww = toWWindow(w); + if(!ww->pointerLocked) + return; + ww->cursorX = x; + ww->cursorY = y; + } + +void WaylandApi::implShowCursor(SystemApi::Window* w, CursorShape show) { + // The pointer lock is requested whenever the cursor is hidden, even with the pointer elsewhere: the compositor + // activates it once the pointer is over the focused window (see Private::setPointerLock). + // The cursor image needs the enter serial, so it's only set while the pointer is over w; enter and focus changes + // re-apply it. An unfocused window shows the desktop cursor. + auto ww = toWWindow(w); + impl->setPointerLock(*ww, show==CursorShape::Hidden); + if(impl->pointerFocus!=ww) + return; + const CursorShape shape = impl->isWindowFocused(ww) ? show : CursorShape::Arrow; + if(shape==CursorShape::Hidden) + wl_pointer_set_cursor(impl->pointer, impl->pointerEnterSerial, nullptr, 0, 0); + else + wp_cursor_shape_device_v1_set_shape(impl->cursorShapeDevice, impl->pointerEnterSerial, Private::toCursorShape(shape)); + } + +bool WaylandApi::implIsRunning() { + return !isExit.load(); + } + +int WaylandApi::implExec(SystemApi::AppCallBack& cb) { + while(!isExit.load()) { + implProcessEvents(cb); + } + return 0; + } + +void WaylandApi::implProcessEvents(SystemApi::AppCallBack& cb) { + // Unlike X11, handles all pending events and then renders, so a steady stream of events can't starve rendering. + // readEvents() only waits while no window may render (see pollTimeout()). + impl->readEvents(); + if(cb.onTimer()==0) + std::this_thread::yield(); + impl->renderWindows(); + } + +#endif // defined(TEMPEST_BUILD_WAYLAND) diff --git a/Engine/system/api/waylandapi.h b/Engine/system/api/waylandapi.h new file mode 100644 index 00000000..79f70c53 --- /dev/null +++ b/Engine/system/api/waylandapi.h @@ -0,0 +1,52 @@ +#pragma once + +#include "system/systemapi.h" + +// opaque libwayland types +struct wl_display; +struct wl_surface; + +namespace Tempest { + +class WaylandApi final: SystemApi { + public: + // For Vulkan (vulkanapi.cpp, vswapchain.cpp). + // nullptr if Wayland is not the active backend + static wl_display* display(); + static wl_surface* surface(SystemApi::Window* w); + + // TEMPEST_DISABLE_WAYLAND=1: X11 is the backend + static bool isDisabled(); + + private: + struct Private; + + WaylandApi(); + ~WaylandApi(); + + Window* implCreateWindow(Tempest::Window *owner, uint32_t width, uint32_t height) override; + Window* implCreateWindow(Tempest::Window *owner, ShowMode sm) override; + void implDestroyWindow(Window* w) override; + void implExit() override; + + Rect implWindowClientRect(SystemApi::Window *w) override; + + bool implSetAsFullscreen(SystemApi::Window *w, bool fullScreen) override; + bool implIsFullscreen(SystemApi::Window *w) override; + float implUiScale(SystemApi::Window* w) override; + + void implSetWindowTitle(SystemApi::Window *w, const char* utf8) override; + + void implSetCursorPosition(SystemApi::Window *w, int x, int y) override; + void implShowCursor(SystemApi::Window *w, CursorShape show) override; + + int implExec(AppCallBack& cb) override; + void implProcessEvents(AppCallBack& cb) override; + bool implIsRunning() override; + + std::unique_ptr impl; + + friend class SystemApi; + }; + +} diff --git a/Engine/system/systemapi.cpp b/Engine/system/systemapi.cpp index 217b241a..98e65963 100644 --- a/Engine/system/systemapi.cpp +++ b/Engine/system/systemapi.cpp @@ -2,6 +2,7 @@ #include "api/windowsapi.h" #include "api/x11api.h" +#include "api/waylandapi.h" #include "api/macosapi.h" #include "api/iosapi.h" #include "api/androidapi.h" @@ -9,6 +10,7 @@ #include #include +#include using namespace Tempest; @@ -105,7 +107,17 @@ SystemApi& SystemApi::inst() { #elif defined(__ANDROID__) static AndroidApi api; #elif defined(__UNIX__) - static X11Api api; + static SystemApi& api = []() -> SystemApi& { +#if defined(TEMPEST_BUILD_WAYLAND) + if(!WaylandApi::isDisabled()) { + static WaylandApi wl; + return wl; + } + Log::i("SystemApi: Wayland disabled by TEMPEST_DISABLE_WAYLAND=1, using X11"); +#endif + static X11Api x11; + return x11; + }(); #elif defined(__OSX__) static MacOSApi api; #elif defined(__IOS__) From 2f986543237f00afc9cbe9330f663844a5faf2a9 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:05:02 +0200 Subject: [PATCH 02/17] remove input --- Engine/system/api/waylandapi.cpp | 770 ++++--------------------------- 1 file changed, 98 insertions(+), 672 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index bb1e1d5b..7d301aca 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -16,21 +15,14 @@ #include #include -#include #include -#include -#include #include -#include #include "xdg-shell-client-protocol.h" #include "xdg-decoration-unstable-v1-client-protocol.h" #include "viewporter-client-protocol.h" #include "fractional-scale-v1-client-protocol.h" -#include "cursor-shape-v1-client-protocol.h" -#include "pointer-constraints-unstable-v1-client-protocol.h" -#include "relative-pointer-unstable-v1-client-protocol.h" using namespace Tempest; @@ -75,15 +67,6 @@ struct WWindow { int32_t dispatchedWidth = 0; // physical int32_t dispatchedHeight = 0; - // Pointer lock for mouse-look, requested while the window's cursor is hidden (see Private::setPointerLock). - zwp_locked_pointer_v1* lockedPointer = nullptr; - bool pointerLocked = false; - - // Cursor position. Button and axis events carry none. While pointer is locked, this is a - // virtual cursor moved by relative motion. - double cursorX = 0; // physical - double cursorY = 0; - bool readyToRender() const { return owner!=nullptr && configured && !suspended; } float scale() const { return float(preferredScale)/120.f; } @@ -107,79 +90,23 @@ struct WaylandApi::Private { // Globals wl_compositor* compositor = nullptr; - wl_seat* seat = nullptr; xdg_wm_base* wmBase = nullptr; zxdg_decoration_manager_v1* decorationManager = nullptr; wp_viewporter* viewporter = nullptr; wp_fractional_scale_manager_v1* fractionalScaleManager = nullptr; - wp_cursor_shape_manager_v1* cursorShapeManager = nullptr; - zwp_pointer_constraints_v1* pointerConstraints = nullptr; - zwp_relative_pointer_manager_v1* relativePointerManager = nullptr; // Exact versions we need to bind successfully: listeners and requests are written for these - static constexpr uint32_t wlCompositorVersion = 6; - static constexpr uint32_t wlSeatVersion = 8; // v8: wl_pointer.axis_value120 (wheel in 1/120 notches) - static constexpr uint32_t xdgWmBaseVersion = 6; // v6: xdg_toplevel state 'suspended' - static constexpr uint32_t zxdgDecorationManagerV1Version = 1; - static constexpr uint32_t wpViewporterVersion = 1; - static constexpr uint32_t wpFractionalScaleManagerV1Version = 1; - static constexpr uint32_t wpCursorShapeManagerV1Version = 1; - static constexpr uint32_t zwpPointerConstraintsV1Version = 1; - static constexpr uint32_t zwpRelativePointerManagerV1Version = 1; - - // Input devices, non-null while seat has the capability (can change at runtime) - wl_pointer* pointer = nullptr; - wp_cursor_shape_device_v1* cursorShapeDevice = nullptr; // exists together with 'pointer' - zwp_relative_pointer_v1* relativePointer = nullptr; // exists together with 'pointer' - wl_keyboard* keyboard = nullptr; - - // Pointer state between wl_pointer.enter and leave. Position is in WWindow::cursorX/Y - WWindow* pointerFocus = nullptr; // window under pointer - uint32_t pointerEnterSerial = 0; // serial of latest enter event - - // Keymap from wl_keyboard.keymap, state follows wl_keyboard.modifiers; both null until first keymap. - xkb_context* xkbContext = nullptr; - xkb_keymap* xkbKeymap = nullptr; - xkb_state* xkbState = nullptr; - WWindow* keyboardFocus = nullptr; // window with keyboard focus (wl_keyboard.enter/leave) - - // Keys (xkb keycodes) and buttons (evdev codes) whose press reached the app. Only their releases are dispatched, - // and they're released on focus or device loss (see releaseHeldKeys / releaseHeldButtons). - std::vector heldKeys; - std::vector heldButtons; - - // Key repeat: Wayland sends none. Manual implementation from wl_keyboard.repeat_info by sending the held key's KeyDown again, - // which Tempest's dispatcher turns into KeyRepeat (like on Windows). - int32_t repeatRate = 0; // repeats per second; 0 = disabled - int32_t repeatDelay = 0; // ms until the first repeat trigger - uint32_t repeatKeycode = 0; // xkb keycode of the repeating key; 0 = none. - // Non-zero only while keyboardFocus is set and repeatRate>0. - std::chrono::steady_clock::time_point repeatNext; // when the next repeat is due - - // WWindow* if a window gained focus during active readEvents() batch, otherwise nullptr (for focus click filter) - WWindow* focusGainedInBatch = nullptr; + static constexpr uint32_t wlCompositorVersion = 6; + static constexpr uint32_t xdgWmBaseVersion = 6; // v6: xdg_toplevel state 'suspended' + static constexpr uint32_t zxdgDecorationManagerV1Version = 1; + static constexpr uint32_t wpViewporterVersion = 1; + static constexpr uint32_t wpFractionalScaleManagerV1Version = 1; std::vector> windows; // owns the windows; each WWindow keeps its address (handle, listener data) - // Session listeners (globals and seat devices). 'data' is Private*. + // Session listeners. 'data' is Private*. static void onWlRegistryGlobal(void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version); static void onXdgWmBasePing(void* data, xdg_wm_base* wmBase, uint32_t serial); - static void onWlSeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities); - static void onWlPointerEnter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, wl_fixed_t x, wl_fixed_t y); - static void onWlPointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface); - static void onWlPointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y); - static void onWlPointerButton(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state); - static void onWlPointerAxisValue120(void* data, wl_pointer* pointer, uint32_t axis, int32_t value120); - static void onZwpRelativePointerV1RelativeMotion(void* data, zwp_relative_pointer_v1* relativePointer, uint32_t utimeHi, - uint32_t utimeLo, wl_fixed_t dx, wl_fixed_t dy, - wl_fixed_t dxUnaccel, wl_fixed_t dyUnaccel); - static void onWlKeyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size); - static void onWlKeyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface, wl_array* keys); - static void onWlKeyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface); - static void onWlKeyboardKey(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state); - static void onWlKeyboardModifiers(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t depressed, - uint32_t latched, uint32_t locked, uint32_t group); - static void onWlKeyboardRepeatInfo(void* data, wl_keyboard* keyboard, int32_t rate, int32_t delay); // Window specific listeners. 'data' is WWindow*. static void onXdgSurfaceConfigure(void* data, xdg_surface* xdgSurface, uint32_t serial); @@ -187,16 +114,10 @@ struct WaylandApi::Private { static void onXdgToplevelClose (void* data, xdg_toplevel* toplevel); static void onZxdgToplevelDecorationV1Configure(void* data, zxdg_toplevel_decoration_v1* decoration, uint32_t mode); static void onWpFractionalScaleV1PreferredScale(void* data, wp_fractional_scale_v1* fractionalScale, uint32_t scale); - static void onZwpLockedPointerV1Locked (void* data, zwp_locked_pointer_v1* lockedPointer); - static void onZwpLockedPointerV1Unlocked(void* data, zwp_locked_pointer_v1* lockedPointer); // Listener tables (session) static const wl_registry_listener wlRegistryListener; static const xdg_wm_base_listener xdgWmBaseListener; - static const wl_seat_listener wlSeatListener; - static const wl_pointer_listener wlPointerListener; - static const zwp_relative_pointer_v1_listener zwpRelativePointerV1Listener; - static const wl_keyboard_listener wlKeyboardListener; // Listener tables (window specific) static const wl_surface_listener wlSurfaceListener; @@ -204,25 +125,12 @@ struct WaylandApi::Private { static const xdg_toplevel_listener xdgToplevelListener; static const zxdg_toplevel_decoration_v1_listener zxdgToplevelDecorationV1Listener; static const wp_fractional_scale_v1_listener wpFractionalScaleV1Listener; - static const zwp_locked_pointer_v1_listener zwpLockedPointerV1Listener; // Helpers - static constexpr auto ioIgnore = [](auto...) {}; // useful to silence Wayland listener stubs - template static void ioDestroy(T*& obj, void (*destroyFn)(T*)); - void logDisplayError() const; - static Event::MouseButton toButton(uint32_t button); - static wp_cursor_shape_device_v1_shape toCursorShape(CursorShape shape); - static MouseEvent pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type); - KeyEvent keyEvent(uint32_t keycode, Event::Type type) const; - bool isWindowFocused(const WWindow* w) const; - void setPointerLock(WWindow& w, bool lock); - void releaseHeldKeys(WWindow& w); - void releaseHeldButtons(WWindow& w); - void releasePointer(); - void releaseKeyboard(); - void startKeyRepeat(uint32_t keycode); - void processKeyRepeat(); + static constexpr auto ioIgnore = [](auto...) {}; // useful to silence Wayland listener stubs + template static void ioDestroy(T*& obj, void (*destroyFn)(T*)); + void logDisplayError() const; // Connection and windows bool hasRequiredGlobals() const; @@ -266,291 +174,31 @@ void WaylandApi::Private::onWlRegistryGlobal(void* data, wl_registry* registry, self->fractionalScaleManager = static_cast( wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, wpFractionalScaleManagerV1Version)); } - if(std::strcmp(interface, wp_cursor_shape_manager_v1_interface.name)==0 && version>=wpCursorShapeManagerV1Version) { - self->cursorShapeManager = static_cast( - wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version)); - } - if(std::strcmp(interface, zwp_pointer_constraints_v1_interface.name)==0 && version>=zwpPointerConstraintsV1Version) { - self->pointerConstraints = static_cast( - wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version)); - } - if(std::strcmp(interface, zwp_relative_pointer_manager_v1_interface.name)==0 && version>=zwpRelativePointerManagerV1Version) { - self->relativePointerManager = static_cast( - wl_registry_bind(registry, name, &zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version)); - } - if(std::strcmp(interface, wl_seat_interface.name)==0 && version>=wlSeatVersion && self->seat==nullptr) { - // Only the first seat: multi-seat setups are rare, and Tempest has a single pointer/keyboard. - static_assert(wlSeatVersionseat = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, wlSeatVersion)); - wl_seat_add_listener(self->seat, &wlSeatListener, self); - } + // [input] if(std::strcmp(interface, wp_cursor_shape_manager_v1_interface.name)==0 && version>=wpCursorShapeManagerV1Version) { + // [input] self->cursorShapeManager = static_cast( + // [input] wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version)); + // [input] } + // [input] if(std::strcmp(interface, zwp_pointer_constraints_v1_interface.name)==0 && version>=zwpPointerConstraintsV1Version) { + // [input] self->pointerConstraints = static_cast( + // [input] wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version)); + // [input] } + // [input] if(std::strcmp(interface, zwp_relative_pointer_manager_v1_interface.name)==0 && version>=zwpRelativePointerManagerV1Version) { + // [input] self->relativePointerManager = static_cast( + // [input] wl_registry_bind(registry, name, &zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version)); + // [input] } + // [input] if(std::strcmp(interface, wl_seat_interface.name)==0 && version>=wlSeatVersion && self->seat==nullptr) { + // [input] // Only the first seat: multi-seat setups are rare, and Tempest has a single pointer/keyboard. + // [input] static_assert(wlSeatVersionseat = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, wlSeatVersion)); + // [input] wl_seat_add_listener(self->seat, &wlSeatListener, self); + // [input] } } void WaylandApi::Private::onXdgWmBasePing([[maybe_unused]] void* data, xdg_wm_base* wmBase, uint32_t serial) { xdg_wm_base_pong(wmBase, serial); } -void WaylandApi::Private::onWlSeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities) { - // Sent after binding and whenever devices come or go: get a device when its capability appears, release it when it goes. - WaylandApi::Private* self = static_cast(data); - - // The managers are bound: capabilities arrive after connect()'s roundtrip. Pointer locks follow on the next cursor - // change (always on enter). - const bool hasPointer = (capabilities & WL_SEAT_CAPABILITY_POINTER)!=0; - if(hasPointer && self->pointer==nullptr) { - self->pointer = wl_seat_get_pointer(seat); - wl_pointer_add_listener(self->pointer, &wlPointerListener, self); - self->cursorShapeDevice = wp_cursor_shape_manager_v1_get_pointer(self->cursorShapeManager, self->pointer); - self->relativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer(self->relativePointerManager, self->pointer); - zwp_relative_pointer_v1_add_listener(self->relativePointer, &zwpRelativePointerV1Listener, self); - } - else if(!hasPointer && self->pointer!=nullptr) { - self->releasePointer(); - } - - const bool hasKeyboard = (capabilities & WL_SEAT_CAPABILITY_KEYBOARD)!=0; - if(hasKeyboard && self->keyboard==nullptr) { - self->keyboard = wl_seat_get_keyboard(seat); - wl_keyboard_add_listener(self->keyboard, &wlKeyboardListener, self); - } - else if(!hasKeyboard && self->keyboard!=nullptr) { - self->releaseKeyboard(); - } - } - -void WaylandApi::Private::onWlPointerEnter(void* data, [[maybe_unused]] wl_pointer* pointer, uint32_t serial, - wl_surface* surface, wl_fixed_t x, wl_fixed_t y) { - // Like X11's EnterNotify: a move to the entry position, then re-apply the window's cursor - auto self = static_cast(data); - if(surface==nullptr) - return; // surface was destroyed after compositor sent the event - auto w = static_cast(wl_surface_get_user_data(surface)); // listener data of our wl_surface - self->pointerFocus = w; - self->pointerEnterSerial = serial; - // logical -> physical - w->cursorX = wl_fixed_to_double(x)*w->scale(); - w->cursorY = wl_fixed_to_double(y)*w->scale(); - if(w->owner==nullptr) - return; - if(self->isWindowFocused(w)) { - MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, e); - } - // Note: Wayland always requires cursor shape on enter (see implShowCursor) - SystemApi::showCursor(reinterpret_cast(w), SystemApi::cursorShape(*w->owner)); - } - -void WaylandApi::Private::onWlPointerLeave(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t serial, - [[maybe_unused]] wl_surface* surface) { - // 'surface' may be null if it was destroyed meanwhile. - static_cast(data)->pointerFocus = nullptr; - } - -void WaylandApi::Private::onWlPointerMotion(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t time, - wl_fixed_t x, wl_fixed_t y) { - // While a button is held, coordinates can be outside the window (implicit grab), like on X11. Not sent while locked. - auto self = static_cast(data); - WWindow* w = self->pointerFocus; - if(w==nullptr) - return; - w->cursorX = wl_fixed_to_double(x)*w->scale(); - w->cursorY = wl_fixed_to_double(y)*w->scale(); - if(w->owner==nullptr || !self->isWindowFocused(w)) - return; - MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, e); - } - -void WaylandApi::Private::onWlPointerButton(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t serial, - [[maybe_unused]] uint32_t time, uint32_t button, uint32_t state) { - // Unknown buttons become ButtonNone, like X11. Releases only for presses the app got (see heldButtons). - auto self = static_cast(data); - WWindow* w = self->pointerFocus; - if(w==nullptr || w->owner==nullptr) - return; - - if(state==WL_POINTER_BUTTON_STATE_PRESSED) { - if(!self->isWindowFocused(w)) - return; - // Focus click filter: the click that only focuses the window must not reach the app. KWin sends - // wl_keyboard.enter and that press together, so a press in the same readEvents() batch as the enter is that click. - if(self->focusGainedInBatch==w) - return; - if(std::find(self->heldButtons.begin(), self->heldButtons.end(), button)==self->heldButtons.end()) - self->heldButtons.push_back(button); - MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseDown); - SystemApi::dispatchMouseDown(*w->owner, e); - } else { - if(std::erase(self->heldButtons, button)==0) - return; - MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseUp); - SystemApi::dispatchMouseUp(*w->owner, e); - } - } - -void WaylandApi::Private::onWlPointerAxisValue120(void* data, [[maybe_unused]] wl_pointer* pointer, uint32_t axis, int32_t value120) { - // 120 per notch, like Tempest's delta on Windows; Wayland's sign is inverted (positive = down). - // Horizontal scrolling is ignored, like on X11. - if(axis!=WL_POINTER_AXIS_VERTICAL_SCROLL) - return; - - auto self = static_cast(data); - WWindow* w = self->pointerFocus; - if(w==nullptr || w->owner==nullptr || !self->isWindowFocused(w)) - return; - MouseEvent e = pointerEvent(*w, Event::ButtonNone, -value120, Event::MouseWheel); - SystemApi::dispatchMouseWheel(*w->owner, e); - } - -void WaylandApi::Private::onZwpRelativePointerV1RelativeMotion(void* data, [[maybe_unused]] zwp_relative_pointer_v1* relativePointer, - [[maybe_unused]] uint32_t utimeHi, [[maybe_unused]] uint32_t utimeLo, - [[maybe_unused]] wl_fixed_t dx, [[maybe_unused]] wl_fixed_t dy, - wl_fixed_t dxUnaccel, wl_fixed_t dyUnaccel) { - // Moves virtual cursor while pointer is locked (otherwise wl_pointer.motion has the position). Raw input deltas - // clamped to the window like a real cursor. - // Note: compositor only activates the lock while focused - auto self = static_cast(data); - WWindow* w = self->pointerFocus; - if(w==nullptr || !w->pointerLocked || w->owner==nullptr) - return; - const double s = w->scale(); - w->cursorX = std::clamp(w->cursorX + wl_fixed_to_double(dxUnaccel)*s, 0.0, double(std::max(w->physicalWidth() -1, 0))); - w->cursorY = std::clamp(w->cursorY + wl_fixed_to_double(dyUnaccel)*s, 0.0, double(std::max(w->physicalHeight()-1, 0))); - MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, e); - } - -void WaylandApi::Private::onWlKeyboardKeymap(void* data, [[maybe_unused]] wl_keyboard* keyboard, - uint32_t format, int32_t fd, uint32_t size) { - // Sent on creation and when the keymap changes. On failure the previous keymap stays. - // Note: need to close fd manually inside function! - auto self = static_cast(data); - if(format!=WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { - Log::e("WaylandApi: unsupported keymap format ", format); - close(fd); - return; - } - - // MAP_PRIVATE: required since wl_keyboard v7 - void* map = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); - close(fd); - if(map==MAP_FAILED) { - Log::e("WaylandApi: can't map the keymap: ", std::strerror(errno)); - return; - } - // The text is null-terminated within 'size'; strnlen makes sure we never read beyond the mapping. - const char* text = static_cast(map); - xkb_keymap* keymap = xkb_keymap_new_from_buffer(self->xkbContext, text, strnlen(text, size), - XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); - munmap(map, size); - if(keymap==nullptr) { - Log::e("WaylandApi: can't compile the keymap"); - return; - } - xkb_state* state = xkb_state_new(keymap); - if(state==nullptr) { - Log::e("WaylandApi: can't create the keyboard state"); - xkb_keymap_unref(keymap); - return; - } - - ioDestroy(self->xkbState, xkb_state_unref); - ioDestroy(self->xkbKeymap, xkb_keymap_unref); - self->xkbKeymap = keymap; - self->xkbState = state; - self->repeatKeycode = 0; // keycodes may mean something else in the new keymap - - const char* layout = xkb_keymap_layout_get_name(keymap, 0); - Log::i("WaylandApi: keymap loaded, ", xkb_keymap_num_layouts(keymap), " layout(s), first: ", (layout!=nullptr ? layout : "unnamed")); - } - -void WaylandApi::Private::onWlKeyboardEnter(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, - wl_surface* surface, [[maybe_unused]] wl_array* keys) { - // Like X11's FocusIn. Keys already held ('keys') are ignored: they aren't in heldKeys, so their releases are dropped. - auto self = static_cast(data); - if(surface==nullptr) - return; // the surface was destroyed after the compositor sent the event - auto w = static_cast(wl_surface_get_user_data(surface)); - self->keyboardFocus = w; - self->focusGainedInBatch = w; - if(w->owner==nullptr) - return; - FocusEvent e(true, Event::UnknownReason); - SystemApi::dispatchFocus(*w->owner, e); - // Pointer already over the window: like an enter, a move to its position and the app's cursor. - if(self->pointerFocus==w) { - MouseEvent m = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, m); - SystemApi::showCursor(reinterpret_cast(w), SystemApi::cursorShape(*w->owner)); - } - } - -void WaylandApi::Private::onWlKeyboardLeave(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, - [[maybe_unused]] wl_surface* surface) { - // Like X11's FocusOut, but held keys and buttons are released first. 'surface' may be null, so the window comes from - // keyboardFocus. - auto self = static_cast(data); - WWindow* w = self->keyboardFocus; - if(w==nullptr) - return; - // On focus loss the real releases never reach the app, so keys and buttons would stay held (e.g. Alt after Alt+Tab). - self->releaseHeldKeys(*w); - self->releaseHeldButtons(*w); - self->keyboardFocus = nullptr; - if(w->owner==nullptr) - return; - FocusEvent e(false, Event::UnknownReason); - SystemApi::dispatchFocus(*w->owner, e); - if(self->pointerFocus==w) - SystemApi::showCursor(reinterpret_cast(w), SystemApi::cursorShape(*w->owner)); - } - -void WaylandApi::Private::onWlKeyboardKey(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, - [[maybe_unused]] uint32_t time, uint32_t key, uint32_t state) { - // xkb keycodes are evdev + 8 (X11's keycodes); also the scancode for the dispatcher. - auto self = static_cast(data); - if(self->keyboardFocus==nullptr || self->keyboardFocus->owner==nullptr) - return; - if(self->xkbState==nullptr) - return; // no keymap compiled - // Only releases of keys the app got a press for are dispatched (see heldKeys). - const uint32_t keycode = key + 8; - if(state==WL_KEYBOARD_KEY_STATE_PRESSED) { - if(std::find(self->heldKeys.begin(), self->heldKeys.end(), keycode)==self->heldKeys.end()) - self->heldKeys.push_back(keycode); - KeyEvent e = self->keyEvent(keycode, Event::KeyDown); - SystemApi::dispatchKeyDown(*self->keyboardFocus->owner, e, keycode); - self->startKeyRepeat(keycode); - } else { - if(std::erase(self->heldKeys, keycode)==0) - return; - if(keycode==self->repeatKeycode) - self->repeatKeycode = 0; - KeyEvent e = self->keyEvent(keycode, Event::KeyUp); - SystemApi::dispatchKeyUp(*self->keyboardFocus->owner, e, keycode); - } - } - -void WaylandApi::Private::onWlKeyboardModifiers(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, - uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group) { - // Not dispatched: Tempest tracks Shift/Ctrl/Alt from the key events itself. xkb needs them for the text of a key - // (Shift, CapsLock) and for the active layout ('group'). - auto self = static_cast(data); - if(self->xkbState==nullptr) - return; // no keymap compiled - xkb_state_update_mask(self->xkbState, depressed, latched, locked, 0, 0, group); - } - -void WaylandApi::Private::onWlKeyboardRepeatInfo(void* data, [[maybe_unused]] wl_keyboard* keyboard, int32_t rate, int32_t delay) { - // The desktop setting; rate 0 = repeat disabled. - auto self = static_cast(data); - self->repeatRate = rate; - self->repeatDelay = delay; - if(rate<=0) - self->repeatKeycode = 0; - } - /// Window specific listeners // void WaylandApi::Private::onXdgSurfaceConfigure(void* data, [[maybe_unused]] xdg_surface* xdgSurface, uint32_t serial) { @@ -616,15 +264,6 @@ void WaylandApi::Private::onWpFractionalScaleV1PreferredScale(void* data, [[mayb w->preferredScale = scale; } -void WaylandApi::Private::onZwpLockedPointerV1Locked(void* data, [[maybe_unused]] zwp_locked_pointer_v1* lockedPointer) { - static_cast(data)->pointerLocked = true; - } - -void WaylandApi::Private::onZwpLockedPointerV1Unlocked(void* data, [[maybe_unused]] zwp_locked_pointer_v1* lockedPointer) { - // E.g. focus lost; the persistent lock may become active again later. - static_cast(data)->pointerLocked = false; - } - //// /// LISTENER TABLES // >> Note: Every event of the bound interface version needs a handler: libwayland aborts on null entry. @@ -638,35 +277,6 @@ const wl_registry_listener WaylandApi::Private::wlRegistryListener = { const xdg_wm_base_listener WaylandApi::Private::xdgWmBaseListener = { .ping = onXdgWmBasePing, }; -const wl_seat_listener WaylandApi::Private::wlSeatListener = { - .capabilities = onWlSeatCapabilities, - .name = ioIgnore, - }; -const wl_pointer_listener WaylandApi::Private::wlPointerListener = { - .enter = onWlPointerEnter, - .leave = onWlPointerLeave, - .motion = onWlPointerMotion, - .button = onWlPointerButton, - // TODO(wayland): touchpad scrolling: continuous axis events (axis_source finger) come without axis_value120 - .axis = ioIgnore, - .frame = ioIgnore, - .axis_source = ioIgnore, - .axis_stop = ioIgnore, - .axis_discrete = ioIgnore, // not sent since v8 (replaced by axis_value120) - .axis_value120 = onWlPointerAxisValue120, - // axis_relative_direction (only v9+) - }; -const zwp_relative_pointer_v1_listener WaylandApi::Private::zwpRelativePointerV1Listener = { - .relative_motion = onZwpRelativePointerV1RelativeMotion, - }; -const wl_keyboard_listener WaylandApi::Private::wlKeyboardListener = { - .keymap = onWlKeyboardKeymap, - .enter = onWlKeyboardEnter, - .leave = onWlKeyboardLeave, - .key = onWlKeyboardKey, - .modifiers = onWlKeyboardModifiers, - .repeat_info = onWlKeyboardRepeatInfo, - }; /// Window specific listener tables // @@ -692,10 +302,6 @@ const zxdg_toplevel_decoration_v1_listener WaylandApi::Private::zxdgToplevelDeco const wp_fractional_scale_v1_listener WaylandApi::Private::wpFractionalScaleV1Listener = { .preferred_scale = onWpFractionalScaleV1PreferredScale, }; -const zwp_locked_pointer_v1_listener WaylandApi::Private::zwpLockedPointerV1Listener = { - .locked = onZwpLockedPointerV1Locked, - .unlocked = onZwpLockedPointerV1Unlocked, - }; //// /// HELPERS @@ -722,155 +328,12 @@ void WaylandApi::Private::logDisplayError() const { } } -Event::MouseButton WaylandApi::Private::toButton(uint32_t button) { - // wl_pointer.button carries evdev codes. - switch(button) { - case BTN_LEFT: return Event::ButtonLeft; - case BTN_RIGHT: return Event::ButtonRight; - case BTN_MIDDLE: return Event::ButtonMid; - case BTN_SIDE: return Event::ButtonBack; - case BTN_EXTRA: return Event::ButtonForward; - } - return Event::ButtonNone; - } - -wp_cursor_shape_device_v1_shape WaylandApi::Private::toCursorShape(CursorShape shape) { - // Hidden is handled by the caller. - switch(shape) { - case CursorShape::Arrow: - case CursorShape::Hidden: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT; - case CursorShape::IBeam: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT; - case CursorShape::SizeVer: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE; - case CursorShape::SizeHor: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE; - case CursorShape::SizeBDiag: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE; // '/' - case CursorShape::SizeFDiag: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE; // '\' - case CursorShape::SizeAll: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL; // four arrows; all_resize needs v2 - } - return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT; - } - -MouseEvent WaylandApi::Private::pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type) { - // floor: the physical pixel the cursor is in. - const int x = int(std::floor(w.cursorX)); - const int y = int(std::floor(w.cursorY)); - return MouseEvent(x, y, button, Event::M_NoModifier, delta, 0, type); - } - -KeyEvent WaylandApi::Private::keyEvent(uint32_t keycode, Event::Type type) const { - // keycode: xkb keycode (evdev + 8). key: level-0 keysym of the active layout, so Shift+W stays K_W (like Windows' - // virtual keys). code: the typed character, 0 if none (e.g. arrows, dead keys). - const xkb_layout_index_t layout = xkb_state_key_get_layout(xkbState, keycode); - const xkb_keysym_t* syms = nullptr; - const int count = xkb_keymap_key_get_syms_by_level(xkbKeymap, keycode, layout, 0, &syms); - const xkb_keysym_t sym = count>0 ? syms[0] : XKB_KEY_NoSymbol; // choose first available - const auto key = Event::KeyType(SystemApi::translateKey(sym)); - - // Only characters up to U+FFFF (Basic Multilingual Plane): Tempest's text input can't hold more. - // Beyond it, nothing is typed (X11 and Windows deliver half a UTF-16 surrogate pair there). - const uint32_t utf32 = xkb_state_key_get_utf32(xkbState, keycode); - const uint32_t code = utf32<=0xFFFF ? utf32 : 0; - return KeyEvent(key, code, Event::M_NoModifier, type); - } - -bool WaylandApi::Private::isWindowFocused(const WWindow* w) const { - // Window focus = keyboard focus: mouse moves, wheel and presses only reach the focused window. - // Without a keyboard every window counts as focused (pointer events only come for the one under the pointer). - return keyboard==nullptr || keyboardFocus==w; - } - -void WaylandApi::Private::setPointerLock(WWindow& w, bool lock) { - // Mouse-look: Wayland apps can't move the pointer, so while the cursor is hidden it's locked in place and the app - // gets relative motion instead (Tempest has no relative-mouse API). Called on every cursor change. - // The lock stays requested: the compositor pauses it while the window is unfocused and resumes it by itself. - if(lock && w.lockedPointer==nullptr && pointer!=nullptr) { - w.lockedPointer = zwp_pointer_constraints_v1_lock_pointer(pointerConstraints, w.surface, pointer, nullptr, - ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); - zwp_locked_pointer_v1_add_listener(w.lockedPointer, &zwpLockedPointerV1Listener, &w); - } - else if(!lock && w.lockedPointer!=nullptr) { - ioDestroy(w.lockedPointer, zwp_locked_pointer_v1_destroy); - w.pointerLocked = false; - } - } - -void WaylandApi::Private::releaseHeldKeys(WWindow& w) { - repeatKeycode = 0; - auto keys = std::move(heldKeys); // save from manipulation - heldKeys.clear(); - if(w.owner==nullptr) - return; - for(uint32_t keycode:keys) { - KeyEvent e = keyEvent(keycode, Event::KeyUp); - SystemApi::dispatchKeyUp(*w.owner, e, keycode); - } - } - -void WaylandApi::Private::releaseHeldButtons(WWindow& w) { - auto buttons = std::move(heldButtons); // save from manipulation - heldButtons.clear(); - if(w.owner==nullptr) - return; - for(uint32_t button:buttons) { - MouseEvent e = pointerEvent(w, toButton(button), 0, Event::MouseUp); - SystemApi::dispatchMouseUp(*w.owner, e); - } - } - -void WaylandApi::Private::releasePointer() { - // release what the app saw pressed while pointer still exists - if(pointerFocus!=nullptr) - releaseHeldButtons(*pointerFocus); - // Objects created from the pointer first. - for(auto& w:windows) - setPointerLock(*w, false); - ioDestroy(relativePointer, zwp_relative_pointer_v1_destroy); - ioDestroy(cursorShapeDevice, wp_cursor_shape_device_v1_destroy); - static_assert(wlSeatVersion>=WL_POINTER_RELEASE_SINCE_VERSION); - ioDestroy(pointer, wl_pointer_release); - pointerFocus = nullptr; - heldButtons.clear(); - } - -void WaylandApi::Private::releaseKeyboard() { - // release what the app saw pressed while keyboard still exists - if(keyboardFocus!=nullptr) - releaseHeldKeys(*keyboardFocus); - static_assert(wlSeatVersion>=WL_KEYBOARD_RELEASE_SINCE_VERSION); - ioDestroy(keyboard, wl_keyboard_release); - keyboardFocus = nullptr; - repeatKeycode = 0; - heldKeys.clear(); - } - -void WaylandApi::Private::startKeyRepeat(uint32_t keycode) { - // Non-repeating keys (modifiers) leave a running repeat alone; a repeating key takes over. - if(repeatRate<=0 || !xkb_keymap_key_repeats(xkbKeymap, keycode)) - return; - repeatKeycode = keycode; - repeatNext = std::chrono::steady_clock::now() + std::chrono::milliseconds(repeatDelay); - } - -void WaylandApi::Private::processKeyRepeat() { - if(repeatKeycode==0) - return; - const auto now = std::chrono::steady_clock::now(); - const auto interval = std::chrono::microseconds(std::chrono::seconds(1))/repeatRate; - if(nowowner, e, repeatKeycode); - } - //// /// CONNECTION AND WINDOWS // bool WaylandApi::Private::hasRequiredGlobals() const { - // No fallbacks yet without fractional-scale, cursor-shape or server-side decorations (e.g. GNOME doesn't work). - // Pointer constraints + relative pointer: mouse-look, since Wayland clients can't warp the pointer. + // No fallbacks yet without fractional-scale or server-side decorations (e.g. GNOME doesn't work). struct Required { const void* object; const wl_interface& interface; @@ -882,10 +345,10 @@ bool WaylandApi::Private::hasRequiredGlobals() const { {decorationManager, zxdg_decoration_manager_v1_interface, zxdgDecorationManagerV1Version }, {viewporter, wp_viewporter_interface, wpViewporterVersion }, {fractionalScaleManager, wp_fractional_scale_manager_v1_interface, wpFractionalScaleManagerV1Version }, - {seat, wl_seat_interface, wlSeatVersion }, - {cursorShapeManager, wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version }, - {pointerConstraints, zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version }, - {relativePointerManager, zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version}, + // [input] {seat, wl_seat_interface, wlSeatVersion }, + // [input] {cursorShapeManager, wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version }, + // [input] {pointerConstraints, zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version }, + // [input] {relativePointerManager, zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version}, }; bool hasAll = true; for(auto& r:required) { @@ -914,12 +377,12 @@ bool WaylandApi::Private::connect() { return false; } - // Needed to compile keymap: without it there's no keyboard input - xkbContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - if(!xkbContext) { - Log::e("WaylandApi: xkb_context_new failed"); - return false; - } + // [input] // Needed to compile keymap: without it there's no keyboard input + // [input] xkbContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + // [input] if(!xkbContext) { + // [input] Log::e("WaylandApi: xkb_context_new failed"); + // [input] return false; + // [input] } registry = wl_display_get_registry(display); wl_registry_add_listener(registry, &wlRegistryListener, this); @@ -941,24 +404,24 @@ void WaylandApi::Private::disconnect() { while(!windows.empty()) destroyWindow(windows.back().get()); - ioDestroy(relativePointerManager, zwp_relative_pointer_manager_v1_destroy); - ioDestroy(pointerConstraints, zwp_pointer_constraints_v1_destroy); - ioDestroy(cursorShapeManager, wp_cursor_shape_manager_v1_destroy); + // [input] ioDestroy(relativePointerManager, zwp_relative_pointer_manager_v1_destroy); + // [input] ioDestroy(pointerConstraints, zwp_pointer_constraints_v1_destroy); + // [input] ioDestroy(cursorShapeManager, wp_cursor_shape_manager_v1_destroy); ioDestroy(fractionalScaleManager, wp_fractional_scale_manager_v1_destroy); ioDestroy(viewporter, wp_viewporter_destroy); ioDestroy(decorationManager, zxdg_decoration_manager_v1_destroy); ioDestroy(wmBase, xdg_wm_base_destroy); - // Seat devices before the seat - releaseKeyboard(); - releasePointer(); + // [input] // Seat devices before the seat + // [input] releaseKeyboard(); + // [input] releasePointer(); - ioDestroy(xkbState, xkb_state_unref); - ioDestroy(xkbKeymap, xkb_keymap_unref); - ioDestroy(xkbContext, xkb_context_unref); + // [input] ioDestroy(xkbState, xkb_state_unref); + // [input] ioDestroy(xkbKeymap, xkb_keymap_unref); + // [input] ioDestroy(xkbContext, xkb_context_unref); - static_assert(wlSeatVersion>=WL_SEAT_RELEASE_SINCE_VERSION); - ioDestroy(seat, wl_seat_release); + // [input] static_assert(wlSeatVersion>=WL_SEAT_RELEASE_SINCE_VERSION); + // [input] ioDestroy(seat, wl_seat_release); ioDestroy(compositor, wl_compositor_destroy); ioDestroy(registry, wl_registry_destroy); @@ -1025,21 +488,21 @@ void WaylandApi::Private::destroyWindow(WWindow* w) { if(w==nullptr) return; - // The compositor sends wl_pointer/wl_keyboard.leave for a destroyed surface, but only after we've deleted w. - if(pointerFocus==w) { - pointerFocus = nullptr; - heldButtons.clear(); - } - if(keyboardFocus==w) { - keyboardFocus = nullptr; - repeatKeycode = 0; - heldKeys.clear(); - heldButtons.clear(); - } - if(focusGainedInBatch==w) - focusGainedInBatch = nullptr; - - setPointerLock(*w, false); // created for the wl_surface + // [input] // The compositor sends wl_pointer/wl_keyboard.leave for a destroyed surface, but only after we've deleted w. + // [input] if(pointerFocus==w) { + // [input] pointerFocus = nullptr; + // [input] heldButtons.clear(); + // [input] } + // [input] if(keyboardFocus==w) { + // [input] keyboardFocus = nullptr; + // [input] repeatKeycode = 0; + // [input] heldKeys.clear(); + // [input] heldButtons.clear(); + // [input] } + // [input] if(focusGainedInBatch==w) + // [input] focusGainedInBatch = nullptr; + + // [input] setPointerLock(*w, false); // created for the wl_surface ioDestroy(w->fractionalScale, wp_fractional_scale_v1_destroy); ioDestroy(w->viewport, wp_viewport_destroy); ioDestroy(w->decoration, zxdg_toplevel_decoration_v1_destroy); // before toplevel @@ -1063,17 +526,18 @@ void WaylandApi::Private::handleConnectionError() { int WaylandApi::Private::pollTimeout() const { // 0 if a window may render; otherwise wait for the compositor (e.g. a configure that ends 'suspended'), - // at most idlePollTimeoutMs, and not beyond the next scheduled key repeat + // at most idlePollTimeoutMs + // [input] (and not beyond the next scheduled key repeat) // Longest sleep in poll() while no window may render; short enough to keep Tempest's timers accurate. constexpr int idlePollTimeoutMs = 5; const bool canRender = std::any_of(windows.begin(), windows.end(), [](const std::unique_ptr& w) { return w->readyToRender(); }); int timeout = canRender ? 0 : idlePollTimeoutMs; - if(repeatKeycode!=0) { - const auto untilRepeat = std::chrono::ceil(repeatNext - std::chrono::steady_clock::now()); - timeout = std::clamp(int(untilRepeat.count()), 0, timeout); - } + // [input] if(repeatKeycode!=0) { + // [input] const auto untilRepeat = std::chrono::ceil(repeatNext - std::chrono::steady_clock::now()); + // [input] timeout = std::clamp(int(untilRepeat.count()), 0, timeout); + // [input] } return timeout; } @@ -1081,7 +545,7 @@ void WaylandApi::Private::readEvents() { // Doesn't block beyond pollTimeout(): Tempest's loop must keep running (timers, rendering). // The Vulkan driver reads the same connection on its own queue; prepare_read -> read_events/cancel_read // is how several readers share the socket without stealing or blocking each other. - focusGainedInBatch = nullptr; // a new batch (see onWlPointerButton) + // [input] focusGainedInBatch = nullptr; // a new batch (see onWlPointerButton) while(wl_display_prepare_read(display)!=0) { // Our queue already holds events (e.g. read by the driver): dispatch them before reading more. if(wl_display_dispatch_pending(display)<0) { @@ -1111,7 +575,7 @@ void WaylandApi::Private::readEvents() { return; } - processKeyRepeat(); // synthetic key events + // [input] processKeyRepeat(); // synthetic key events } void WaylandApi::Private::updateState(WWindow& w) { @@ -1141,45 +605,7 @@ void WaylandApi::Private::renderWindows() { // WaylandApi::WaylandApi() { - // Same table as X11: XKB keysyms have the values of X11's keysyms. Letters, digits and F-keys are ranges - // (see SystemApi::translateKey). - static const TranslateKeyPair k[] = { - { XKB_KEY_Control_L, Event::K_LControl }, - { XKB_KEY_Control_R, Event::K_RControl }, - - { XKB_KEY_Shift_L, Event::K_LShift }, - { XKB_KEY_Shift_R, Event::K_RShift }, - - { XKB_KEY_Alt_L, Event::K_LAlt }, - { XKB_KEY_Alt_R, Event::K_RAlt }, - - { XKB_KEY_Insert, Event::K_Insert }, - { XKB_KEY_Delete, Event::K_Delete }, - { XKB_KEY_Home, Event::K_Home }, - { XKB_KEY_End, Event::K_End }, - { XKB_KEY_Page_Up, Event::K_PageUp }, - { XKB_KEY_Page_Down, Event::K_PageDown }, - - { XKB_KEY_Left, Event::K_Left }, - { XKB_KEY_Right, Event::K_Right }, - { XKB_KEY_Up, Event::K_Up }, - { XKB_KEY_Down, Event::K_Down }, - - { XKB_KEY_Escape, Event::K_ESCAPE }, - { XKB_KEY_Tab, Event::K_Tab }, - { XKB_KEY_BackSpace, Event::K_Back }, - { XKB_KEY_Pause, Event::K_Pause }, - { XKB_KEY_Return, Event::K_Return }, - { XKB_KEY_space, Event::K_Space }, - { XKB_KEY_Caps_Lock, Event::K_CapsLock }, - - { XKB_KEY_F1, Event::K_F1 }, - { XKB_KEY_0, Event::K_0 }, - { XKB_KEY_a, Event::K_A }, - - { 0, Event::K_NoKey } - }; - setupKeyTranslate(k,24); + // [input] key table + setupKeyTranslate(k,24): see notes/split-removed.md impl.reset(new Private()); if(impl->connect()) { @@ -1264,30 +690,30 @@ void WaylandApi::implSetWindowTitle(SystemApi::Window* w, const char* utf8) { xdg_toplevel_set_title(toWWindow(w)->toplevel, utf8); } -void WaylandApi::implSetCursorPosition(SystemApi::Window* w, int x, int y) { - // Wayland clients can't move the pointer. While it's locked, the virtual cursor is all the app sees, so setting it is - // enough (no MouseMove for the jump). - auto ww = toWWindow(w); - if(!ww->pointerLocked) - return; - ww->cursorX = x; - ww->cursorY = y; - } - -void WaylandApi::implShowCursor(SystemApi::Window* w, CursorShape show) { - // The pointer lock is requested whenever the cursor is hidden, even with the pointer elsewhere: the compositor - // activates it once the pointer is over the focused window (see Private::setPointerLock). - // The cursor image needs the enter serial, so it's only set while the pointer is over w; enter and focus changes - // re-apply it. An unfocused window shows the desktop cursor. - auto ww = toWWindow(w); - impl->setPointerLock(*ww, show==CursorShape::Hidden); - if(impl->pointerFocus!=ww) - return; - const CursorShape shape = impl->isWindowFocused(ww) ? show : CursorShape::Arrow; - if(shape==CursorShape::Hidden) - wl_pointer_set_cursor(impl->pointer, impl->pointerEnterSerial, nullptr, 0, 0); - else - wp_cursor_shape_device_v1_set_shape(impl->cursorShapeDevice, impl->pointerEnterSerial, Private::toCursorShape(shape)); +void WaylandApi::implSetCursorPosition([[maybe_unused]] SystemApi::Window* w, [[maybe_unused]] int x, [[maybe_unused]] int y) { + // [input] // Wayland clients can't move the pointer. While it's locked, the virtual cursor is all the app sees, so setting it is + // [input] // enough (no MouseMove for the jump). + // [input] auto ww = toWWindow(w); + // [input] if(!ww->pointerLocked) + // [input] return; + // [input] ww->cursorX = x; + // [input] ww->cursorY = y; + } + +void WaylandApi::implShowCursor([[maybe_unused]] SystemApi::Window* w, [[maybe_unused]] CursorShape show) { + // [input] // The pointer lock is requested whenever the cursor is hidden, even with the pointer elsewhere: the compositor + // [input] // activates it once the pointer is over the focused window (see Private::setPointerLock). + // [input] // The cursor image needs the enter serial, so it's only set while the pointer is over w; enter and focus changes + // [input] // re-apply it. An unfocused window shows the desktop cursor. + // [input] auto ww = toWWindow(w); + // [input] impl->setPointerLock(*ww, show==CursorShape::Hidden); + // [input] if(impl->pointerFocus!=ww) + // [input] return; + // [input] const CursorShape shape = impl->isWindowFocused(ww) ? show : CursorShape::Arrow; + // [input] if(shape==CursorShape::Hidden) + // [input] wl_pointer_set_cursor(impl->pointer, impl->pointerEnterSerial, nullptr, 0, 0); + // [input] else + // [input] wp_cursor_shape_device_v1_set_shape(impl->cursorShapeDevice, impl->pointerEnterSerial, Private::toCursorShape(shape)); } bool WaylandApi::implIsRunning() { From 934b0afd8c8d3786b98ef5f98819415662355ea2 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:25:05 +0200 Subject: [PATCH 03/17] remove trivial input code --- Engine/system/api/waylandapi.cpp | 44 +++----------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 7d301aca..00227544 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -174,25 +174,7 @@ void WaylandApi::Private::onWlRegistryGlobal(void* data, wl_registry* registry, self->fractionalScaleManager = static_cast( wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, wpFractionalScaleManagerV1Version)); } - // [input] if(std::strcmp(interface, wp_cursor_shape_manager_v1_interface.name)==0 && version>=wpCursorShapeManagerV1Version) { - // [input] self->cursorShapeManager = static_cast( - // [input] wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version)); - // [input] } - // [input] if(std::strcmp(interface, zwp_pointer_constraints_v1_interface.name)==0 && version>=zwpPointerConstraintsV1Version) { - // [input] self->pointerConstraints = static_cast( - // [input] wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version)); - // [input] } - // [input] if(std::strcmp(interface, zwp_relative_pointer_manager_v1_interface.name)==0 && version>=zwpRelativePointerManagerV1Version) { - // [input] self->relativePointerManager = static_cast( - // [input] wl_registry_bind(registry, name, &zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version)); - // [input] } - // [input] if(std::strcmp(interface, wl_seat_interface.name)==0 && version>=wlSeatVersion && self->seat==nullptr) { - // [input] // Only the first seat: multi-seat setups are rare, and Tempest has a single pointer/keyboard. - // [input] static_assert(wlSeatVersionseat = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, wlSeatVersion)); - // [input] wl_seat_add_listener(self->seat, &wlSeatListener, self); - // [input] } + // [input] } void WaylandApi::Private::onXdgWmBasePing([[maybe_unused]] void* data, xdg_wm_base* wmBase, uint32_t serial) { @@ -605,7 +587,7 @@ void WaylandApi::Private::renderWindows() { // WaylandApi::WaylandApi() { - // [input] key table + setupKeyTranslate(k,24): see notes/split-removed.md + // [input] key table + setupKeyTranslate(k,24) impl.reset(new Private()); if(impl->connect()) { @@ -691,29 +673,11 @@ void WaylandApi::implSetWindowTitle(SystemApi::Window* w, const char* utf8) { } void WaylandApi::implSetCursorPosition([[maybe_unused]] SystemApi::Window* w, [[maybe_unused]] int x, [[maybe_unused]] int y) { - // [input] // Wayland clients can't move the pointer. While it's locked, the virtual cursor is all the app sees, so setting it is - // [input] // enough (no MouseMove for the jump). - // [input] auto ww = toWWindow(w); - // [input] if(!ww->pointerLocked) - // [input] return; - // [input] ww->cursorX = x; - // [input] ww->cursorY = y; + // [input] } void WaylandApi::implShowCursor([[maybe_unused]] SystemApi::Window* w, [[maybe_unused]] CursorShape show) { - // [input] // The pointer lock is requested whenever the cursor is hidden, even with the pointer elsewhere: the compositor - // [input] // activates it once the pointer is over the focused window (see Private::setPointerLock). - // [input] // The cursor image needs the enter serial, so it's only set while the pointer is over w; enter and focus changes - // [input] // re-apply it. An unfocused window shows the desktop cursor. - // [input] auto ww = toWWindow(w); - // [input] impl->setPointerLock(*ww, show==CursorShape::Hidden); - // [input] if(impl->pointerFocus!=ww) - // [input] return; - // [input] const CursorShape shape = impl->isWindowFocused(ww) ? show : CursorShape::Arrow; - // [input] if(shape==CursorShape::Hidden) - // [input] wl_pointer_set_cursor(impl->pointer, impl->pointerEnterSerial, nullptr, 0, 0); - // [input] else - // [input] wp_cursor_shape_device_v1_set_shape(impl->cursorShapeDevice, impl->pointerEnterSerial, Private::toCursorShape(shape)); + // [input] } bool WaylandApi::implIsRunning() { From a9c6613210c7e274bf79c6377166e53ba9a1c4e7 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:39:37 +0200 Subject: [PATCH 04/17] wl_compositor version lowering --- Engine/system/api/waylandapi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 00227544..167fd900 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -96,7 +96,7 @@ struct WaylandApi::Private { wp_fractional_scale_manager_v1* fractionalScaleManager = nullptr; // Exact versions we need to bind successfully: listeners and requests are written for these - static constexpr uint32_t wlCompositorVersion = 6; + static constexpr uint32_t wlCompositorVersion = 1; static constexpr uint32_t xdgWmBaseVersion = 6; // v6: xdg_toplevel state 'suspended' static constexpr uint32_t zxdgDecorationManagerV1Version = 1; static constexpr uint32_t wpViewporterVersion = 1; From ec00dbf73bf57fa01ac3d0edf2a525faa88edd12 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:13:10 +0200 Subject: [PATCH 05/17] return native window handle --- Engine/gapi/vulkan/vswapchain.cpp | 2 +- Engine/system/api/waylandapi.cpp | 33 ++++++++++++++----------------- Engine/system/api/waylandapi.h | 2 -- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Engine/gapi/vulkan/vswapchain.cpp b/Engine/gapi/vulkan/vswapchain.cpp index 01f259aa..9fd2ef09 100644 --- a/Engine/gapi/vulkan/vswapchain.cpp +++ b/Engine/gapi/vulkan/vswapchain.cpp @@ -281,7 +281,7 @@ VkResult VSwapchain::createSurface(VkInstance instance, void* hwnd, VkSurfaceKHR VkWaylandSurfaceCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR; createInfo.display = dpy; - createInfo.surface = WaylandApi::surface(reinterpret_cast(hwnd)); + createInfo.surface = reinterpret_cast(hwnd); return vkCreateWaylandSurfaceKHR(instance, &createInfo, nullptr, pSurface); } #endif diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 167fd900..a74f5c4a 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -33,7 +33,7 @@ static std::atomic_bool isExit{0}; /// WINDOW STATE // -// Per-window state. The SystemApi::Window* handed to Tempest points to this struct. +// Per-window state. user data in SystemApi::Window*/wl_surface* handed to Tempest struct WWindow { Tempest::Window* owner = nullptr; @@ -77,7 +77,10 @@ struct WWindow { }; static WWindow* toWWindow(SystemApi::Window* w) { - return reinterpret_cast(w); + // the wl_surface listener's data (= its user data) is the WWindow, see createWindow() + if(w==nullptr) + return nullptr; + return static_cast(wl_surface_get_user_data(reinterpret_cast(w))); } //// @@ -133,11 +136,11 @@ struct WaylandApi::Private { void logDisplayError() const; // Connection and windows - bool hasRequiredGlobals() const; - bool connect(); - void disconnect(); - WWindow* createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm); - void destroyWindow(WWindow* w); + bool hasRequiredGlobals() const; + bool connect(); + void disconnect(); + SystemApi::Window* createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm); + void destroyWindow(WWindow* w); // Event loop void handleConnectionError(); @@ -412,7 +415,7 @@ void WaylandApi::Private::disconnect() { display = nullptr; } -WWindow* WaylandApi::Private::createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm) { +SystemApi::Window* WaylandApi::Private::createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm) { // all roles and state requests needed before the first commit windows.emplace_back(std::make_unique()); WWindow* win = windows.back().get(); @@ -421,7 +424,7 @@ WWindow* WaylandApi::Private::createWindow(Tempest::Window* owner, uint32_t widt win->appRequestedHeight = int32_t(height); win->surface = wl_compositor_create_surface(compositor); - wl_surface_add_listener(win->surface, &wlSurfaceListener, win); + wl_surface_add_listener(win->surface, &wlSurfaceListener, win); // also attaches WWindow user data to wl_surface win->xdgSurface = xdg_wm_base_get_xdg_surface(wmBase, win->surface); xdg_surface_add_listener(win->xdgSurface, &xdgSurfaceListener, win); @@ -461,7 +464,7 @@ WWindow* WaylandApi::Private::createWindow(Tempest::Window* owner, uint32_t widt // Window's constructor reads the client rect itself after this returns: no SizeEvent for this size. win->dispatchedWidth = win->physicalWidth(); win->dispatchedHeight = win->physicalHeight(); - return win; + return reinterpret_cast(win->surface); // wl_surface* native handle } void WaylandApi::Private::destroyWindow(WWindow* w) { @@ -615,16 +618,10 @@ wl_display* WaylandApi::display() { return dpy; } -wl_surface* WaylandApi::surface(SystemApi::Window* w) { - if(dpy==nullptr || w==nullptr) - return nullptr; - return toWWindow(w)->surface; - } - SystemApi::Window* WaylandApi::implCreateWindow(Tempest::Window* owner, uint32_t width, uint32_t height) { if(dpy==nullptr) return nullptr; - return reinterpret_cast(impl->createWindow(owner,width,height,SystemApi::Normal)); + return impl->createWindow(owner,width,height,SystemApi::Normal); } SystemApi::Window* WaylandApi::implCreateWindow(Tempest::Window* owner, ShowMode sm) { @@ -632,7 +629,7 @@ SystemApi::Window* WaylandApi::implCreateWindow(Tempest::Window* owner, ShowMode // Hidden treated as Normal. if(dpy==nullptr) return nullptr; - return reinterpret_cast(impl->createWindow(owner,800,600,sm)); + return impl->createWindow(owner,800,600,sm); } void WaylandApi::implDestroyWindow(SystemApi::Window* w) { diff --git a/Engine/system/api/waylandapi.h b/Engine/system/api/waylandapi.h index 79f70c53..98fe0967 100644 --- a/Engine/system/api/waylandapi.h +++ b/Engine/system/api/waylandapi.h @@ -4,7 +4,6 @@ // opaque libwayland types struct wl_display; -struct wl_surface; namespace Tempest { @@ -13,7 +12,6 @@ class WaylandApi final: SystemApi { // For Vulkan (vulkanapi.cpp, vswapchain.cpp). // nullptr if Wayland is not the active backend static wl_display* display(); - static wl_surface* surface(SystemApi::Window* w); // TEMPEST_DISABLE_WAYLAND=1: X11 is the backend static bool isDisabled(); From a7fd661eddb10628c0befdd25d2d849945496b5c Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:45:46 +0200 Subject: [PATCH 06/17] Rework event loop to single events --- Engine/system/api/waylandapi.cpp | 67 +++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index a74f5c4a..3621afdf 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ struct WWindow { bool fullscreen = false; bool suspended = false; // compositor doesn't repaint the window (e.g. minimized): pause rendering bool configured = false; // first configure acked; no buffer (= Vulkan present) before that + bool closeQueued = false; // a Close is in appEvents: further close requests add nothing until it's dispatched int32_t appRequestedWidth = 0; // physical; used until the compositor sends a size int32_t appRequestedHeight = 0; @@ -83,6 +85,21 @@ static WWindow* toWWindow(SystemApi::Window* w) { return static_cast(wl_surface_get_user_data(reinterpret_cast(w))); } +//// +/// APP EVENTS +// + +// Events for the app/systemAPI. Listeners only record them. implProcessEvents() dispatches one per call +struct AppEvent { + enum Type : uint8_t { + Close, + }; + Type type; + WWindow* w; + }; + +static std::deque appEvents; // in the order the compositor sent them + //// /// SESSION STATE // @@ -134,6 +151,7 @@ struct WaylandApi::Private { static constexpr auto ioIgnore = [](auto...) {}; // useful to silence Wayland listener stubs template static void ioDestroy(T*& obj, void (*destroyFn)(T*)); void logDisplayError() const; + static void dispatchCloseEvent(WWindow& w); // Connection and windows bool hasRequiredGlobals() const; @@ -146,6 +164,7 @@ struct WaylandApi::Private { void handleConnectionError(); int pollTimeout() const; void readEvents(); + static bool dispatchAppEvent(); static void updateState(WWindow& w); void renderWindows(); }; @@ -229,14 +248,12 @@ void WaylandApi::Private::onXdgToplevelConfigure(void* data, [[maybe_unused]] xd } void WaylandApi::Private::onXdgToplevelClose(void* data, [[maybe_unused]] xdg_toplevel* toplevel) { - // Only a request. Like the Windows backend: the app or an overlay may accept the CloseEvent; otherwise exit. + // Only recorded, see dispatchCloseEvent(). Requests before that one is dispatched count as one. auto w = static_cast(data); - if(w->owner==nullptr) + if(w->closeQueued) return; - CloseEvent e; - SystemApi::dispatchClose(*w->owner, e); - if(!e.isAccepted()) - SystemApi::exit(); + w->closeQueued = true; + appEvents.push_back({AppEvent::Close, w}); } void WaylandApi::Private::onZxdgToplevelDecorationV1Configure([[maybe_unused]] void* data, [[maybe_unused]] zxdg_toplevel_decoration_v1* decoration, uint32_t mode) { @@ -313,6 +330,18 @@ void WaylandApi::Private::logDisplayError() const { } } +void WaylandApi::Private::dispatchCloseEvent(WWindow& w) { + // Only a request. Like the Windows backend: the app or an overlay may accept the CloseEvent; otherwise exit. + // w isn't used after the dispatch: the app may destroy the window while handling it. + w.closeQueued = false; + if(w.owner==nullptr) + return; + CloseEvent e; + SystemApi::dispatchClose(*w.owner, e); + if(!e.isAccepted()) + SystemApi::exit(); + } + //// /// CONNECTION AND WINDOWS // @@ -495,7 +524,8 @@ void WaylandApi::Private::destroyWindow(WWindow* w) { ioDestroy(w->xdgSurface, xdg_surface_destroy); ioDestroy(w->surface, wl_surface_destroy); - // After deletion events still queued for its objects are dropped by libwayland + // Our recorded events for w are dropped here; events still queued for its objects are dropped by libwayland + std::erase_if(appEvents, [w](const AppEvent& e) { return e.w==w; }); std::erase_if(windows, [w](const std::unique_ptr& p) { return p.get()==w; }); } @@ -563,6 +593,20 @@ void WaylandApi::Private::readEvents() { // [input] processKeyRepeat(); // synthetic key events } +bool WaylandApi::Private::dispatchAppEvent() { + // At most one event. Taken out before dispatching, so a nested event loop continues with the next one. + if(appEvents.empty()) + return false; + const AppEvent e = appEvents.front(); + appEvents.pop_front(); + switch(e.type) { + case AppEvent::Close: + dispatchCloseEvent(*e.w); + break; + } + return true; + } + void WaylandApi::Private::updateState(WWindow& w) { // Update things that should change only once per draw, not on every triggered callback (e.g. swapchain resize) const int32_t pw = w.physicalWidth(); @@ -689,9 +733,14 @@ int WaylandApi::implExec(SystemApi::AppCallBack& cb) { } void WaylandApi::implProcessEvents(SystemApi::AppCallBack& cb) { - // Unlike X11, handles all pending events and then renders, so a steady stream of events can't starve rendering. - // readEvents() only waits while no window may render (see pollTimeout()). + // Like X11: one app event per call, else timers and rendering. Wayland events are read (all at once, libwayland + // can't do less) only when no app event is left; their listeners only record. Configures change state only, so they + // can't starve rendering. readEvents() only waits while no window may render (see pollTimeout()). + if(impl->dispatchAppEvent()) + return; impl->readEvents(); + if(impl->dispatchAppEvent()) + return; if(cb.onTimer()==0) std::this_thread::yield(); impl->renderWindows(); From 7fcf4f796c0fdda99d840d89a13f07aa29027d2e Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:44:01 +0200 Subject: [PATCH 07/17] fix appevent structure --- Engine/system/api/waylandapi.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 3621afdf..5b64e477 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -91,15 +91,10 @@ static WWindow* toWWindow(SystemApi::Window* w) { // Events for the app/systemAPI. Listeners only record them. implProcessEvents() dispatches one per call struct AppEvent { - enum Type : uint8_t { - Close, - }; - Type type; - WWindow* w; + Tempest::Event::Type type; + WWindow* w; }; -static std::deque appEvents; // in the order the compositor sent them - //// /// SESSION STATE // @@ -123,6 +118,7 @@ struct WaylandApi::Private { static constexpr uint32_t wpFractionalScaleManagerV1Version = 1; std::vector> windows; // owns the windows; each WWindow keeps its address (handle, listener data) + static inline std::deque appEvents; // in the order the compositor sent them // Session listeners. 'data' is Private*. static void onWlRegistryGlobal(void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version); @@ -253,7 +249,7 @@ void WaylandApi::Private::onXdgToplevelClose(void* data, [[maybe_unused]] xdg_to if(w->closeQueued) return; w->closeQueued = true; - appEvents.push_back({AppEvent::Close, w}); + appEvents.push_back({Event::Close, w}); } void WaylandApi::Private::onZxdgToplevelDecorationV1Configure([[maybe_unused]] void* data, [[maybe_unused]] zxdg_toplevel_decoration_v1* decoration, uint32_t mode) { @@ -600,9 +596,11 @@ bool WaylandApi::Private::dispatchAppEvent() { const AppEvent e = appEvents.front(); appEvents.pop_front(); switch(e.type) { - case AppEvent::Close: + case Event::Close: dispatchCloseEvent(*e.w); break; + default: + break; } return true; } From 2b89c92cda351d9566d9f18348a476a7f7a7419f Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:49:35 +0200 Subject: [PATCH 08/17] relax close handling --- Engine/system/api/waylandapi.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 5b64e477..3994ff32 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -60,7 +60,6 @@ struct WWindow { bool fullscreen = false; bool suspended = false; // compositor doesn't repaint the window (e.g. minimized): pause rendering bool configured = false; // first configure acked; no buffer (= Vulkan present) before that - bool closeQueued = false; // a Close is in appEvents: further close requests add nothing until it's dispatched int32_t appRequestedWidth = 0; // physical; used until the compositor sends a size int32_t appRequestedHeight = 0; @@ -244,11 +243,8 @@ void WaylandApi::Private::onXdgToplevelConfigure(void* data, [[maybe_unused]] xd } void WaylandApi::Private::onXdgToplevelClose(void* data, [[maybe_unused]] xdg_toplevel* toplevel) { - // Only recorded, see dispatchCloseEvent(). Requests before that one is dispatched count as one. + // Only recorded, see dispatchCloseEvent(). auto w = static_cast(data); - if(w->closeQueued) - return; - w->closeQueued = true; appEvents.push_back({Event::Close, w}); } @@ -329,7 +325,6 @@ void WaylandApi::Private::logDisplayError() const { void WaylandApi::Private::dispatchCloseEvent(WWindow& w) { // Only a request. Like the Windows backend: the app or an overlay may accept the CloseEvent; otherwise exit. // w isn't used after the dispatch: the app may destroy the window while handling it. - w.closeQueued = false; if(w.owner==nullptr) return; CloseEvent e; From 8540598ba5b7bb94e2c72daa49d06c7e3ed89fc1 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:52:17 +0200 Subject: [PATCH 09/17] Throw on failed wayland connect --- Engine/exceptions/exception.cpp | 2 ++ Engine/exceptions/exception.h | 11 ++++++----- Engine/system/api/waylandapi.cpp | 22 ++++++++-------------- Engine/system/api/waylandapi.h | 2 +- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/Engine/exceptions/exception.cpp b/Engine/exceptions/exception.cpp index aa5dd6cd..4941a831 100644 --- a/Engine/exceptions/exception.cpp +++ b/Engine/exceptions/exception.cpp @@ -24,6 +24,8 @@ std::string SystemErrCategory::message(int ev) const { return "Unable to load asset"; case SystemErrc::UnableToSaveAsset: return "Unable to save asset"; + case SystemErrc::UnableToConnectDisplay: + return "Unable to connect to the display server"; } return "(unrecognized error)"; } diff --git a/Engine/exceptions/exception.h b/Engine/exceptions/exception.h index 32ed4d45..4a4bdf8c 100644 --- a/Engine/exceptions/exception.h +++ b/Engine/exceptions/exception.h @@ -52,11 +52,12 @@ class BadTextureCastException : public std::bad_cast { }; enum class SystemErrc { - InvalidWindowClass = 0, - UnableToCreateWindow = 1, - UnableToOpenFile = 2, - UnableToLoadAsset = 3, - UnableToSaveAsset = 4, + InvalidWindowClass = 0, + UnableToCreateWindow = 1, + UnableToOpenFile = 2, + UnableToLoadAsset = 3, + UnableToSaveAsset = 4, + UnableToConnectDisplay = 5, }; struct SystemErrCategory : std::error_category { diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 3994ff32..e5904643 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -2,6 +2,7 @@ #if defined(TEMPEST_BUILD_WAYLAND) #include +#include #include #include @@ -151,7 +152,7 @@ struct WaylandApi::Private { // Connection and windows bool hasRequiredGlobals() const; bool connect(); - void disconnect(); + ~Private(); SystemApi::Window* createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm); void destroyWindow(WWindow* w); @@ -399,8 +400,8 @@ bool WaylandApi::Private::connect() { return hasRequiredGlobals(); } -void WaylandApi::Private::disconnect() { - // Handles partial state and is safe to call twice (constructor on failure, destructor). +WaylandApi::Private::~Private() { + // Also runs for a half-connected state when the WaylandApi constructor throws. // Connection errors are ignored while tearing down. if(!display) return; @@ -432,7 +433,6 @@ void WaylandApi::Private::disconnect() { wl_display_flush(display); wl_display_disconnect(display); - display = nullptr; } SystemApi::Window* WaylandApi::Private::createWindow(Tempest::Window* owner, uint32_t width, uint32_t height, SystemApi::ShowMode sm) { @@ -630,18 +630,16 @@ WaylandApi::WaylandApi() { // [input] key table + setupKeyTranslate(k,24) impl.reset(new Private()); - if(impl->connect()) { - dpy = impl->display; // display() non-null only while connected - } else { - impl->disconnect(); // leave nothing half-connected + if(!impl->connect()) { + // impl's destructor cleans up the half-connected state Log::e("WaylandApi: Wayland is not usable (use TEMPEST_DISABLE_WAYLAND=1 for X11)"); + throw std::system_error(Tempest::SystemErrc::UnableToConnectDisplay); } + dpy = impl->display; } WaylandApi::~WaylandApi() { dpy = nullptr; - impl->disconnect(); - impl.reset(); } bool WaylandApi::isDisabled() { @@ -656,16 +654,12 @@ wl_display* WaylandApi::display() { } SystemApi::Window* WaylandApi::implCreateWindow(Tempest::Window* owner, uint32_t width, uint32_t height) { - if(dpy==nullptr) - return nullptr; return impl->createWindow(owner,width,height,SystemApi::Normal); } SystemApi::Window* WaylandApi::implCreateWindow(Tempest::Window* owner, ShowMode sm) { // 800x600 like the other backends; Maximized/FullScreen get their size from the first configure. // Hidden treated as Normal. - if(dpy==nullptr) - return nullptr; return impl->createWindow(owner,800,600,sm); } diff --git a/Engine/system/api/waylandapi.h b/Engine/system/api/waylandapi.h index 98fe0967..76d90bbf 100644 --- a/Engine/system/api/waylandapi.h +++ b/Engine/system/api/waylandapi.h @@ -10,7 +10,7 @@ namespace Tempest { class WaylandApi final: SystemApi { public: // For Vulkan (vulkanapi.cpp, vswapchain.cpp). - // nullptr if Wayland is not the active backend + // nullptr if Wayland is not the active backend; throws if Wayland is the backend but not usable static wl_display* display(); // TEMPEST_DISABLE_WAYLAND=1: X11 is the backend From 6b4236b0db444e44091eb802564435bfbb7ac956 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:58:52 +0200 Subject: [PATCH 10/17] include timeToTimer feature --- Engine/system/api/waylandapi.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index e5904643..67abf022 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -158,8 +158,8 @@ struct WaylandApi::Private { // Event loop void handleConnectionError(); - int pollTimeout() const; - void readEvents(); + int pollTimeout(const SystemApi::AppCallBack& cb) const; + void readEvents(const SystemApi::AppCallBack& cb); static bool dispatchAppEvent(); static void updateState(WWindow& w); void renderWindows(); @@ -530,16 +530,18 @@ void WaylandApi::Private::handleConnectionError() { SystemApi::exit(); } -int WaylandApi::Private::pollTimeout() const { +int WaylandApi::Private::pollTimeout(const SystemApi::AppCallBack& cb) const { // 0 if a window may render; otherwise wait for the compositor (e.g. a configure that ends 'suspended'), - // at most idlePollTimeoutMs + // but not beyond the next timer and at most idlePollTimeoutMs // [input] (and not beyond the next scheduled key repeat) - // Longest sleep in poll() while no window may render; short enough to keep Tempest's timers accurate. + // Longest sleep in poll() while no window may render constexpr int idlePollTimeoutMs = 5; const bool canRender = std::any_of(windows.begin(), windows.end(), [](const std::unique_ptr& w) { return w->readyToRender(); }); - int timeout = canRender ? 0 : idlePollTimeoutMs; + if(canRender) + return 0; + int timeout = int(std::min(cb.timeToTimer(), idlePollTimeoutMs)); // [input] if(repeatKeycode!=0) { // [input] const auto untilRepeat = std::chrono::ceil(repeatNext - std::chrono::steady_clock::now()); // [input] timeout = std::clamp(int(untilRepeat.count()), 0, timeout); @@ -547,7 +549,7 @@ int WaylandApi::Private::pollTimeout() const { return timeout; } -void WaylandApi::Private::readEvents() { +void WaylandApi::Private::readEvents(const SystemApi::AppCallBack& cb) { // Doesn't block beyond pollTimeout(): Tempest's loop must keep running (timers, rendering). // The Vulkan driver reads the same connection on its own queue; prepare_read -> read_events/cancel_read // is how several readers share the socket without stealing or blocking each other. @@ -567,7 +569,7 @@ void WaylandApi::Private::readEvents() { fd.fd = wl_display_get_fd(display); fd.events = POLLIN; // Timeout computed after dispatching the queued events: a configure among them may let a window render now. - if(poll(&fd, 1, pollTimeout())>0) { + if(poll(&fd, 1, pollTimeout(cb))>0) { if(wl_display_read_events(display)<0) { handleConnectionError(); return; @@ -725,7 +727,7 @@ void WaylandApi::implProcessEvents(SystemApi::AppCallBack& cb) { // can't starve rendering. readEvents() only waits while no window may render (see pollTimeout()). if(impl->dispatchAppEvent()) return; - impl->readEvents(); + impl->readEvents(cb); if(impl->dispatchAppEvent()) return; if(cb.onTimer()==0) From d3f5f9e63c6115be3d41221d7a8528736d5e35be Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:20:33 +0200 Subject: [PATCH 11/17] remove appRequestedWidth/Height --- Engine/system/api/waylandapi.cpp | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 67abf022..b45a1dbc 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -62,9 +62,6 @@ struct WWindow { bool suspended = false; // compositor doesn't repaint the window (e.g. minimized): pause rendering bool configured = false; // first configure acked; no buffer (= Vulkan present) before that - int32_t appRequestedWidth = 0; // physical; used until the compositor sends a size - int32_t appRequestedHeight = 0; - // size the app last saw; updateState() dispatches a resize when it differs. int32_t dispatchedWidth = 0; // physical int32_t dispatchedHeight = 0; @@ -204,19 +201,15 @@ void WaylandApi::Private::onXdgWmBasePing([[maybe_unused]] void* data, xdg_wm_ba void WaylandApi::Private::onXdgSurfaceConfigure(void* data, [[maybe_unused]] xdg_surface* xdgSurface, uint32_t serial) { // End of configure sequence (after xdg_toplevel.configure etc.): pending becomes current and is acked auto w = static_cast(data); - if(w->pending.width==0 || w->pending.height==0) { - // pending is 0 only if the compositor never sent a size (first configure) - // client decides: app requested size needs conversion to logical (KWin sends preferred_scale before the first configure). - w->pending.width = std::max(1, int32_t(std::lround(float(w->appRequestedWidth) /w->scale()))); - w->pending.height = std::max(1, int32_t(std::lround(float(w->appRequestedHeight)/w->scale()))); - } w->width = w->pending.width; w->height = w->pending.height; w->fullscreen = w->pending.fullscreen; w->suspended = w->pending.suspended; // Viewport state is double-buffered: it applies with the next commit, which Vulkan does in vkQueuePresent. - wp_viewport_set_destination(w->viewport, w->width, w->height); + // Size 0 only if the compositor never sent one (first configure): createWindow() picks the size then. + if(w->width>0 && w->height>0) + wp_viewport_set_destination(w->viewport, w->width, w->height); xdg_surface_ack_configure(w->xdgSurface, serial); w->configured = true; @@ -439,9 +432,7 @@ SystemApi::Window* WaylandApi::Private::createWindow(Tempest::Window* owner, uin // all roles and state requests needed before the first commit windows.emplace_back(std::make_unique()); WWindow* win = windows.back().get(); - win->owner = owner; - win->appRequestedWidth = int32_t(width); - win->appRequestedHeight = int32_t(height); + win->owner = owner; win->surface = wl_compositor_create_surface(compositor); wl_surface_add_listener(win->surface, &wlSurfaceListener, win); // also attaches WWindow user data to wl_surface @@ -481,6 +472,18 @@ SystemApi::Window* WaylandApi::Private::createWindow(Tempest::Window* owner, uin } } + if(win->width==0 || win->height==0) { + // The compositor left the size to us: the app's size, physical -> logical + // (the scale is known now, KWin sends preferred_scale before the first configure). + win->width = std::max(1, int32_t(std::lround(float(width) /win->scale()))); + win->height = std::max(1, int32_t(std::lround(float(height)/win->scale()))); + // Also in pending: later configures without a size keep it. + win->pending.width = win->width; + win->pending.height = win->height; + // Before the first buffer (Vulkan's first present), so it applies with that commit. + wp_viewport_set_destination(win->viewport, win->width, win->height); + } + // Window's constructor reads the client rect itself after this returns: no SizeEvent for this size. win->dispatchedWidth = win->physicalWidth(); win->dispatchedHeight = win->physicalHeight(); From ed082b01d751e6ff59abb428e75f5346c40b2b87 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:34:12 +0200 Subject: [PATCH 12/17] remove dipatchedWidth/height in favour of owner->w/h --- Engine/system/api/waylandapi.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index b45a1dbc..1c5c0bca 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -62,10 +62,6 @@ struct WWindow { bool suspended = false; // compositor doesn't repaint the window (e.g. minimized): pause rendering bool configured = false; // first configure acked; no buffer (= Vulkan present) before that - // size the app last saw; updateState() dispatches a resize when it differs. - int32_t dispatchedWidth = 0; // physical - int32_t dispatchedHeight = 0; - bool readyToRender() const { return owner!=nullptr && configured && !suspended; } float scale() const { return float(preferredScale)/120.f; } @@ -484,9 +480,6 @@ SystemApi::Window* WaylandApi::Private::createWindow(Tempest::Window* owner, uin wp_viewport_set_destination(win->viewport, win->width, win->height); } - // Window's constructor reads the client rect itself after this returns: no SizeEvent for this size. - win->dispatchedWidth = win->physicalWidth(); - win->dispatchedHeight = win->physicalHeight(); return reinterpret_cast(win->surface); // wl_surface* native handle } @@ -609,9 +602,8 @@ void WaylandApi::Private::updateState(WWindow& w) { // Update things that should change only once per draw, not on every triggered callback (e.g. swapchain resize) const int32_t pw = w.physicalWidth(); const int32_t ph = w.physicalHeight(); - if(pw!=w.dispatchedWidth || ph!=w.dispatchedHeight) { - w.dispatchedWidth = pw; - w.dispatchedHeight = ph; + // The app's size: set from the client rect by Window's constructor, then by each SizeEvent + if(pw!=w.owner->w() || ph!=w.owner->h()) { SizeEvent e(pw, ph); SystemApi::dispatchResize(*w.owner, e); } From 260c1aae1a92c87953d06a00d86a6c2aeba018a0 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:21:55 +0200 Subject: [PATCH 13/17] restore input --- Engine/system/api/waylandapi.cpp | 732 ++++++++++++++++++++++++++++--- 1 file changed, 671 insertions(+), 61 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 1c5c0bca..9d46b478 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -17,14 +18,21 @@ #include #include +#include #include +#include +#include #include +#include #include "xdg-shell-client-protocol.h" #include "xdg-decoration-unstable-v1-client-protocol.h" #include "viewporter-client-protocol.h" #include "fractional-scale-v1-client-protocol.h" +#include "cursor-shape-v1-client-protocol.h" +#include "pointer-constraints-unstable-v1-client-protocol.h" +#include "relative-pointer-unstable-v1-client-protocol.h" using namespace Tempest; @@ -62,6 +70,15 @@ struct WWindow { bool suspended = false; // compositor doesn't repaint the window (e.g. minimized): pause rendering bool configured = false; // first configure acked; no buffer (= Vulkan present) before that + // Pointer lock for mouse-look, requested while the window's cursor is hidden (see Private::setPointerLock). + zwp_locked_pointer_v1* lockedPointer = nullptr; + bool pointerLocked = false; + + // Cursor position. Button and axis events carry none. While pointer is locked, this is a + // virtual cursor moved by relative motion. + double cursorX = 0; // physical + double cursorY = 0; + bool readyToRender() const { return owner!=nullptr && configured && !suspended; } float scale() const { return float(preferredScale)/120.f; } @@ -98,24 +115,80 @@ struct WaylandApi::Private { // Globals wl_compositor* compositor = nullptr; + wl_seat* seat = nullptr; xdg_wm_base* wmBase = nullptr; zxdg_decoration_manager_v1* decorationManager = nullptr; wp_viewporter* viewporter = nullptr; wp_fractional_scale_manager_v1* fractionalScaleManager = nullptr; + wp_cursor_shape_manager_v1* cursorShapeManager = nullptr; + zwp_pointer_constraints_v1* pointerConstraints = nullptr; + zwp_relative_pointer_manager_v1* relativePointerManager = nullptr; // Exact versions we need to bind successfully: listeners and requests are written for these - static constexpr uint32_t wlCompositorVersion = 1; - static constexpr uint32_t xdgWmBaseVersion = 6; // v6: xdg_toplevel state 'suspended' - static constexpr uint32_t zxdgDecorationManagerV1Version = 1; - static constexpr uint32_t wpViewporterVersion = 1; - static constexpr uint32_t wpFractionalScaleManagerV1Version = 1; + static constexpr uint32_t wlCompositorVersion = 1; + static constexpr uint32_t wlSeatVersion = 8; // v8: wl_pointer.axis_value120 (wheel in 1/120 notches) + static constexpr uint32_t xdgWmBaseVersion = 6; // v6: xdg_toplevel state 'suspended' + static constexpr uint32_t zxdgDecorationManagerV1Version = 1; + static constexpr uint32_t wpViewporterVersion = 1; + static constexpr uint32_t wpFractionalScaleManagerV1Version = 1; + static constexpr uint32_t wpCursorShapeManagerV1Version = 1; + static constexpr uint32_t zwpPointerConstraintsV1Version = 1; + static constexpr uint32_t zwpRelativePointerManagerV1Version = 1; + + // Input devices, non-null while seat has the capability (can change at runtime) + wl_pointer* pointer = nullptr; + wp_cursor_shape_device_v1* cursorShapeDevice = nullptr; // exists together with 'pointer' + zwp_relative_pointer_v1* relativePointer = nullptr; // exists together with 'pointer' + wl_keyboard* keyboard = nullptr; + + // Pointer state between wl_pointer.enter and leave. Position is in WWindow::cursorX/Y + WWindow* pointerFocus = nullptr; // window under pointer + uint32_t pointerEnterSerial = 0; // serial of latest enter event + + // Keymap from wl_keyboard.keymap, state follows wl_keyboard.modifiers; both null until first keymap. + xkb_context* xkbContext = nullptr; + xkb_keymap* xkbKeymap = nullptr; + xkb_state* xkbState = nullptr; + WWindow* keyboardFocus = nullptr; // window with keyboard focus (wl_keyboard.enter/leave) + + // Keys (xkb keycodes) and buttons (evdev codes) whose press reached the app. Only their releases are dispatched, + // and they're released on focus or device loss (see releaseHeldKeys / releaseHeldButtons). + std::vector heldKeys; + std::vector heldButtons; + + // Key repeat: Wayland sends none. Manual implementation from wl_keyboard.repeat_info by sending the held key's KeyDown again, + // which Tempest's dispatcher turns into KeyRepeat (like on Windows). + int32_t repeatRate = 0; // repeats per second; 0 = disabled + int32_t repeatDelay = 0; // ms until the first repeat trigger + uint32_t repeatKeycode = 0; // xkb keycode of the repeating key; 0 = none. + // Non-zero only while keyboardFocus is set and repeatRate>0. + std::chrono::steady_clock::time_point repeatNext; // when the next repeat is due + + // WWindow* if a window gained focus during active readEvents() batch, otherwise nullptr (for focus click filter) + WWindow* focusGainedInBatch = nullptr; std::vector> windows; // owns the windows; each WWindow keeps its address (handle, listener data) static inline std::deque appEvents; // in the order the compositor sent them - // Session listeners. 'data' is Private*. + // Session listeners (globals and seat devices). 'data' is Private*. static void onWlRegistryGlobal(void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version); static void onXdgWmBasePing(void* data, xdg_wm_base* wmBase, uint32_t serial); + static void onWlSeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities); + static void onWlPointerEnter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, wl_fixed_t x, wl_fixed_t y); + static void onWlPointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface); + static void onWlPointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y); + static void onWlPointerButton(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state); + static void onWlPointerAxisValue120(void* data, wl_pointer* pointer, uint32_t axis, int32_t value120); + static void onZwpRelativePointerV1RelativeMotion(void* data, zwp_relative_pointer_v1* relativePointer, uint32_t utimeHi, + uint32_t utimeLo, wl_fixed_t dx, wl_fixed_t dy, + wl_fixed_t dxUnaccel, wl_fixed_t dyUnaccel); + static void onWlKeyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size); + static void onWlKeyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface, wl_array* keys); + static void onWlKeyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface); + static void onWlKeyboardKey(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state); + static void onWlKeyboardModifiers(void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t depressed, + uint32_t latched, uint32_t locked, uint32_t group); + static void onWlKeyboardRepeatInfo(void* data, wl_keyboard* keyboard, int32_t rate, int32_t delay); // Window specific listeners. 'data' is WWindow*. static void onXdgSurfaceConfigure(void* data, xdg_surface* xdgSurface, uint32_t serial); @@ -123,10 +196,16 @@ struct WaylandApi::Private { static void onXdgToplevelClose (void* data, xdg_toplevel* toplevel); static void onZxdgToplevelDecorationV1Configure(void* data, zxdg_toplevel_decoration_v1* decoration, uint32_t mode); static void onWpFractionalScaleV1PreferredScale(void* data, wp_fractional_scale_v1* fractionalScale, uint32_t scale); + static void onZwpLockedPointerV1Locked (void* data, zwp_locked_pointer_v1* lockedPointer); + static void onZwpLockedPointerV1Unlocked(void* data, zwp_locked_pointer_v1* lockedPointer); // Listener tables (session) static const wl_registry_listener wlRegistryListener; static const xdg_wm_base_listener xdgWmBaseListener; + static const wl_seat_listener wlSeatListener; + static const wl_pointer_listener wlPointerListener; + static const zwp_relative_pointer_v1_listener zwpRelativePointerV1Listener; + static const wl_keyboard_listener wlKeyboardListener; // Listener tables (window specific) static const wl_surface_listener wlSurfaceListener; @@ -134,13 +213,26 @@ struct WaylandApi::Private { static const xdg_toplevel_listener xdgToplevelListener; static const zxdg_toplevel_decoration_v1_listener zxdgToplevelDecorationV1Listener; static const wp_fractional_scale_v1_listener wpFractionalScaleV1Listener; + static const zwp_locked_pointer_v1_listener zwpLockedPointerV1Listener; // Helpers - static constexpr auto ioIgnore = [](auto...) {}; // useful to silence Wayland listener stubs - template static void ioDestroy(T*& obj, void (*destroyFn)(T*)); - void logDisplayError() const; - static void dispatchCloseEvent(WWindow& w); + static constexpr auto ioIgnore = [](auto...) {}; // useful to silence Wayland listener stubs + template static void ioDestroy(T*& obj, void (*destroyFn)(T*)); + void logDisplayError() const; + static Event::MouseButton toButton(uint32_t button); + static wp_cursor_shape_device_v1_shape toCursorShape(CursorShape shape); + static MouseEvent pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type); + KeyEvent keyEvent(uint32_t keycode, Event::Type type) const; + bool isWindowFocused(const WWindow* w) const; + void setPointerLock(WWindow& w, bool lock); + void releaseHeldKeys(WWindow& w); + void releaseHeldButtons(WWindow& w); + void releasePointer(); + void releaseKeyboard(); + void startKeyRepeat(uint32_t keycode); + void processKeyRepeat(); + static void dispatchCloseEvent(WWindow& w); // Connection and windows bool hasRequiredGlobals() const; @@ -185,13 +277,291 @@ void WaylandApi::Private::onWlRegistryGlobal(void* data, wl_registry* registry, self->fractionalScaleManager = static_cast( wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, wpFractionalScaleManagerV1Version)); } - // [input] + if(std::strcmp(interface, wp_cursor_shape_manager_v1_interface.name)==0 && version>=wpCursorShapeManagerV1Version) { + self->cursorShapeManager = static_cast( + wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version)); + } + if(std::strcmp(interface, zwp_pointer_constraints_v1_interface.name)==0 && version>=zwpPointerConstraintsV1Version) { + self->pointerConstraints = static_cast( + wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version)); + } + if(std::strcmp(interface, zwp_relative_pointer_manager_v1_interface.name)==0 && version>=zwpRelativePointerManagerV1Version) { + self->relativePointerManager = static_cast( + wl_registry_bind(registry, name, &zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version)); + } + if(std::strcmp(interface, wl_seat_interface.name)==0 && version>=wlSeatVersion && self->seat==nullptr) { + // Only the first seat: multi-seat setups are rare, and Tempest has a single pointer/keyboard. + static_assert(wlSeatVersionseat = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, wlSeatVersion)); + wl_seat_add_listener(self->seat, &wlSeatListener, self); + } } void WaylandApi::Private::onXdgWmBasePing([[maybe_unused]] void* data, xdg_wm_base* wmBase, uint32_t serial) { xdg_wm_base_pong(wmBase, serial); } +void WaylandApi::Private::onWlSeatCapabilities(void* data, wl_seat* seat, uint32_t capabilities) { + // Sent after binding and whenever devices come or go: get a device when its capability appears, release it when it goes. + WaylandApi::Private* self = static_cast(data); + + // The managers are bound: capabilities arrive after connect()'s roundtrip. Pointer locks follow on the next cursor + // change (always on enter). + const bool hasPointer = (capabilities & WL_SEAT_CAPABILITY_POINTER)!=0; + if(hasPointer && self->pointer==nullptr) { + self->pointer = wl_seat_get_pointer(seat); + wl_pointer_add_listener(self->pointer, &wlPointerListener, self); + self->cursorShapeDevice = wp_cursor_shape_manager_v1_get_pointer(self->cursorShapeManager, self->pointer); + self->relativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer(self->relativePointerManager, self->pointer); + zwp_relative_pointer_v1_add_listener(self->relativePointer, &zwpRelativePointerV1Listener, self); + } + else if(!hasPointer && self->pointer!=nullptr) { + self->releasePointer(); + } + + const bool hasKeyboard = (capabilities & WL_SEAT_CAPABILITY_KEYBOARD)!=0; + if(hasKeyboard && self->keyboard==nullptr) { + self->keyboard = wl_seat_get_keyboard(seat); + wl_keyboard_add_listener(self->keyboard, &wlKeyboardListener, self); + } + else if(!hasKeyboard && self->keyboard!=nullptr) { + self->releaseKeyboard(); + } + } + +void WaylandApi::Private::onWlPointerEnter(void* data, [[maybe_unused]] wl_pointer* pointer, uint32_t serial, + wl_surface* surface, wl_fixed_t x, wl_fixed_t y) { + // Like X11's EnterNotify: a move to the entry position, then re-apply the window's cursor + auto self = static_cast(data); + if(surface==nullptr) + return; // surface was destroyed after compositor sent the event + auto w = static_cast(wl_surface_get_user_data(surface)); // listener data of our wl_surface + self->pointerFocus = w; + self->pointerEnterSerial = serial; + // logical -> physical + w->cursorX = wl_fixed_to_double(x)*w->scale(); + w->cursorY = wl_fixed_to_double(y)*w->scale(); + if(w->owner==nullptr) + return; + if(self->isWindowFocused(w)) { + MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, e); + } + // Note: Wayland always requires cursor shape on enter (see implShowCursor) + SystemApi::showCursor(reinterpret_cast(w->surface), SystemApi::cursorShape(*w->owner)); + } + +void WaylandApi::Private::onWlPointerLeave(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] wl_surface* surface) { + // 'surface' may be null if it was destroyed meanwhile. + static_cast(data)->pointerFocus = nullptr; + } + +void WaylandApi::Private::onWlPointerMotion(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t time, + wl_fixed_t x, wl_fixed_t y) { + // While a button is held, coordinates can be outside the window (implicit grab), like on X11. Not sent while locked. + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr) + return; + w->cursorX = wl_fixed_to_double(x)*w->scale(); + w->cursorY = wl_fixed_to_double(y)*w->scale(); + if(w->owner==nullptr || !self->isWindowFocused(w)) + return; + MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, e); + } + +void WaylandApi::Private::onWlPointerButton(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] uint32_t time, uint32_t button, uint32_t state) { + // Unknown buttons become ButtonNone, like X11. Releases only for presses the app got (see heldButtons). + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr || w->owner==nullptr) + return; + + if(state==WL_POINTER_BUTTON_STATE_PRESSED) { + if(!self->isWindowFocused(w)) + return; + // Focus click filter: the click that only focuses the window must not reach the app. KWin sends + // wl_keyboard.enter and that press together, so a press in the same readEvents() batch as the enter is that click. + if(self->focusGainedInBatch==w) + return; + if(std::find(self->heldButtons.begin(), self->heldButtons.end(), button)==self->heldButtons.end()) + self->heldButtons.push_back(button); + MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseDown); + SystemApi::dispatchMouseDown(*w->owner, e); + } else { + if(std::erase(self->heldButtons, button)==0) + return; + MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseUp); + SystemApi::dispatchMouseUp(*w->owner, e); + } + } + +void WaylandApi::Private::onWlPointerAxisValue120(void* data, [[maybe_unused]] wl_pointer* pointer, uint32_t axis, int32_t value120) { + // 120 per notch, like Tempest's delta on Windows; Wayland's sign is inverted (positive = down). + // Horizontal scrolling is ignored, like on X11. + if(axis!=WL_POINTER_AXIS_VERTICAL_SCROLL) + return; + + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr || w->owner==nullptr || !self->isWindowFocused(w)) + return; + MouseEvent e = pointerEvent(*w, Event::ButtonNone, -value120, Event::MouseWheel); + SystemApi::dispatchMouseWheel(*w->owner, e); + } + +void WaylandApi::Private::onZwpRelativePointerV1RelativeMotion(void* data, [[maybe_unused]] zwp_relative_pointer_v1* relativePointer, + [[maybe_unused]] uint32_t utimeHi, [[maybe_unused]] uint32_t utimeLo, + [[maybe_unused]] wl_fixed_t dx, [[maybe_unused]] wl_fixed_t dy, + wl_fixed_t dxUnaccel, wl_fixed_t dyUnaccel) { + // Moves virtual cursor while pointer is locked (otherwise wl_pointer.motion has the position). Raw input deltas + // clamped to the window like a real cursor. + // Note: compositor only activates the lock while focused + auto self = static_cast(data); + WWindow* w = self->pointerFocus; + if(w==nullptr || !w->pointerLocked || w->owner==nullptr) + return; + const double s = w->scale(); + w->cursorX = std::clamp(w->cursorX + wl_fixed_to_double(dxUnaccel)*s, 0.0, double(std::max(w->physicalWidth() -1, 0))); + w->cursorY = std::clamp(w->cursorY + wl_fixed_to_double(dyUnaccel)*s, 0.0, double(std::max(w->physicalHeight()-1, 0))); + MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, e); + } + +void WaylandApi::Private::onWlKeyboardKeymap(void* data, [[maybe_unused]] wl_keyboard* keyboard, + uint32_t format, int32_t fd, uint32_t size) { + // Sent on creation and when the keymap changes. On failure the previous keymap stays. + // Note: need to close fd manually inside function! + auto self = static_cast(data); + if(format!=WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + Log::e("WaylandApi: unsupported keymap format ", format); + close(fd); + return; + } + + // MAP_PRIVATE: required since wl_keyboard v7 + void* map = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if(map==MAP_FAILED) { + Log::e("WaylandApi: can't map the keymap: ", std::strerror(errno)); + return; + } + // The text is null-terminated within 'size'; strnlen makes sure we never read beyond the mapping. + const char* text = static_cast(map); + xkb_keymap* keymap = xkb_keymap_new_from_buffer(self->xkbContext, text, strnlen(text, size), + XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(map, size); + if(keymap==nullptr) { + Log::e("WaylandApi: can't compile the keymap"); + return; + } + xkb_state* state = xkb_state_new(keymap); + if(state==nullptr) { + Log::e("WaylandApi: can't create the keyboard state"); + xkb_keymap_unref(keymap); + return; + } + + ioDestroy(self->xkbState, xkb_state_unref); + ioDestroy(self->xkbKeymap, xkb_keymap_unref); + self->xkbKeymap = keymap; + self->xkbState = state; + self->repeatKeycode = 0; // keycodes may mean something else in the new keymap + + const char* layout = xkb_keymap_layout_get_name(keymap, 0); + Log::i("WaylandApi: keymap loaded, ", xkb_keymap_num_layouts(keymap), " layout(s), first: ", (layout!=nullptr ? layout : "unnamed")); + } + +void WaylandApi::Private::onWlKeyboardEnter(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + wl_surface* surface, [[maybe_unused]] wl_array* keys) { + // Like X11's FocusIn. Keys already held ('keys') are ignored: they aren't in heldKeys, so their releases are dropped. + auto self = static_cast(data); + if(surface==nullptr) + return; // the surface was destroyed after the compositor sent the event + auto w = static_cast(wl_surface_get_user_data(surface)); + self->keyboardFocus = w; + self->focusGainedInBatch = w; + if(w->owner==nullptr) + return; + FocusEvent e(true, Event::UnknownReason); + SystemApi::dispatchFocus(*w->owner, e); + // Pointer already over the window: like an enter, a move to its position and the app's cursor. + if(self->pointerFocus==w) { + MouseEvent m = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); + SystemApi::dispatchMouseMove(*w->owner, m); + SystemApi::showCursor(reinterpret_cast(w->surface), SystemApi::cursorShape(*w->owner)); + } + } + +void WaylandApi::Private::onWlKeyboardLeave(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] wl_surface* surface) { + // Like X11's FocusOut, but held keys and buttons are released first. 'surface' may be null, so the window comes from + // keyboardFocus. + auto self = static_cast(data); + WWindow* w = self->keyboardFocus; + if(w==nullptr) + return; + // On focus loss the real releases never reach the app, so keys and buttons would stay held (e.g. Alt after Alt+Tab). + self->releaseHeldKeys(*w); + self->releaseHeldButtons(*w); + self->keyboardFocus = nullptr; + if(w->owner==nullptr) + return; + FocusEvent e(false, Event::UnknownReason); + SystemApi::dispatchFocus(*w->owner, e); + if(self->pointerFocus==w) + SystemApi::showCursor(reinterpret_cast(w->surface), SystemApi::cursorShape(*w->owner)); + } + +void WaylandApi::Private::onWlKeyboardKey(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + [[maybe_unused]] uint32_t time, uint32_t key, uint32_t state) { + // xkb keycodes are evdev + 8 (X11's keycodes); also the scancode for the dispatcher. + auto self = static_cast(data); + if(self->keyboardFocus==nullptr || self->keyboardFocus->owner==nullptr) + return; + if(self->xkbState==nullptr) + return; // no keymap compiled + // Only releases of keys the app got a press for are dispatched (see heldKeys). + const uint32_t keycode = key + 8; + if(state==WL_KEYBOARD_KEY_STATE_PRESSED) { + if(std::find(self->heldKeys.begin(), self->heldKeys.end(), keycode)==self->heldKeys.end()) + self->heldKeys.push_back(keycode); + KeyEvent e = self->keyEvent(keycode, Event::KeyDown); + SystemApi::dispatchKeyDown(*self->keyboardFocus->owner, e, keycode); + self->startKeyRepeat(keycode); + } else { + if(std::erase(self->heldKeys, keycode)==0) + return; + if(keycode==self->repeatKeycode) + self->repeatKeycode = 0; + KeyEvent e = self->keyEvent(keycode, Event::KeyUp); + SystemApi::dispatchKeyUp(*self->keyboardFocus->owner, e, keycode); + } + } + +void WaylandApi::Private::onWlKeyboardModifiers(void* data, [[maybe_unused]] wl_keyboard* keyboard, [[maybe_unused]] uint32_t serial, + uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group) { + // Not dispatched: Tempest tracks Shift/Ctrl/Alt from the key events itself. xkb needs them for the text of a key + // (Shift, CapsLock) and for the active layout ('group'). + auto self = static_cast(data); + if(self->xkbState==nullptr) + return; // no keymap compiled + xkb_state_update_mask(self->xkbState, depressed, latched, locked, 0, 0, group); + } + +void WaylandApi::Private::onWlKeyboardRepeatInfo(void* data, [[maybe_unused]] wl_keyboard* keyboard, int32_t rate, int32_t delay) { + // The desktop setting; rate 0 = repeat disabled. + auto self = static_cast(data); + self->repeatRate = rate; + self->repeatDelay = delay; + if(rate<=0) + self->repeatKeycode = 0; + } + /// Window specific listeners // void WaylandApi::Private::onXdgSurfaceConfigure(void* data, [[maybe_unused]] xdg_surface* xdgSurface, uint32_t serial) { @@ -248,6 +618,15 @@ void WaylandApi::Private::onWpFractionalScaleV1PreferredScale(void* data, [[mayb w->preferredScale = scale; } +void WaylandApi::Private::onZwpLockedPointerV1Locked(void* data, [[maybe_unused]] zwp_locked_pointer_v1* lockedPointer) { + static_cast(data)->pointerLocked = true; + } + +void WaylandApi::Private::onZwpLockedPointerV1Unlocked(void* data, [[maybe_unused]] zwp_locked_pointer_v1* lockedPointer) { + // E.g. focus lost; the persistent lock may become active again later. + static_cast(data)->pointerLocked = false; + } + //// /// LISTENER TABLES // >> Note: Every event of the bound interface version needs a handler: libwayland aborts on null entry. @@ -261,6 +640,35 @@ const wl_registry_listener WaylandApi::Private::wlRegistryListener = { const xdg_wm_base_listener WaylandApi::Private::xdgWmBaseListener = { .ping = onXdgWmBasePing, }; +const wl_seat_listener WaylandApi::Private::wlSeatListener = { + .capabilities = onWlSeatCapabilities, + .name = ioIgnore, + }; +const wl_pointer_listener WaylandApi::Private::wlPointerListener = { + .enter = onWlPointerEnter, + .leave = onWlPointerLeave, + .motion = onWlPointerMotion, + .button = onWlPointerButton, + // TODO(wayland): touchpad scrolling: continuous axis events (axis_source finger) come without axis_value120 + .axis = ioIgnore, + .frame = ioIgnore, + .axis_source = ioIgnore, + .axis_stop = ioIgnore, + .axis_discrete = ioIgnore, // not sent since v8 (replaced by axis_value120) + .axis_value120 = onWlPointerAxisValue120, + // axis_relative_direction (only v9+) + }; +const zwp_relative_pointer_v1_listener WaylandApi::Private::zwpRelativePointerV1Listener = { + .relative_motion = onZwpRelativePointerV1RelativeMotion, + }; +const wl_keyboard_listener WaylandApi::Private::wlKeyboardListener = { + .keymap = onWlKeyboardKeymap, + .enter = onWlKeyboardEnter, + .leave = onWlKeyboardLeave, + .key = onWlKeyboardKey, + .modifiers = onWlKeyboardModifiers, + .repeat_info = onWlKeyboardRepeatInfo, + }; /// Window specific listener tables // @@ -286,6 +694,10 @@ const zxdg_toplevel_decoration_v1_listener WaylandApi::Private::zxdgToplevelDeco const wp_fractional_scale_v1_listener WaylandApi::Private::wpFractionalScaleV1Listener = { .preferred_scale = onWpFractionalScaleV1PreferredScale, }; +const zwp_locked_pointer_v1_listener WaylandApi::Private::zwpLockedPointerV1Listener = { + .locked = onZwpLockedPointerV1Locked, + .unlocked = onZwpLockedPointerV1Unlocked, + }; //// /// HELPERS @@ -312,6 +724,148 @@ void WaylandApi::Private::logDisplayError() const { } } +Event::MouseButton WaylandApi::Private::toButton(uint32_t button) { + // wl_pointer.button carries evdev codes. + switch(button) { + case BTN_LEFT: return Event::ButtonLeft; + case BTN_RIGHT: return Event::ButtonRight; + case BTN_MIDDLE: return Event::ButtonMid; + case BTN_SIDE: return Event::ButtonBack; + case BTN_EXTRA: return Event::ButtonForward; + } + return Event::ButtonNone; + } + +wp_cursor_shape_device_v1_shape WaylandApi::Private::toCursorShape(CursorShape shape) { + // Hidden is handled by the caller. + switch(shape) { + case CursorShape::Arrow: + case CursorShape::Hidden: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT; + case CursorShape::IBeam: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT; + case CursorShape::SizeVer: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE; + case CursorShape::SizeHor: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE; + case CursorShape::SizeBDiag: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE; // '/' + case CursorShape::SizeFDiag: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE; // '\' + case CursorShape::SizeAll: return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL; // four arrows; all_resize needs v2 + } + return WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT; + } + +MouseEvent WaylandApi::Private::pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type) { + // floor: the physical pixel the cursor is in. + const int x = int(std::floor(w.cursorX)); + const int y = int(std::floor(w.cursorY)); + return MouseEvent(x, y, button, Event::M_NoModifier, delta, 0, type); + } + +KeyEvent WaylandApi::Private::keyEvent(uint32_t keycode, Event::Type type) const { + // keycode: xkb keycode (evdev + 8). key: level-0 keysym of the active layout, so Shift+W stays K_W (like Windows' + // virtual keys). code: the typed character, 0 if none (e.g. arrows, dead keys). + const xkb_layout_index_t layout = xkb_state_key_get_layout(xkbState, keycode); + const xkb_keysym_t* syms = nullptr; + const int count = xkb_keymap_key_get_syms_by_level(xkbKeymap, keycode, layout, 0, &syms); + const xkb_keysym_t sym = count>0 ? syms[0] : XKB_KEY_NoSymbol; // choose first available + const auto key = Event::KeyType(SystemApi::translateKey(sym)); + + // Only characters up to U+FFFF (Basic Multilingual Plane): Tempest's text input can't hold more. + // Beyond it, nothing is typed (X11 and Windows deliver half a UTF-16 surrogate pair there). + const uint32_t utf32 = xkb_state_key_get_utf32(xkbState, keycode); + const uint32_t code = utf32<=0xFFFF ? utf32 : 0; + return KeyEvent(key, code, Event::M_NoModifier, type); + } + +bool WaylandApi::Private::isWindowFocused(const WWindow* w) const { + // Window focus = keyboard focus: mouse moves, wheel and presses only reach the focused window. + // Without a keyboard every window counts as focused (pointer events only come for the one under the pointer). + return keyboard==nullptr || keyboardFocus==w; + } + +void WaylandApi::Private::setPointerLock(WWindow& w, bool lock) { + // Mouse-look: Wayland apps can't move the pointer, so while the cursor is hidden it's locked in place and the app + // gets relative motion instead (Tempest has no relative-mouse API). Called on every cursor change. + // The lock stays requested: the compositor pauses it while the window is unfocused and resumes it by itself. + if(lock && w.lockedPointer==nullptr && pointer!=nullptr) { + w.lockedPointer = zwp_pointer_constraints_v1_lock_pointer(pointerConstraints, w.surface, pointer, nullptr, + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + zwp_locked_pointer_v1_add_listener(w.lockedPointer, &zwpLockedPointerV1Listener, &w); + } + else if(!lock && w.lockedPointer!=nullptr) { + ioDestroy(w.lockedPointer, zwp_locked_pointer_v1_destroy); + w.pointerLocked = false; + } + } + +void WaylandApi::Private::releaseHeldKeys(WWindow& w) { + repeatKeycode = 0; + auto keys = std::move(heldKeys); // save from manipulation + heldKeys.clear(); + if(w.owner==nullptr) + return; + for(uint32_t keycode:keys) { + KeyEvent e = keyEvent(keycode, Event::KeyUp); + SystemApi::dispatchKeyUp(*w.owner, e, keycode); + } + } + +void WaylandApi::Private::releaseHeldButtons(WWindow& w) { + auto buttons = std::move(heldButtons); // save from manipulation + heldButtons.clear(); + if(w.owner==nullptr) + return; + for(uint32_t button:buttons) { + MouseEvent e = pointerEvent(w, toButton(button), 0, Event::MouseUp); + SystemApi::dispatchMouseUp(*w.owner, e); + } + } + +void WaylandApi::Private::releasePointer() { + // release what the app saw pressed while pointer still exists + if(pointerFocus!=nullptr) + releaseHeldButtons(*pointerFocus); + // Objects created from the pointer first. + for(auto& w:windows) + setPointerLock(*w, false); + ioDestroy(relativePointer, zwp_relative_pointer_v1_destroy); + ioDestroy(cursorShapeDevice, wp_cursor_shape_device_v1_destroy); + static_assert(wlSeatVersion>=WL_POINTER_RELEASE_SINCE_VERSION); + ioDestroy(pointer, wl_pointer_release); + pointerFocus = nullptr; + heldButtons.clear(); + } + +void WaylandApi::Private::releaseKeyboard() { + // release what the app saw pressed while keyboard still exists + if(keyboardFocus!=nullptr) + releaseHeldKeys(*keyboardFocus); + static_assert(wlSeatVersion>=WL_KEYBOARD_RELEASE_SINCE_VERSION); + ioDestroy(keyboard, wl_keyboard_release); + keyboardFocus = nullptr; + repeatKeycode = 0; + heldKeys.clear(); + } + +void WaylandApi::Private::startKeyRepeat(uint32_t keycode) { + // Non-repeating keys (modifiers) leave a running repeat alone; a repeating key takes over. + if(repeatRate<=0 || !xkb_keymap_key_repeats(xkbKeymap, keycode)) + return; + repeatKeycode = keycode; + repeatNext = std::chrono::steady_clock::now() + std::chrono::milliseconds(repeatDelay); + } + +void WaylandApi::Private::processKeyRepeat() { + if(repeatKeycode==0) + return; + const auto now = std::chrono::steady_clock::now(); + const auto interval = std::chrono::microseconds(std::chrono::seconds(1))/repeatRate; + if(nowowner, e, repeatKeycode); + } + void WaylandApi::Private::dispatchCloseEvent(WWindow& w) { // Only a request. Like the Windows backend: the app or an overlay may accept the CloseEvent; otherwise exit. // w isn't used after the dispatch: the app may destroy the window while handling it. @@ -328,7 +882,8 @@ void WaylandApi::Private::dispatchCloseEvent(WWindow& w) { // bool WaylandApi::Private::hasRequiredGlobals() const { - // No fallbacks yet without fractional-scale or server-side decorations (e.g. GNOME doesn't work). + // No fallbacks yet without fractional-scale, cursor-shape or server-side decorations (e.g. GNOME doesn't work). + // Pointer constraints + relative pointer: mouse-look, since Wayland clients can't warp the pointer. struct Required { const void* object; const wl_interface& interface; @@ -340,10 +895,10 @@ bool WaylandApi::Private::hasRequiredGlobals() const { {decorationManager, zxdg_decoration_manager_v1_interface, zxdgDecorationManagerV1Version }, {viewporter, wp_viewporter_interface, wpViewporterVersion }, {fractionalScaleManager, wp_fractional_scale_manager_v1_interface, wpFractionalScaleManagerV1Version }, - // [input] {seat, wl_seat_interface, wlSeatVersion }, - // [input] {cursorShapeManager, wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version }, - // [input] {pointerConstraints, zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version }, - // [input] {relativePointerManager, zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version}, + {seat, wl_seat_interface, wlSeatVersion }, + {cursorShapeManager, wp_cursor_shape_manager_v1_interface, wpCursorShapeManagerV1Version }, + {pointerConstraints, zwp_pointer_constraints_v1_interface, zwpPointerConstraintsV1Version }, + {relativePointerManager, zwp_relative_pointer_manager_v1_interface, zwpRelativePointerManagerV1Version}, }; bool hasAll = true; for(auto& r:required) { @@ -372,12 +927,12 @@ bool WaylandApi::Private::connect() { return false; } - // [input] // Needed to compile keymap: without it there's no keyboard input - // [input] xkbContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - // [input] if(!xkbContext) { - // [input] Log::e("WaylandApi: xkb_context_new failed"); - // [input] return false; - // [input] } + // Needed to compile keymap: without it there's no keyboard input + xkbContext = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if(!xkbContext) { + Log::e("WaylandApi: xkb_context_new failed"); + return false; + } registry = wl_display_get_registry(display); wl_registry_add_listener(registry, &wlRegistryListener, this); @@ -399,24 +954,24 @@ WaylandApi::Private::~Private() { while(!windows.empty()) destroyWindow(windows.back().get()); - // [input] ioDestroy(relativePointerManager, zwp_relative_pointer_manager_v1_destroy); - // [input] ioDestroy(pointerConstraints, zwp_pointer_constraints_v1_destroy); - // [input] ioDestroy(cursorShapeManager, wp_cursor_shape_manager_v1_destroy); + ioDestroy(relativePointerManager, zwp_relative_pointer_manager_v1_destroy); + ioDestroy(pointerConstraints, zwp_pointer_constraints_v1_destroy); + ioDestroy(cursorShapeManager, wp_cursor_shape_manager_v1_destroy); ioDestroy(fractionalScaleManager, wp_fractional_scale_manager_v1_destroy); ioDestroy(viewporter, wp_viewporter_destroy); ioDestroy(decorationManager, zxdg_decoration_manager_v1_destroy); ioDestroy(wmBase, xdg_wm_base_destroy); - // [input] // Seat devices before the seat - // [input] releaseKeyboard(); - // [input] releasePointer(); + // Seat devices before the seat + releaseKeyboard(); + releasePointer(); - // [input] ioDestroy(xkbState, xkb_state_unref); - // [input] ioDestroy(xkbKeymap, xkb_keymap_unref); - // [input] ioDestroy(xkbContext, xkb_context_unref); + ioDestroy(xkbState, xkb_state_unref); + ioDestroy(xkbKeymap, xkb_keymap_unref); + ioDestroy(xkbContext, xkb_context_unref); - // [input] static_assert(wlSeatVersion>=WL_SEAT_RELEASE_SINCE_VERSION); - // [input] ioDestroy(seat, wl_seat_release); + static_assert(wlSeatVersion>=WL_SEAT_RELEASE_SINCE_VERSION); + ioDestroy(seat, wl_seat_release); ioDestroy(compositor, wl_compositor_destroy); ioDestroy(registry, wl_registry_destroy); @@ -489,21 +1044,21 @@ void WaylandApi::Private::destroyWindow(WWindow* w) { if(w==nullptr) return; - // [input] // The compositor sends wl_pointer/wl_keyboard.leave for a destroyed surface, but only after we've deleted w. - // [input] if(pointerFocus==w) { - // [input] pointerFocus = nullptr; - // [input] heldButtons.clear(); - // [input] } - // [input] if(keyboardFocus==w) { - // [input] keyboardFocus = nullptr; - // [input] repeatKeycode = 0; - // [input] heldKeys.clear(); - // [input] heldButtons.clear(); - // [input] } - // [input] if(focusGainedInBatch==w) - // [input] focusGainedInBatch = nullptr; - - // [input] setPointerLock(*w, false); // created for the wl_surface + // The compositor sends wl_pointer/wl_keyboard.leave for a destroyed surface, but only after we've deleted w. + if(pointerFocus==w) { + pointerFocus = nullptr; + heldButtons.clear(); + } + if(keyboardFocus==w) { + keyboardFocus = nullptr; + repeatKeycode = 0; + heldKeys.clear(); + heldButtons.clear(); + } + if(focusGainedInBatch==w) + focusGainedInBatch = nullptr; + + setPointerLock(*w, false); // created for the wl_surface ioDestroy(w->fractionalScale, wp_fractional_scale_v1_destroy); ioDestroy(w->viewport, wp_viewport_destroy); ioDestroy(w->decoration, zxdg_toplevel_decoration_v1_destroy); // before toplevel @@ -528,8 +1083,7 @@ void WaylandApi::Private::handleConnectionError() { int WaylandApi::Private::pollTimeout(const SystemApi::AppCallBack& cb) const { // 0 if a window may render; otherwise wait for the compositor (e.g. a configure that ends 'suspended'), - // but not beyond the next timer and at most idlePollTimeoutMs - // [input] (and not beyond the next scheduled key repeat) + // but not beyond the next timer or key repeat, and at most idlePollTimeoutMs // Longest sleep in poll() while no window may render constexpr int idlePollTimeoutMs = 5; @@ -538,10 +1092,10 @@ int WaylandApi::Private::pollTimeout(const SystemApi::AppCallBack& cb) const { if(canRender) return 0; int timeout = int(std::min(cb.timeToTimer(), idlePollTimeoutMs)); - // [input] if(repeatKeycode!=0) { - // [input] const auto untilRepeat = std::chrono::ceil(repeatNext - std::chrono::steady_clock::now()); - // [input] timeout = std::clamp(int(untilRepeat.count()), 0, timeout); - // [input] } + if(repeatKeycode!=0) { + const auto untilRepeat = std::chrono::ceil(repeatNext - std::chrono::steady_clock::now()); + timeout = std::clamp(int(untilRepeat.count()), 0, timeout); + } return timeout; } @@ -549,7 +1103,7 @@ void WaylandApi::Private::readEvents(const SystemApi::AppCallBack& cb) { // Doesn't block beyond pollTimeout(): Tempest's loop must keep running (timers, rendering). // The Vulkan driver reads the same connection on its own queue; prepare_read -> read_events/cancel_read // is how several readers share the socket without stealing or blocking each other. - // [input] focusGainedInBatch = nullptr; // a new batch (see onWlPointerButton) + focusGainedInBatch = nullptr; // a new batch (see onWlPointerButton) while(wl_display_prepare_read(display)!=0) { // Our queue already holds events (e.g. read by the driver): dispatch them before reading more. if(wl_display_dispatch_pending(display)<0) { @@ -579,7 +1133,7 @@ void WaylandApi::Private::readEvents(const SystemApi::AppCallBack& cb) { return; } - // [input] processKeyRepeat(); // synthetic key events + processKeyRepeat(); // synthetic key events } bool WaylandApi::Private::dispatchAppEvent() { @@ -624,7 +1178,45 @@ void WaylandApi::Private::renderWindows() { // WaylandApi::WaylandApi() { - // [input] key table + setupKeyTranslate(k,24) + // Same table as X11: XKB keysyms have the values of X11's keysyms. Letters, digits and F-keys are ranges + // (see SystemApi::translateKey). + static const TranslateKeyPair k[] = { + { XKB_KEY_Control_L, Event::K_LControl }, + { XKB_KEY_Control_R, Event::K_RControl }, + + { XKB_KEY_Shift_L, Event::K_LShift }, + { XKB_KEY_Shift_R, Event::K_RShift }, + + { XKB_KEY_Alt_L, Event::K_LAlt }, + { XKB_KEY_Alt_R, Event::K_RAlt }, + + { XKB_KEY_Insert, Event::K_Insert }, + { XKB_KEY_Delete, Event::K_Delete }, + { XKB_KEY_Home, Event::K_Home }, + { XKB_KEY_End, Event::K_End }, + { XKB_KEY_Page_Up, Event::K_PageUp }, + { XKB_KEY_Page_Down, Event::K_PageDown }, + + { XKB_KEY_Left, Event::K_Left }, + { XKB_KEY_Right, Event::K_Right }, + { XKB_KEY_Up, Event::K_Up }, + { XKB_KEY_Down, Event::K_Down }, + + { XKB_KEY_Escape, Event::K_ESCAPE }, + { XKB_KEY_Tab, Event::K_Tab }, + { XKB_KEY_BackSpace, Event::K_Back }, + { XKB_KEY_Pause, Event::K_Pause }, + { XKB_KEY_Return, Event::K_Return }, + { XKB_KEY_space, Event::K_Space }, + { XKB_KEY_Caps_Lock, Event::K_CapsLock }, + + { XKB_KEY_F1, Event::K_F1 }, + { XKB_KEY_0, Event::K_0 }, + { XKB_KEY_a, Event::K_A }, + + { 0, Event::K_NoKey } + }; + setupKeyTranslate(k,24); impl.reset(new Private()); if(!impl->connect()) { @@ -697,12 +1289,30 @@ void WaylandApi::implSetWindowTitle(SystemApi::Window* w, const char* utf8) { xdg_toplevel_set_title(toWWindow(w)->toplevel, utf8); } -void WaylandApi::implSetCursorPosition([[maybe_unused]] SystemApi::Window* w, [[maybe_unused]] int x, [[maybe_unused]] int y) { - // [input] +void WaylandApi::implSetCursorPosition(SystemApi::Window* w, int x, int y) { + // Wayland clients can't move the pointer. While it's locked, the virtual cursor is all the app sees, so setting it is + // enough (no MouseMove for the jump). + auto ww = toWWindow(w); + if(!ww->pointerLocked) + return; + ww->cursorX = x; + ww->cursorY = y; } -void WaylandApi::implShowCursor([[maybe_unused]] SystemApi::Window* w, [[maybe_unused]] CursorShape show) { - // [input] +void WaylandApi::implShowCursor(SystemApi::Window* w, CursorShape show) { + // The pointer lock is requested whenever the cursor is hidden, even with the pointer elsewhere: the compositor + // activates it once the pointer is over the focused window (see Private::setPointerLock). + // The cursor image needs the enter serial, so it's only set while the pointer is over w; enter and focus changes + // re-apply it. An unfocused window shows the desktop cursor. + auto ww = toWWindow(w); + impl->setPointerLock(*ww, show==CursorShape::Hidden); + if(impl->pointerFocus!=ww) + return; + const CursorShape shape = impl->isWindowFocused(ww) ? show : CursorShape::Arrow; + if(shape==CursorShape::Hidden) + wl_pointer_set_cursor(impl->pointer, impl->pointerEnterSerial, nullptr, 0, 0); + else + wp_cursor_shape_device_v1_set_shape(impl->cursorShapeDevice, impl->pointerEnterSerial, Private::toCursorShape(shape)); } bool WaylandApi::implIsRunning() { From 2864254c85fc39070255146bbbe2c94ab4c69552 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:40:04 +0200 Subject: [PATCH 14/17] convert input events to appEvents queue --- Engine/system/api/waylandapi.cpp | 108 +++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 33 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 9d46b478..a6bbb914 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -101,8 +101,14 @@ static WWindow* toWWindow(SystemApi::Window* w) { // Events for the app/systemAPI. Listeners only record them. implProcessEvents() dispatches one per call struct AppEvent { - Tempest::Event::Type type; - WWindow* w; + Tempest::Event::Type type; + WWindow* w; + // Data per type, recorded by the listener + Tempest::Event::KeyType key = Tempest::Event::K_NoKey; // KeyDown/KeyUp + uint32_t code = 0; // KeyDown/KeyUp: typed character + uint32_t keycode = 0; // KeyDown/KeyUp: scancode for the dispatcher + Tempest::Event::MouseButton button = Tempest::Event::ButtonNone; // MouseUp + bool focusIn = false; // Focus }; //// @@ -223,7 +229,7 @@ struct WaylandApi::Private { static Event::MouseButton toButton(uint32_t button); static wp_cursor_shape_device_v1_shape toCursorShape(CursorShape shape); static MouseEvent pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type); - KeyEvent keyEvent(uint32_t keycode, Event::Type type) const; + AppEvent keyEvent(WWindow& w, uint32_t keycode, Event::Type type) const; bool isWindowFocused(const WWindow* w) const; void setPointerLock(WWindow& w, bool lock); void releaseHeldKeys(WWindow& w); @@ -233,6 +239,9 @@ struct WaylandApi::Private { void startKeyRepeat(uint32_t keycode); void processKeyRepeat(); static void dispatchCloseEvent(WWindow& w); + static void dispatchKeyEvent(const AppEvent& e); + static void dispatchFocusEvent(const AppEvent& e); + static void dispatchMouseEvent(const AppEvent& e); // Connection and windows bool hasRequiredGlobals() const; @@ -487,12 +496,10 @@ void WaylandApi::Private::onWlKeyboardEnter(void* data, [[maybe_unused]] wl_keyb self->focusGainedInBatch = w; if(w->owner==nullptr) return; - FocusEvent e(true, Event::UnknownReason); - SystemApi::dispatchFocus(*w->owner, e); + appEvents.push_back({.type=Event::Focus, .w=w, .focusIn=true}); // Pointer already over the window: like an enter, a move to its position and the app's cursor. if(self->pointerFocus==w) { - MouseEvent m = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, m); + appEvents.push_back({.type=Event::MouseMove, .w=w}); SystemApi::showCursor(reinterpret_cast(w->surface), SystemApi::cursorShape(*w->owner)); } } @@ -511,8 +518,7 @@ void WaylandApi::Private::onWlKeyboardLeave(void* data, [[maybe_unused]] wl_keyb self->keyboardFocus = nullptr; if(w->owner==nullptr) return; - FocusEvent e(false, Event::UnknownReason); - SystemApi::dispatchFocus(*w->owner, e); + appEvents.push_back({.type=Event::Focus, .w=w, .focusIn=false}); if(self->pointerFocus==w) SystemApi::showCursor(reinterpret_cast(w->surface), SystemApi::cursorShape(*w->owner)); } @@ -526,20 +532,21 @@ void WaylandApi::Private::onWlKeyboardKey(void* data, [[maybe_unused]] wl_keyboa if(self->xkbState==nullptr) return; // no keymap compiled // Only releases of keys the app got a press for are dispatched (see heldKeys). + // heldKeys is updated after recording: if push_back throws, both stay consistent. const uint32_t keycode = key + 8; + const bool held = std::find(self->heldKeys.begin(), self->heldKeys.end(), keycode)!=self->heldKeys.end(); if(state==WL_KEYBOARD_KEY_STATE_PRESSED) { - if(std::find(self->heldKeys.begin(), self->heldKeys.end(), keycode)==self->heldKeys.end()) + appEvents.push_back(self->keyEvent(*self->keyboardFocus, keycode, Event::KeyDown)); + if(!held) self->heldKeys.push_back(keycode); - KeyEvent e = self->keyEvent(keycode, Event::KeyDown); - SystemApi::dispatchKeyDown(*self->keyboardFocus->owner, e, keycode); self->startKeyRepeat(keycode); } else { - if(std::erase(self->heldKeys, keycode)==0) + if(!held) return; + appEvents.push_back(self->keyEvent(*self->keyboardFocus, keycode, Event::KeyUp)); + std::erase(self->heldKeys, keycode); if(keycode==self->repeatKeycode) self->repeatKeycode = 0; - KeyEvent e = self->keyEvent(keycode, Event::KeyUp); - SystemApi::dispatchKeyUp(*self->keyboardFocus->owner, e, keycode); } } @@ -758,9 +765,10 @@ MouseEvent WaylandApi::Private::pointerEvent(const WWindow& w, Event::MouseButto return MouseEvent(x, y, button, Event::M_NoModifier, delta, 0, type); } -KeyEvent WaylandApi::Private::keyEvent(uint32_t keycode, Event::Type type) const { +AppEvent WaylandApi::Private::keyEvent(WWindow& w, uint32_t keycode, Event::Type type) const { // keycode: xkb keycode (evdev + 8). key: level-0 keysym of the active layout, so Shift+W stays K_W (like Windows' // virtual keys). code: the typed character, 0 if none (e.g. arrows, dead keys). + // Built when recorded, not when dispatched: later 'modifiers' events change xkbState (Shift released, CapsLock). const xkb_layout_index_t layout = xkb_state_key_get_layout(xkbState, keycode); const xkb_keysym_t* syms = nullptr; const int count = xkb_keymap_key_get_syms_by_level(xkbKeymap, keycode, layout, 0, &syms); @@ -771,7 +779,7 @@ KeyEvent WaylandApi::Private::keyEvent(uint32_t keycode, Event::Type type) const // Beyond it, nothing is typed (X11 and Windows deliver half a UTF-16 surrogate pair there). const uint32_t utf32 = xkb_state_key_get_utf32(xkbState, keycode); const uint32_t code = utf32<=0xFFFF ? utf32 : 0; - return KeyEvent(key, code, Event::M_NoModifier, type); + return {.type=type, .w=&w, .key=key, .code=code, .keycode=keycode}; } bool WaylandApi::Private::isWindowFocused(const WWindow* w) const { @@ -797,25 +805,15 @@ void WaylandApi::Private::setPointerLock(WWindow& w, bool lock) { void WaylandApi::Private::releaseHeldKeys(WWindow& w) { repeatKeycode = 0; - auto keys = std::move(heldKeys); // save from manipulation + for(uint32_t keycode:heldKeys) + appEvents.push_back(keyEvent(w, keycode, Event::KeyUp)); heldKeys.clear(); - if(w.owner==nullptr) - return; - for(uint32_t keycode:keys) { - KeyEvent e = keyEvent(keycode, Event::KeyUp); - SystemApi::dispatchKeyUp(*w.owner, e, keycode); - } } void WaylandApi::Private::releaseHeldButtons(WWindow& w) { - auto buttons = std::move(heldButtons); // save from manipulation + for(uint32_t button:heldButtons) + appEvents.push_back({.type=Event::MouseUp, .w=&w, .button=toButton(button)}); heldButtons.clear(); - if(w.owner==nullptr) - return; - for(uint32_t button:buttons) { - MouseEvent e = pointerEvent(w, toButton(button), 0, Event::MouseUp); - SystemApi::dispatchMouseUp(*w.owner, e); - } } void WaylandApi::Private::releasePointer() { @@ -862,8 +860,7 @@ void WaylandApi::Private::processKeyRepeat() { repeatNext += interval; if(repeatNext<=now) // avoid bursts repeatNext = now + interval; - KeyEvent e = keyEvent(repeatKeycode, Event::KeyDown); - SystemApi::dispatchKeyDown(*keyboardFocus->owner, e, repeatKeycode); + appEvents.push_back(keyEvent(*keyboardFocus, repeatKeycode, Event::KeyDown)); } void WaylandApi::Private::dispatchCloseEvent(WWindow& w) { @@ -877,6 +874,40 @@ void WaylandApi::Private::dispatchCloseEvent(WWindow& w) { SystemApi::exit(); } +void WaylandApi::Private::dispatchKeyEvent(const AppEvent& e) { + if(e.w->owner==nullptr) + return; + KeyEvent k(e.key, e.code, Event::M_NoModifier, e.type); + if(e.type==Event::KeyDown) + SystemApi::dispatchKeyDown(*e.w->owner, k, e.keycode); + else + SystemApi::dispatchKeyUp(*e.w->owner, k, e.keycode); + } + +void WaylandApi::Private::dispatchFocusEvent(const AppEvent& e) { + if(e.w->owner==nullptr) + return; + FocusEvent f(e.focusIn, Event::UnknownReason); + SystemApi::dispatchFocus(*e.w->owner, f); + } + +void WaylandApi::Private::dispatchMouseEvent(const AppEvent& e) { + // The position is read now, not when recorded (see WWindow::cursorX). + if(e.w->owner==nullptr) + return; + MouseEvent m = pointerEvent(*e.w, e.button, 0, e.type); + switch(e.type) { + case Event::MouseMove: + SystemApi::dispatchMouseMove(*e.w->owner, m); + break; + case Event::MouseUp: + SystemApi::dispatchMouseUp(*e.w->owner, m); + break; + default: + break; + } + } + //// /// CONNECTION AND WINDOWS // @@ -1146,6 +1177,17 @@ bool WaylandApi::Private::dispatchAppEvent() { case Event::Close: dispatchCloseEvent(*e.w); break; + case Event::KeyDown: + case Event::KeyUp: + dispatchKeyEvent(e); + break; + case Event::Focus: + dispatchFocusEvent(e); + break; + case Event::MouseMove: + case Event::MouseUp: + dispatchMouseEvent(e); + break; default: break; } From 3c48557227a78d1ab25150417718d062a73867b2 Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:57:26 +0200 Subject: [PATCH 15/17] pointer event to app queue --- Engine/system/api/waylandapi.cpp | 82 +++++++++++++++++++------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index a6bbb914..93ff485d 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -74,10 +74,14 @@ struct WWindow { zwp_locked_pointer_v1* lockedPointer = nullptr; bool pointerLocked = false; - // Cursor position. Button and axis events carry none. While pointer is locked, this is a - // virtual cursor moved by relative motion. - double cursorX = 0; // physical - double cursorY = 0; + // Pointer position as the compositor last reported it (enter, motion). Recorded into MouseMove events. + double compositorPointerX = 0; // physical + double compositorPointerY = 0; + + // Cursor as the app has seen it: set when a MouseMove is dispatched (or by setCursorPosition while locked), read + // for buttons and wheel, which carry no position. While the pointer is locked, a virtual cursor moved by relative motion. + double appCursorX = 0; // physical + double appCursorY = 0; bool readyToRender() const { return owner!=nullptr && configured && !suspended; } @@ -107,7 +111,10 @@ struct AppEvent { Tempest::Event::KeyType key = Tempest::Event::K_NoKey; // KeyDown/KeyUp uint32_t code = 0; // KeyDown/KeyUp: typed character uint32_t keycode = 0; // KeyDown/KeyUp: scancode for the dispatcher - Tempest::Event::MouseButton button = Tempest::Event::ButtonNone; // MouseUp + Tempest::Event::MouseButton button = Tempest::Event::ButtonNone; // MouseDown/MouseUp + int delta = 0; // MouseWheel + double x = 0; // MouseMove: physical position + double y = 0; bool focusIn = false; // Focus }; @@ -147,7 +154,7 @@ struct WaylandApi::Private { zwp_relative_pointer_v1* relativePointer = nullptr; // exists together with 'pointer' wl_keyboard* keyboard = nullptr; - // Pointer state between wl_pointer.enter and leave. Position is in WWindow::cursorX/Y + // Pointer state between wl_pointer.enter and leave. Position is in WWindow::compositorPointerX/Y WWindow* pointerFocus = nullptr; // window under pointer uint32_t pointerEnterSerial = 0; // serial of latest enter event @@ -349,14 +356,12 @@ void WaylandApi::Private::onWlPointerEnter(void* data, [[maybe_unused]] wl_point self->pointerFocus = w; self->pointerEnterSerial = serial; // logical -> physical - w->cursorX = wl_fixed_to_double(x)*w->scale(); - w->cursorY = wl_fixed_to_double(y)*w->scale(); + w->compositorPointerX = wl_fixed_to_double(x)*w->scale(); + w->compositorPointerY = wl_fixed_to_double(y)*w->scale(); if(w->owner==nullptr) return; - if(self->isWindowFocused(w)) { - MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, e); - } + if(self->isWindowFocused(w)) + appEvents.push_back({.type=Event::MouseMove, .w=w, .x=w->compositorPointerX, .y=w->compositorPointerY}); // Note: Wayland always requires cursor shape on enter (see implShowCursor) SystemApi::showCursor(reinterpret_cast(w->surface), SystemApi::cursorShape(*w->owner)); } @@ -374,22 +379,23 @@ void WaylandApi::Private::onWlPointerMotion(void* data, [[maybe_unused]] wl_poin WWindow* w = self->pointerFocus; if(w==nullptr) return; - w->cursorX = wl_fixed_to_double(x)*w->scale(); - w->cursorY = wl_fixed_to_double(y)*w->scale(); + w->compositorPointerX = wl_fixed_to_double(x)*w->scale(); + w->compositorPointerY = wl_fixed_to_double(y)*w->scale(); if(w->owner==nullptr || !self->isWindowFocused(w)) return; - MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, e); + appEvents.push_back({.type=Event::MouseMove, .w=w, .x=w->compositorPointerX, .y=w->compositorPointerY}); } void WaylandApi::Private::onWlPointerButton(void* data, [[maybe_unused]] wl_pointer* pointer, [[maybe_unused]] uint32_t serial, [[maybe_unused]] uint32_t time, uint32_t button, uint32_t state) { // Unknown buttons become ButtonNone, like X11. Releases only for presses the app got (see heldButtons). + // heldButtons is updated after recording: if push_back throws, both stay consistent. auto self = static_cast(data); WWindow* w = self->pointerFocus; if(w==nullptr || w->owner==nullptr) return; + const bool held = std::find(self->heldButtons.begin(), self->heldButtons.end(), button)!=self->heldButtons.end(); if(state==WL_POINTER_BUTTON_STATE_PRESSED) { if(!self->isWindowFocused(w)) return; @@ -397,15 +403,14 @@ void WaylandApi::Private::onWlPointerButton(void* data, [[maybe_unused]] wl_poin // wl_keyboard.enter and that press together, so a press in the same readEvents() batch as the enter is that click. if(self->focusGainedInBatch==w) return; - if(std::find(self->heldButtons.begin(), self->heldButtons.end(), button)==self->heldButtons.end()) + appEvents.push_back({.type=Event::MouseDown, .w=w, .button=toButton(button)}); + if(!held) self->heldButtons.push_back(button); - MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseDown); - SystemApi::dispatchMouseDown(*w->owner, e); } else { - if(std::erase(self->heldButtons, button)==0) + if(!held) return; - MouseEvent e = pointerEvent(*w, toButton(button), 0, Event::MouseUp); - SystemApi::dispatchMouseUp(*w->owner, e); + appEvents.push_back({.type=Event::MouseUp, .w=w, .button=toButton(button)}); + std::erase(self->heldButtons, button); } } @@ -419,8 +424,7 @@ void WaylandApi::Private::onWlPointerAxisValue120(void* data, [[maybe_unused]] w WWindow* w = self->pointerFocus; if(w==nullptr || w->owner==nullptr || !self->isWindowFocused(w)) return; - MouseEvent e = pointerEvent(*w, Event::ButtonNone, -value120, Event::MouseWheel); - SystemApi::dispatchMouseWheel(*w->owner, e); + appEvents.push_back({.type=Event::MouseWheel, .w=w, .delta=-value120}); } void WaylandApi::Private::onZwpRelativePointerV1RelativeMotion(void* data, [[maybe_unused]] zwp_relative_pointer_v1* relativePointer, @@ -435,8 +439,8 @@ void WaylandApi::Private::onZwpRelativePointerV1RelativeMotion(void* data, [[may if(w==nullptr || !w->pointerLocked || w->owner==nullptr) return; const double s = w->scale(); - w->cursorX = std::clamp(w->cursorX + wl_fixed_to_double(dxUnaccel)*s, 0.0, double(std::max(w->physicalWidth() -1, 0))); - w->cursorY = std::clamp(w->cursorY + wl_fixed_to_double(dyUnaccel)*s, 0.0, double(std::max(w->physicalHeight()-1, 0))); + w->appCursorX = std::clamp(w->appCursorX + wl_fixed_to_double(dxUnaccel)*s, 0.0, double(std::max(w->physicalWidth() -1, 0))); + w->appCursorY = std::clamp(w->appCursorY + wl_fixed_to_double(dyUnaccel)*s, 0.0, double(std::max(w->physicalHeight()-1, 0))); MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); SystemApi::dispatchMouseMove(*w->owner, e); } @@ -499,7 +503,7 @@ void WaylandApi::Private::onWlKeyboardEnter(void* data, [[maybe_unused]] wl_keyb appEvents.push_back({.type=Event::Focus, .w=w, .focusIn=true}); // Pointer already over the window: like an enter, a move to its position and the app's cursor. if(self->pointerFocus==w) { - appEvents.push_back({.type=Event::MouseMove, .w=w}); + appEvents.push_back({.type=Event::MouseMove, .w=w, .x=w->compositorPointerX, .y=w->compositorPointerY}); SystemApi::showCursor(reinterpret_cast(w->surface), SystemApi::cursorShape(*w->owner)); } } @@ -760,8 +764,8 @@ wp_cursor_shape_device_v1_shape WaylandApi::Private::toCursorShape(CursorShape s MouseEvent WaylandApi::Private::pointerEvent(const WWindow& w, Event::MouseButton button, int delta, Event::Type type) { // floor: the physical pixel the cursor is in. - const int x = int(std::floor(w.cursorX)); - const int y = int(std::floor(w.cursorY)); + const int x = int(std::floor(w.appCursorX)); + const int y = int(std::floor(w.appCursorY)); return MouseEvent(x, y, button, Event::M_NoModifier, delta, 0, type); } @@ -892,17 +896,27 @@ void WaylandApi::Private::dispatchFocusEvent(const AppEvent& e) { } void WaylandApi::Private::dispatchMouseEvent(const AppEvent& e) { - // The position is read now, not when recorded (see WWindow::cursorX). + // A move updates the app's cursor (see WWindow::appCursorX) if(e.w->owner==nullptr) return; - MouseEvent m = pointerEvent(*e.w, e.button, 0, e.type); + if(e.type==Event::MouseMove) { + e.w->appCursorX = e.x; + e.w->appCursorY = e.y; + } + MouseEvent m = pointerEvent(*e.w, e.button, e.delta, e.type); switch(e.type) { case Event::MouseMove: SystemApi::dispatchMouseMove(*e.w->owner, m); break; + case Event::MouseDown: + SystemApi::dispatchMouseDown(*e.w->owner, m); + break; case Event::MouseUp: SystemApi::dispatchMouseUp(*e.w->owner, m); break; + case Event::MouseWheel: + SystemApi::dispatchMouseWheel(*e.w->owner, m); + break; default: break; } @@ -1185,7 +1199,9 @@ bool WaylandApi::Private::dispatchAppEvent() { dispatchFocusEvent(e); break; case Event::MouseMove: + case Event::MouseDown: case Event::MouseUp: + case Event::MouseWheel: dispatchMouseEvent(e); break; default: @@ -1337,8 +1353,8 @@ void WaylandApi::implSetCursorPosition(SystemApi::Window* w, int x, int y) { auto ww = toWWindow(w); if(!ww->pointerLocked) return; - ww->cursorX = x; - ww->cursorY = y; + ww->appCursorX = x; + ww->appCursorY = y; } void WaylandApi::implShowCursor(SystemApi::Window* w, CursorShape show) { From 723ee84ebb15957ce329578fc9ba30f364ab330c Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:33:59 +0200 Subject: [PATCH 16/17] relative motion to app queue --- Engine/system/api/waylandapi.cpp | 35 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index 93ff485d..f7c8cc33 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -108,14 +108,15 @@ struct AppEvent { Tempest::Event::Type type; WWindow* w; // Data per type, recorded by the listener - Tempest::Event::KeyType key = Tempest::Event::K_NoKey; // KeyDown/KeyUp - uint32_t code = 0; // KeyDown/KeyUp: typed character - uint32_t keycode = 0; // KeyDown/KeyUp: scancode for the dispatcher - Tempest::Event::MouseButton button = Tempest::Event::ButtonNone; // MouseDown/MouseUp - int delta = 0; // MouseWheel - double x = 0; // MouseMove: physical position - double y = 0; - bool focusIn = false; // Focus + Tempest::Event::KeyType key = Tempest::Event::K_NoKey; // KeyDown/KeyUp + uint32_t code = 0; // KeyDown/KeyUp: typed character + uint32_t keycode = 0; // KeyDown/KeyUp: scancode for the dispatcher + Tempest::Event::MouseButton button = Tempest::Event::ButtonNone; // MouseDown/MouseUp + int delta = 0; // MouseWheel + double x = 0; // MouseMove: physical position, or delta if relative + double y = 0; + bool relative = false; // MouseMove: x/y move the virtual cursor (pointer locked) + bool focusIn = false; // Focus }; //// @@ -431,18 +432,16 @@ void WaylandApi::Private::onZwpRelativePointerV1RelativeMotion(void* data, [[may [[maybe_unused]] uint32_t utimeHi, [[maybe_unused]] uint32_t utimeLo, [[maybe_unused]] wl_fixed_t dx, [[maybe_unused]] wl_fixed_t dy, wl_fixed_t dxUnaccel, wl_fixed_t dyUnaccel) { - // Moves virtual cursor while pointer is locked (otherwise wl_pointer.motion has the position). Raw input deltas - // clamped to the window like a real cursor. + // Moves virtual cursor while pointer is locked (otherwise wl_pointer.motion has the position). Raw input deltas, + // added at dispatch (see dispatchMouseEvent). // Note: compositor only activates the lock while focused auto self = static_cast(data); WWindow* w = self->pointerFocus; if(w==nullptr || !w->pointerLocked || w->owner==nullptr) return; const double s = w->scale(); - w->appCursorX = std::clamp(w->appCursorX + wl_fixed_to_double(dxUnaccel)*s, 0.0, double(std::max(w->physicalWidth() -1, 0))); - w->appCursorY = std::clamp(w->appCursorY + wl_fixed_to_double(dyUnaccel)*s, 0.0, double(std::max(w->physicalHeight()-1, 0))); - MouseEvent e = pointerEvent(*w, Event::ButtonNone, 0, Event::MouseMove); - SystemApi::dispatchMouseMove(*w->owner, e); + appEvents.push_back({.type=Event::MouseMove, .w=w, .x=wl_fixed_to_double(dxUnaccel)*s, .y=wl_fixed_to_double(dyUnaccel)*s, + .relative=true}); } void WaylandApi::Private::onWlKeyboardKeymap(void* data, [[maybe_unused]] wl_keyboard* keyboard, @@ -896,10 +895,14 @@ void WaylandApi::Private::dispatchFocusEvent(const AppEvent& e) { } void WaylandApi::Private::dispatchMouseEvent(const AppEvent& e) { - // A move updates the app's cursor (see WWindow::appCursorX) + // A move updates the app's cursor (see WWindow::appCursorX). A relative move adds to where the app last saw it, + // incl. its own setCursorPosition (mouse-look recentring), clamped to the window like a real cursor. if(e.w->owner==nullptr) return; - if(e.type==Event::MouseMove) { + if(e.type==Event::MouseMove && e.relative) { + e.w->appCursorX = std::clamp(e.w->appCursorX + e.x, 0.0, double(std::max(e.w->physicalWidth() -1, 0))); + e.w->appCursorY = std::clamp(e.w->appCursorY + e.y, 0.0, double(std::max(e.w->physicalHeight()-1, 0))); + } else if(e.type==Event::MouseMove) { e.w->appCursorX = e.x; e.w->appCursorY = e.y; } From 1eeb3a4bcace5c6ed6e41af6ce8b364361c779be Mon Sep 17 00:00:00 2001 From: LordKobra <39347022+LordKobra@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:16 +0200 Subject: [PATCH 17/17] rework focus click filter to timer --- Engine/system/api/waylandapi.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Engine/system/api/waylandapi.cpp b/Engine/system/api/waylandapi.cpp index f7c8cc33..5ccc3ac0 100644 --- a/Engine/system/api/waylandapi.cpp +++ b/Engine/system/api/waylandapi.cpp @@ -178,8 +178,8 @@ struct WaylandApi::Private { // Non-zero only while keyboardFocus is set and repeatRate>0. std::chrono::steady_clock::time_point repeatNext; // when the next repeat is due - // WWindow* if a window gained focus during active readEvents() batch, otherwise nullptr (for focus click filter) - WWindow* focusGainedInBatch = nullptr; + // When the last wl_keyboard.enter was read (for the focus click filter) + std::chrono::steady_clock::time_point focusInTime; std::vector> windows; // owns the windows; each WWindow keeps its address (handle, listener data) static inline std::deque appEvents; // in the order the compositor sent them @@ -400,9 +400,8 @@ void WaylandApi::Private::onWlPointerButton(void* data, [[maybe_unused]] wl_poin if(state==WL_POINTER_BUTTON_STATE_PRESSED) { if(!self->isWindowFocused(w)) return; - // Focus click filter: the click that only focuses the window must not reach the app. KWin sends - // wl_keyboard.enter and that press together, so a press in the same readEvents() batch as the enter is that click. - if(self->focusGainedInBatch==w) + // Focus click filter: the click that focuses the window comes right after wl_keyboard.enter and doesn't reach the app. + if(std::chrono::steady_clock::now()-self->focusInTime < std::chrono::milliseconds(50)) return; appEvents.push_back({.type=Event::MouseDown, .w=w, .button=toButton(button)}); if(!held) @@ -495,8 +494,8 @@ void WaylandApi::Private::onWlKeyboardEnter(void* data, [[maybe_unused]] wl_keyb if(surface==nullptr) return; // the surface was destroyed after the compositor sent the event auto w = static_cast(wl_surface_get_user_data(surface)); - self->keyboardFocus = w; - self->focusGainedInBatch = w; + self->keyboardFocus = w; + self->focusInTime = std::chrono::steady_clock::now(); if(w->owner==nullptr) return; appEvents.push_back({.type=Event::Focus, .w=w, .focusIn=true}); @@ -1103,8 +1102,6 @@ void WaylandApi::Private::destroyWindow(WWindow* w) { heldKeys.clear(); heldButtons.clear(); } - if(focusGainedInBatch==w) - focusGainedInBatch = nullptr; setPointerLock(*w, false); // created for the wl_surface ioDestroy(w->fractionalScale, wp_fractional_scale_v1_destroy); @@ -1151,7 +1148,6 @@ void WaylandApi::Private::readEvents(const SystemApi::AppCallBack& cb) { // Doesn't block beyond pollTimeout(): Tempest's loop must keep running (timers, rendering). // The Vulkan driver reads the same connection on its own queue; prepare_read -> read_events/cancel_read // is how several readers share the socket without stealing or blocking each other. - focusGainedInBatch = nullptr; // a new batch (see onWlPointerButton) while(wl_display_prepare_read(display)!=0) { // Our queue already holds events (e.g. read by the driver): dispatch them before reading more. if(wl_display_dispatch_pending(display)<0) {