| // |
| // Copyright 2022 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| |
| // WaylandWindow.cpp: Implementation of OSWindow for Wayland |
| |
| #include "util/linux/wayland/WaylandWindow.h" |
| #include "common/unsafe_buffers.h" |
| |
| #include <linux/input-event-codes.h> |
| #include <sys/mman.h> |
| #include <sys/timerfd.h> |
| #include <unistd.h> |
| |
| #include <cerrno> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <cstring> |
| |
| #include "common/debug.h" |
| |
| // C++ workaround for Wayland macro |
| #define WL_ARRAY_FOR_EACH(pos, array, type) \ |
| for (pos = (type)(array)->data; \ |
| (const char *)pos < ((const char *)(array)->data + (array)->size); (pos)++) |
| |
| // The wl_display is intentionally process-scoped: per Wayland convention the |
| // connection lives for the lifetime of the process. Tying it to per-WaylandWindow |
| // lifetime breaks multi-window apps and corrupts Mesa Vulkan WSI internal state |
| // that retains wl_display proxy wrappers across vkCreateSwapchainKHR calls. |
| static struct wl_display *gDisplay = nullptr; |
| static bool gLocalDisplay = false; |
| |
| namespace |
| { |
| // Forward declaration so messageLoop (defined earlier in this file than the |
| // keymap table at the bottom) can call it for synthetic key-repeat events. |
| angle::KeyType LinuxKeyCodeToKey(uint32_t code); |
| |
| void DisconnectGDisplayAtExit() |
| { |
| if (gLocalDisplay && gDisplay) |
| { |
| wl_display_flush(gDisplay); |
| wl_display_disconnect(gDisplay); |
| gDisplay = nullptr; |
| gLocalDisplay = false; |
| } |
| } |
| } // namespace |
| |
| WaylandWindow::WaylandWindow(void *nativeDisplay) |
| : mCompositor{nullptr}, mSurface{nullptr}, mWindow{nullptr} |
| { |
| if (nativeDisplay != nullptr) |
| { |
| if (gDisplay != nullptr && nativeDisplay != gDisplay) |
| { |
| if (gLocalDisplay) |
| { |
| wl_display_disconnect(gDisplay); |
| gLocalDisplay = false; |
| } |
| fprintf(stderr, "WaylandWindow::WaylandWindow: Display reassignment\n"); |
| } |
| gDisplay = reinterpret_cast<wl_display *>(nativeDisplay); |
| } |
| else if (!gDisplay) |
| { |
| gDisplay = wl_display_connect(nullptr); |
| if (gDisplay) |
| { |
| gLocalDisplay = true; |
| std::atexit(DisconnectGDisplayAtExit); |
| } |
| } |
| } |
| |
| WaylandWindow::~WaylandWindow() |
| { |
| destroy(); |
| // Do not disconnect gDisplay here: see the comment on the static above. |
| // The atexit handler installed when we connected handles cleanup at |
| // process exit so Valgrind doesn't see a leak. |
| } |
| |
| void WaylandWindow::RegistryHandleGlobal(void *data, |
| struct wl_registry *registry, |
| uint32_t name, |
| const char *interface, |
| uint32_t version) |
| { |
| WaylandWindow *vc = reinterpret_cast<WaylandWindow *>(data); |
| |
| if (ANGLE_UNSAFE_TODO(strcmp(interface, xdg_wm_base_interface.name)) == 0) |
| { |
| vc->mXdgWmBase = static_cast<xdg_wm_base *>(wl_registry_bind( |
| registry, name, &xdg_wm_base_interface, std::min(static_cast<uint32_t>(3), version))); |
| xdg_wm_base_add_listener(vc->mXdgWmBase, &xdg_wm_base_listener, data); |
| } |
| else if (ANGLE_UNSAFE_TODO(strcmp(interface, wl_compositor_interface.name)) == 0) |
| { |
| // v4+ is needed for wl_surface.damage_buffer (opcode 9). Cap at 4 since |
| // we don't use later additions (v5 offset / v6 preferred_buffer_*). |
| vc->mCompositor = static_cast<wl_compositor *>(wl_registry_bind( |
| registry, name, &wl_compositor_interface, std::min(static_cast<uint32_t>(4), version))); |
| } |
| else if (ANGLE_UNSAFE_TODO(strcmp(interface, wl_seat_interface.name)) == 0) |
| { |
| // v5+ gives us pointer.frame / axis_discrete; clamp to 5 since that |
| // covers every common compositor and our pointer listener stops there. |
| vc->mSeat = static_cast<wl_seat *>(wl_registry_bind( |
| registry, name, &wl_seat_interface, std::min(static_cast<uint32_t>(5), version))); |
| wl_seat_add_listener(vc->mSeat, &seat_listener, vc); |
| } |
| else if (ANGLE_UNSAFE_TODO(strcmp(interface, wl_shm_interface.name)) == 0) |
| { |
| // Bound for the setVisible(true) placeholder buffer. Version 1 is |
| // sufficient -- we only use create_pool/create_buffer. |
| vc->mShm = static_cast<wl_shm *>(wl_registry_bind(registry, name, &wl_shm_interface, 1)); |
| } |
| else if (ANGLE_UNSAFE_TODO(strcmp(interface, zxdg_decoration_manager_v1_interface.name)) == 0) |
| { |
| // Optional global. Compositors that don't advertise it leave this |
| // null; the window is then decorationless. |
| vc->mDecorationManager = static_cast<zxdg_decoration_manager_v1 *>( |
| wl_registry_bind(registry, name, &zxdg_decoration_manager_v1_interface, 1)); |
| } |
| } |
| |
| void WaylandWindow::RegistryHandleGlobalRemove(void *data, |
| struct wl_registry *registry, |
| uint32_t name) |
| {} |
| |
| const struct wl_registry_listener WaylandWindow::registryListener = { |
| WaylandWindow::RegistryHandleGlobal, WaylandWindow::RegistryHandleGlobalRemove}; |
| |
| bool WaylandWindow::initializeImpl(const std::string &name, int width, int height) |
| { |
| destroy(); |
| |
| if (!gDisplay) |
| { |
| gDisplay = wl_display_connect(nullptr); |
| gLocalDisplay = true; |
| if (!gDisplay) |
| { |
| return false; |
| } |
| } |
| |
| // Get registry of display |
| struct wl_registry *registry = wl_display_get_registry(gDisplay); |
| wl_registry_add_listener(registry, ®istryListener, this); |
| |
| // Round-trip to get globals |
| wl_display_roundtrip(gDisplay); |
| if (!mCompositor) |
| { |
| return false; |
| } |
| |
| // We don't need this anymore; we don't track output changes |
| wl_registry_destroy(registry); |
| |
| mSurface = wl_compositor_create_surface(mCompositor); |
| if (!mSurface) |
| { |
| return false; |
| } |
| |
| { |
| mXdgSurface = xdg_wm_base_get_xdg_surface(mXdgWmBase, mSurface); |
| xdg_surface_add_listener(mXdgSurface, &xdg_surface_listener, this); |
| |
| mXdgToplevel = xdg_surface_get_toplevel(mXdgSurface); |
| xdg_toplevel_add_listener(mXdgToplevel, &xdg_toplevel_listener, this); |
| xdg_toplevel_set_app_id(mXdgToplevel, name.c_str()); |
| xdg_toplevel_set_title(mXdgToplevel, name.c_str()); |
| |
| // Request server-side decorations. Must happen before the initial |
| // surface commit so the first configure carries the granted mode. |
| // Null mDecorationManager means the compositor doesn't expose the |
| // protocol; the window is then decorationless. |
| if (mDecorationManager) |
| { |
| mToplevelDecoration = zxdg_decoration_manager_v1_get_toplevel_decoration( |
| mDecorationManager, mXdgToplevel); |
| zxdg_toplevel_decoration_v1_add_listener(mToplevelDecoration, &decoration_listener, |
| this); |
| zxdg_toplevel_decoration_v1_set_mode(mToplevelDecoration, |
| ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE); |
| } |
| |
| mWaitForConfigure = true; |
| wl_surface_commit(mSurface); |
| while (mWaitForConfigure) |
| { |
| if (wl_display_dispatch(gDisplay) < 0) |
| { |
| break; |
| } |
| } |
| } |
| |
| mWindow = wl_egl_window_create(mSurface, width, height); |
| if (!mWindow) |
| { |
| return false; |
| } |
| |
| fds[0] = {wl_display_get_fd(gDisplay), POLLIN, 0}; |
| // Lazy-create the key-repeat timerfd (one per WaylandWindow). Closed in |
| // destroy(). Non-blocking + cloexec is the Wayland-client convention. |
| if (mRepeatTimerFd < 0) |
| { |
| mRepeatTimerFd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); |
| } |
| fds[1] = {mRepeatTimerFd, POLLIN, 0}; |
| |
| mY = 0; |
| mX = 0; |
| mWidth = width; |
| mHeight = height; |
| |
| return true; |
| } |
| |
| void WaylandWindow::disableErrorMessageDialog() {} |
| |
| void WaylandWindow::destroy() |
| { |
| // Tear down in reverse-dependency order so children are released before |
| // their parents: input objects -> wl_egl_window -> wl_surface chain -> |
| // wl_seat / xdg_wm_base / wl_compositor (the registry globals). |
| if (mKeyboard) |
| { |
| wl_keyboard_release(mKeyboard); |
| mKeyboard = nullptr; |
| } |
| if (mPointer) |
| { |
| wl_pointer_release(mPointer); |
| mPointer = nullptr; |
| } |
| if (mSeat) |
| { |
| wl_seat_destroy(mSeat); |
| mSeat = nullptr; |
| } |
| |
| if (mRepeatTimerFd >= 0) |
| { |
| close(mRepeatTimerFd); |
| mRepeatTimerFd = -1; |
| mRepeatKey = 0; |
| } |
| |
| releaseShmPlaceholder(); |
| mEglHandoffComplete = false; |
| |
| if (mShm) |
| { |
| wl_shm_destroy(mShm); |
| mShm = nullptr; |
| } |
| |
| if (mWindow) |
| { |
| wl_egl_window_destroy(mWindow); |
| mWindow = nullptr; |
| } |
| |
| // The toplevel_decoration is a child of xdg_toplevel; release it first. |
| if (mToplevelDecoration) |
| { |
| zxdg_toplevel_decoration_v1_destroy(mToplevelDecoration); |
| mToplevelDecoration = nullptr; |
| } |
| |
| if (mXdgToplevel) |
| { |
| xdg_toplevel_destroy(mXdgToplevel); |
| mXdgToplevel = nullptr; |
| } |
| |
| if (mXdgSurface) |
| { |
| xdg_surface_destroy(mXdgSurface); |
| mXdgSurface = nullptr; |
| } |
| |
| if (mSurface) |
| { |
| wl_surface_destroy(mSurface); |
| mSurface = nullptr; |
| } |
| |
| if (mDecorationManager) |
| { |
| zxdg_decoration_manager_v1_destroy(mDecorationManager); |
| mDecorationManager = nullptr; |
| } |
| |
| if (mXdgWmBase) |
| { |
| xdg_wm_base_destroy(mXdgWmBase); |
| mXdgWmBase = nullptr; |
| } |
| |
| if (mCompositor) |
| { |
| wl_compositor_destroy(mCompositor); |
| mCompositor = nullptr; |
| } |
| } |
| |
| void WaylandWindow::resetNativeWindow() {} |
| |
| void WaylandWindow::setNativeDisplay(EGLNativeDisplayType display) |
| { |
| wl_display *newDisplay = reinterpret_cast<wl_display *>(display); |
| if (gDisplay != nullptr && newDisplay != gDisplay) |
| { |
| if (gLocalDisplay) |
| { |
| // A window opened a local connection we haven't created surfaces |
| // on yet; drop it and use the display provided by the caller. |
| wl_display_disconnect(gDisplay); |
| gLocalDisplay = false; |
| } |
| fprintf(stderr, "WaylandWindow::setNativeDisplay: Display reassignment\n"); |
| } |
| gDisplay = newDisplay; |
| } |
| |
| EGLNativeWindowType WaylandWindow::getNativeWindow() const |
| { |
| // First call hands the wl_surface to EGL (the caller passes this to |
| // eglCreateWindowSurface or an equivalent path used by dEQP and the |
| // perf tests). Retire the shm placeholder so subsequent setVisible/ |
| // resize stop touching the surface behind EGL's back. |
| if (!mEglHandoffComplete) |
| { |
| mEglHandoffComplete = true; |
| releaseShmPlaceholder(); |
| } |
| return reinterpret_cast<EGLNativeWindowType>(mWindow); |
| } |
| |
| EGLNativeDisplayType WaylandWindow::getNativeDisplay() const |
| { |
| return reinterpret_cast<EGLNativeDisplayType>(gDisplay); |
| } |
| |
| EGLenum WaylandWindow::getNativeDisplayPlatformType() const |
| { |
| return EGL_PLATFORM_WAYLAND_EXT; |
| } |
| |
| void WaylandWindow::messageLoop() |
| { |
| while (wl_display_prepare_read(gDisplay) != 0) |
| { |
| wl_display_dispatch_pending(gDisplay); |
| } |
| if (wl_display_flush(gDisplay) < 0 && errno != EAGAIN) |
| { |
| wl_display_cancel_read(gDisplay); |
| return; |
| } |
| |
| // Poll both wayland and the repeat timer. Timeout 0 = non-blocking; this |
| // function is invoked from a tight render loop, so we never want to wait. |
| const nfds_t nfds = (mRepeatTimerFd >= 0) ? 2 : 1; |
| const int ret = poll(fds, nfds, 0); |
| |
| if (ret > 0 && (fds[0].revents & POLLIN)) |
| { |
| wl_display_read_events(gDisplay); |
| wl_display_dispatch_pending(gDisplay); |
| } |
| else |
| { |
| wl_display_cancel_read(gDisplay); |
| } |
| |
| if (ret > 0 && nfds > 1 && (fds[1].revents & POLLIN) && mRepeatKey != 0) |
| { |
| uint64_t expirations = 0; |
| if (read(mRepeatTimerFd, &expirations, sizeof(expirations)) == sizeof(expirations)) |
| { |
| // Cap to a sane upper bound so a stalled loop catching up doesn't |
| // dump hundreds of synthetic key events at once. |
| const uint64_t kMaxBurst = 8; |
| const uint64_t n = std::min(expirations, kMaxBurst); |
| for (uint64_t i = 0; i < n; ++i) |
| { |
| Event event{}; |
| event.Type = Event::EVENT_KEY_PRESSED; |
| event.Key.Code = LinuxKeyCodeToKey(mRepeatKey); |
| event.Key.Shift = mShiftDown; |
| event.Key.Control = mControlDown; |
| event.Key.Alt = mAltDown; |
| event.Key.System = mSuperDown; |
| pushEvent(std::move(event)); |
| } |
| } |
| } |
| } |
| |
| void WaylandWindow::setMousePosition(int x, int y) {} |
| |
| bool WaylandWindow::setOrientation(int /*width*/, int /*height*/) |
| { |
| UNIMPLEMENTED(); |
| return false; |
| } |
| |
| bool WaylandWindow::setPosition(int /*x*/, int /*y*/) |
| { |
| // Wayland forbids client-initiated absolute window positioning; the |
| // compositor places toplevels. Returning true keeps the OSWindow contract |
| // for callers that don't gate on the result. |
| return true; |
| } |
| |
| bool WaylandWindow::resize(int width, int height) |
| { |
| // mWindow may be null during the initial xdg_toplevel configure (which can |
| // arrive before initializeImpl creates the wl_egl_window). |
| if (mWindow) |
| { |
| wl_egl_window_resize(mWindow, width, height, 0, 0); |
| } |
| |
| // pushEvent below updates mWidth/mHeight via OSWindow::pushEvent; do it now |
| // so ensureShmBuffer (which reads mWidth/mHeight) sees the new dimensions. |
| const bool sizeChanged = (width != mWidth || height != mHeight); |
| if (sizeChanged) |
| { |
| Event event{}; |
| event.Type = Event::EVENT_RESIZED; |
| event.Size.Width = width; |
| event.Size.Height = height; |
| pushEvent(std::move(event)); |
| } |
| |
| // Reallocate the placeholder for the new size, only while we still own |
| // the surface. After handoff mShmBuffer is null and this block is |
| // skipped; EGL handles resize via wl_egl_window_resize above. |
| if (sizeChanged && mVisible && mShmBuffer && mSurface && mShm) |
| { |
| wl_buffer_destroy(mShmBuffer); |
| mShmBuffer = nullptr; |
| mShmBufferWidth = 0; |
| mShmBufferHeight = 0; |
| if (ensureShmBuffer()) |
| { |
| wl_surface_attach(mSurface, mShmBuffer, 0, 0); |
| wl_surface_damage_buffer(mSurface, 0, 0, mShmBufferWidth, mShmBufferHeight); |
| wl_surface_commit(mSurface); |
| wl_display_flush(gDisplay); |
| } |
| } |
| |
| return true; |
| } |
| |
| bool WaylandWindow::ensureShmBuffer() |
| { |
| if (mShmBuffer != nullptr) |
| { |
| return true; |
| } |
| if (!mShm) |
| { |
| // Compositor didn't advertise wl_shm; nothing we can attach. |
| return false; |
| } |
| |
| const int32_t width = mWidth > 0 ? mWidth : 1; |
| const int32_t height = mHeight > 0 ? mHeight : 1; |
| const int32_t stride = width * 4; |
| const off_t size = static_cast<off_t>(stride) * height; |
| |
| int fd = memfd_create("waylandwindow-shm", MFD_CLOEXEC); |
| if (fd < 0) |
| { |
| return false; |
| } |
| if (ftruncate(fd, size) < 0) |
| { |
| close(fd); |
| return false; |
| } |
| |
| void *data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); |
| if (data == MAP_FAILED) |
| { |
| close(fd); |
| return false; |
| } |
| |
| // Fill with opaque dark gray so a surface that never gets EGL-rendered |
| // still has visible pixels. WL_SHM_FORMAT_ARGB8888 specifies the byte |
| // sequence B, G, R, A in memory regardless of CPU endianness, so we |
| // write bytes directly rather than a uint32_t. |
| static constexpr uint8_t kPixel[4] = {0x20, 0x20, 0x20, 0xFF}; // B, G, R, A |
| uint8_t *bytes = static_cast<uint8_t *>(data); |
| for (int32_t i = 0; i < width * height; ++i) |
| { |
| ANGLE_UNSAFE_TODO(std::memcpy(bytes + i * 4, kPixel, sizeof(kPixel))); |
| } |
| munmap(data, size); |
| |
| struct wl_shm_pool *pool = wl_shm_create_pool(mShm, fd, static_cast<int32_t>(size)); |
| mShmBuffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); |
| wl_shm_pool_destroy(pool); |
| close(fd); |
| |
| if (!mShmBuffer) |
| { |
| return false; |
| } |
| mShmBufferWidth = width; |
| mShmBufferHeight = height; |
| return true; |
| } |
| |
| void WaylandWindow::releaseShmPlaceholder() const |
| { |
| if (mShmBuffer) |
| { |
| wl_buffer_destroy(mShmBuffer); |
| mShmBuffer = nullptr; |
| mShmBufferWidth = 0; |
| mShmBufferHeight = 0; |
| } |
| } |
| |
| void WaylandWindow::setVisible(bool isVisible) |
| { |
| // Idempotent: skip the protocol round-trip if state already matches. |
| if (mVisible == isVisible) |
| { |
| return; |
| } |
| |
| if (!mSurface || !gDisplay) |
| { |
| return; |
| } |
| |
| // After EGL handoff the swap chain owns the wl_surface; only the |
| // visibility flag can change here. The compositor keeps showing |
| // EGL's most recent frame until eglSwapBuffers replaces it. |
| if (mEglHandoffComplete) |
| { |
| mVisible = isVisible; |
| return; |
| } |
| |
| if (isVisible) |
| { |
| // Compositors ignore xdg_toplevel until they see a buffered |
| // surface, so map by attaching the placeholder + committing. |
| if (!ensureShmBuffer()) |
| { |
| return; |
| } |
| wl_surface_attach(mSurface, mShmBuffer, 0, 0); |
| wl_surface_damage_buffer(mSurface, 0, 0, mShmBufferWidth, mShmBufferHeight); |
| wl_surface_commit(mSurface); |
| } |
| else |
| { |
| // Unmap: NULL buffer attach + commit. The compositor removes the |
| // surface from screen until a new buffer is attached. |
| wl_surface_attach(mSurface, nullptr, 0, 0); |
| wl_surface_commit(mSurface); |
| } |
| |
| mVisible = isVisible; |
| wl_display_flush(gDisplay); |
| } |
| |
| void WaylandWindow::signalTestEvent() |
| { |
| Event event{}; |
| event.Type = Event::EVENT_TEST; |
| pushEvent(std::move(event)); |
| } |
| |
| void WaylandWindow::xdg_wm_base_ping(void * /* data */, |
| struct xdg_wm_base *xdg_wm_base, |
| uint32_t serial) |
| { |
| xdg_wm_base_pong(xdg_wm_base, serial); |
| } |
| |
| const struct xdg_wm_base_listener WaylandWindow::xdg_wm_base_listener = { |
| .ping = xdg_wm_base_ping, |
| }; |
| |
| void WaylandWindow::handle_xdg_surface_configure(void *data, |
| struct xdg_surface *xdg_surface, |
| uint32_t serial) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| xdg_surface_ack_configure(xdg_surface, serial); |
| w->mWaitForConfigure = false; |
| } |
| |
| const struct xdg_surface_listener WaylandWindow::xdg_surface_listener = { |
| .configure = handle_xdg_surface_configure}; |
| |
| void WaylandWindow::handle_toplevel_configure(void *data, |
| struct xdg_toplevel * /* toplevel */, |
| int32_t width, |
| int32_t height, |
| struct wl_array *states) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| |
| // Snapshot mActivated before recomputing so GAINED/LOST fire exactly once |
| // per real focus change. The activated bit is the WM-level focus signal; |
| // wl_keyboard enter/leave is input-routing and is unreliable (headless |
| // compositors don't advertise a keyboard capability). |
| const bool wasActivated = w->mActivated; |
| w->mFullscreen = false; |
| w->mMaximized = false; |
| w->mResize = false; |
| w->mActivated = false; |
| |
| const uint32_t *state; |
| ANGLE_UNSAFE_TODO(WL_ARRAY_FOR_EACH(state, states, const uint32_t *)) |
| { |
| switch (*state) |
| { |
| case XDG_TOPLEVEL_STATE_FULLSCREEN: |
| w->mFullscreen = true; |
| break; |
| case XDG_TOPLEVEL_STATE_MAXIMIZED: |
| w->mMaximized = true; |
| break; |
| case XDG_TOPLEVEL_STATE_RESIZING: |
| // EVENT_RESIZED is emitted from WaylandWindow::resize() below, |
| // using the post-clamp mGeometry -- the raw configure (width, |
| // height) here can be (0, 0) meaning "client picks". |
| w->mResize = true; |
| break; |
| case XDG_TOPLEVEL_STATE_ACTIVATED: |
| w->mActivated = true; |
| break; |
| } |
| } |
| |
| if (w->mActivated != wasActivated) |
| { |
| Event event{}; |
| event.Type = w->mActivated ? Event::EVENT_GAINED_FOCUS : Event::EVENT_LOST_FOCUS; |
| w->pushEvent(std::move(event)); |
| } |
| |
| if (width > 0 && height > 0) |
| { |
| if (!w->mFullscreen && !w->mMaximized) |
| { |
| w->mWindowSize.width = width; |
| w->mWindowSize.height = height; |
| } |
| w->mGeometry.width = width; |
| w->mGeometry.height = height; |
| } |
| else if (!w->mFullscreen && !w->mMaximized) |
| { |
| w->mGeometry.width = w->mWindowSize.width; |
| w->mGeometry.height = w->mWindowSize.height; |
| } |
| |
| w->resize(w->mGeometry.width, w->mGeometry.height); |
| } |
| |
| void WaylandWindow::handle_toplevel_close(void *data, struct xdg_toplevel * /* xdg_toplevel */) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| |
| Event event{}; |
| event.Type = Event::EVENT_CLOSED; |
| w->pushEvent(std::move(event)); |
| } |
| |
| const struct xdg_toplevel_listener WaylandWindow::xdg_toplevel_listener = { |
| .configure = handle_toplevel_configure, |
| .close = handle_toplevel_close, |
| .configure_bounds = nullptr, |
| .wm_capabilities = nullptr, |
| }; |
| |
| void WaylandWindow::decoration_configure(void * /*data*/, |
| struct zxdg_toplevel_decoration_v1 * /*decoration*/, |
| uint32_t mode) |
| { |
| // We only ever request SERVER_SIDE. A CLIENT_SIDE response leaves the |
| // window decorationless (we don't draw our own chrome) -- log so the |
| // missing decorations are debuggable rather than mysterious. |
| if (mode != ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) |
| { |
| fprintf(stderr, |
| "WaylandWindow: compositor refused SERVER_SIDE decorations " |
| "(mode=%u); window will be undecorated.\n", |
| mode); |
| } |
| } |
| |
| const struct zxdg_toplevel_decoration_v1_listener WaylandWindow::decoration_listener = { |
| .configure = decoration_configure, |
| }; |
| |
| // --- Input ---------------------------------------------------------------- |
| // |
| // Wayland delivers key events as raw Linux input-event-codes (KEY_*) and |
| // mouse button events as raw codes (BTN_*). We translate directly to ANGLE's |
| // KeyType / MouseButtonType enums and intentionally skip xkbcommon-based |
| // keymap translation: this code path is for key identification, not text |
| // entry. EVENT_TEXT_ENTERED is unimplemented (matches the X11 backend). |
| |
| namespace |
| { |
| angle::KeyType LinuxKeyCodeToKey(uint32_t code) |
| { |
| switch (code) |
| { |
| case KEY_ESC: |
| return angle::KeyType::ESCAPE; |
| case KEY_1: |
| return angle::KeyType::NUM1; |
| case KEY_2: |
| return angle::KeyType::NUM2; |
| case KEY_3: |
| return angle::KeyType::NUM3; |
| case KEY_4: |
| return angle::KeyType::NUM4; |
| case KEY_5: |
| return angle::KeyType::NUM5; |
| case KEY_6: |
| return angle::KeyType::NUM6; |
| case KEY_7: |
| return angle::KeyType::NUM7; |
| case KEY_8: |
| return angle::KeyType::NUM8; |
| case KEY_9: |
| return angle::KeyType::NUM9; |
| case KEY_0: |
| return angle::KeyType::NUM0; |
| case KEY_MINUS: |
| return angle::KeyType::DASH; |
| case KEY_EQUAL: |
| return angle::KeyType::EQUAL; |
| case KEY_BACKSPACE: |
| return angle::KeyType::BACK; |
| case KEY_TAB: |
| return angle::KeyType::TAB; |
| case KEY_Q: |
| return angle::KeyType::Q; |
| case KEY_W: |
| return angle::KeyType::W; |
| case KEY_E: |
| return angle::KeyType::E; |
| case KEY_R: |
| return angle::KeyType::R; |
| case KEY_T: |
| return angle::KeyType::T; |
| case KEY_Y: |
| return angle::KeyType::Y; |
| case KEY_U: |
| return angle::KeyType::U; |
| case KEY_I: |
| return angle::KeyType::I; |
| case KEY_O: |
| return angle::KeyType::O; |
| case KEY_P: |
| return angle::KeyType::P; |
| case KEY_LEFTBRACE: |
| return angle::KeyType::LBRACKET; |
| case KEY_RIGHTBRACE: |
| return angle::KeyType::RBRACKET; |
| case KEY_ENTER: |
| return angle::KeyType::RETURN; |
| case KEY_LEFTCTRL: |
| return angle::KeyType::LCONTROL; |
| case KEY_A: |
| return angle::KeyType::A; |
| case KEY_S: |
| return angle::KeyType::S; |
| case KEY_D: |
| return angle::KeyType::D; |
| case KEY_F: |
| return angle::KeyType::F; |
| case KEY_G: |
| return angle::KeyType::G; |
| case KEY_H: |
| return angle::KeyType::H; |
| case KEY_J: |
| return angle::KeyType::J; |
| case KEY_K: |
| return angle::KeyType::K; |
| case KEY_L: |
| return angle::KeyType::L; |
| case KEY_SEMICOLON: |
| return angle::KeyType::SEMICOLON; |
| case KEY_APOSTROPHE: |
| return angle::KeyType::QUOTE; |
| case KEY_GRAVE: |
| return angle::KeyType::TILDE; |
| case KEY_LEFTSHIFT: |
| return angle::KeyType::LSHIFT; |
| case KEY_BACKSLASH: |
| return angle::KeyType::BACKSLASH; |
| case KEY_Z: |
| return angle::KeyType::Z; |
| case KEY_X: |
| return angle::KeyType::X; |
| case KEY_C: |
| return angle::KeyType::C; |
| case KEY_V: |
| return angle::KeyType::V; |
| case KEY_B: |
| return angle::KeyType::B; |
| case KEY_N: |
| return angle::KeyType::N; |
| case KEY_M: |
| return angle::KeyType::M; |
| case KEY_COMMA: |
| return angle::KeyType::COMMA; |
| case KEY_DOT: |
| return angle::KeyType::PERIOD; |
| case KEY_SLASH: |
| return angle::KeyType::SLASH; |
| case KEY_RIGHTSHIFT: |
| return angle::KeyType::RSHIFT; |
| case KEY_KPASTERISK: |
| return angle::KeyType::MULTIPLY; |
| case KEY_LEFTALT: |
| return angle::KeyType::LALT; |
| case KEY_SPACE: |
| return angle::KeyType::SPACE; |
| case KEY_F1: |
| return angle::KeyType::F1; |
| case KEY_F2: |
| return angle::KeyType::F2; |
| case KEY_F3: |
| return angle::KeyType::F3; |
| case KEY_F4: |
| return angle::KeyType::F4; |
| case KEY_F5: |
| return angle::KeyType::F5; |
| case KEY_F6: |
| return angle::KeyType::F6; |
| case KEY_F7: |
| return angle::KeyType::F7; |
| case KEY_F8: |
| return angle::KeyType::F8; |
| case KEY_F9: |
| return angle::KeyType::F9; |
| case KEY_F10: |
| return angle::KeyType::F10; |
| case KEY_KP7: |
| return angle::KeyType::NUMPAD7; |
| case KEY_KP8: |
| return angle::KeyType::NUMPAD8; |
| case KEY_KP9: |
| return angle::KeyType::NUMPAD9; |
| case KEY_KPMINUS: |
| return angle::KeyType::SUBTRACT; |
| case KEY_KP4: |
| return angle::KeyType::NUMPAD4; |
| case KEY_KP5: |
| return angle::KeyType::NUMPAD5; |
| case KEY_KP6: |
| return angle::KeyType::NUMPAD6; |
| case KEY_KPPLUS: |
| return angle::KeyType::ADD; |
| case KEY_KP1: |
| return angle::KeyType::NUMPAD1; |
| case KEY_KP2: |
| return angle::KeyType::NUMPAD2; |
| case KEY_KP3: |
| return angle::KeyType::NUMPAD3; |
| case KEY_KP0: |
| return angle::KeyType::NUMPAD0; |
| case KEY_F11: |
| return angle::KeyType::F11; |
| case KEY_F12: |
| return angle::KeyType::F12; |
| case KEY_F13: |
| return angle::KeyType::F13; |
| case KEY_F14: |
| return angle::KeyType::F14; |
| case KEY_F15: |
| return angle::KeyType::F15; |
| case KEY_KPSLASH: |
| return angle::KeyType::DIVIDE; |
| case KEY_RIGHTALT: |
| return angle::KeyType::RALT; |
| case KEY_HOME: |
| return angle::KeyType::HOME; |
| case KEY_UP: |
| return angle::KeyType::UP; |
| case KEY_PAGEUP: |
| return angle::KeyType::PAGEUP; |
| case KEY_LEFT: |
| return angle::KeyType::LEFT; |
| case KEY_RIGHT: |
| return angle::KeyType::RIGHT; |
| case KEY_END: |
| return angle::KeyType::END; |
| case KEY_DOWN: |
| return angle::KeyType::DOWN; |
| case KEY_PAGEDOWN: |
| return angle::KeyType::PAGEDOWN; |
| case KEY_INSERT: |
| return angle::KeyType::INSERT; |
| case KEY_DELETE: |
| return angle::KeyType::DEL; |
| case KEY_PAUSE: |
| return angle::KeyType::PAUSE; |
| case KEY_LEFTMETA: |
| return angle::KeyType::LSYSTEM; |
| case KEY_RIGHTMETA: |
| return angle::KeyType::RSYSTEM; |
| case KEY_RIGHTCTRL: |
| return angle::KeyType::RCONTROL; |
| case KEY_MENU: |
| return angle::KeyType::MENU; |
| default: |
| return angle::KeyType(0); |
| } |
| } |
| |
| angle::MouseButtonType LinuxButtonToMouseButton(uint32_t button) |
| { |
| switch (button) |
| { |
| case BTN_LEFT: |
| return angle::MouseButtonType::LEFT; |
| case BTN_RIGHT: |
| return angle::MouseButtonType::RIGHT; |
| case BTN_MIDDLE: |
| return angle::MouseButtonType::MIDDLE; |
| case BTN_SIDE: |
| return angle::MouseButtonType::BUTTON4; |
| case BTN_EXTRA: |
| return angle::MouseButtonType::BUTTON5; |
| default: |
| return angle::MouseButtonType::UNKNOWN; |
| } |
| } |
| } // namespace |
| |
| void WaylandWindow::seat_capabilities(void *data, struct wl_seat *seat, uint32_t caps) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| |
| const bool wantKeyboard = (caps & WL_SEAT_CAPABILITY_KEYBOARD) != 0; |
| if (wantKeyboard && !w->mKeyboard) |
| { |
| w->mKeyboard = wl_seat_get_keyboard(seat); |
| wl_keyboard_add_listener(w->mKeyboard, &keyboard_listener, w); |
| } |
| else if (!wantKeyboard && w->mKeyboard) |
| { |
| wl_keyboard_release(w->mKeyboard); |
| w->mKeyboard = nullptr; |
| } |
| |
| const bool wantPointer = (caps & WL_SEAT_CAPABILITY_POINTER) != 0; |
| if (wantPointer && !w->mPointer) |
| { |
| w->mPointer = wl_seat_get_pointer(seat); |
| wl_pointer_add_listener(w->mPointer, &pointer_listener, w); |
| } |
| else if (!wantPointer && w->mPointer) |
| { |
| wl_pointer_release(w->mPointer); |
| w->mPointer = nullptr; |
| } |
| } |
| |
| void WaylandWindow::seat_name(void * /*data*/, struct wl_seat * /*seat*/, const char * /*name*/) {} |
| |
| const struct wl_seat_listener WaylandWindow::seat_listener = { |
| .capabilities = seat_capabilities, |
| .name = seat_name, |
| }; |
| |
| void WaylandWindow::keyboard_keymap(void * /*data*/, |
| struct wl_keyboard * /*keyboard*/, |
| uint32_t /*format*/, |
| int32_t fd, |
| uint32_t /*size*/) |
| { |
| // We don't translate via xkbcommon; close the fd the compositor mmap'd for |
| // us to keep the file table tidy (libwayland-client always sends one). |
| close(fd); |
| } |
| |
| void WaylandWindow::keyboard_enter(void * /*data*/, |
| struct wl_keyboard * /*keyboard*/, |
| uint32_t /*serial*/, |
| struct wl_surface * /*surface*/, |
| struct wl_array * /*keys*/) |
| { |
| // Focus events come from xdg_toplevel.state.activated (see |
| // handle_toplevel_configure), not from keyboard enter/leave. The |
| // activated bit is the WM-level focus signal and works even when the |
| // compositor doesn't advertise a keyboard capability. |
| } |
| |
| void WaylandWindow::keyboard_leave(void *data, |
| struct wl_keyboard * /*keyboard*/, |
| uint32_t /*serial*/, |
| struct wl_surface * /*surface*/) |
| { |
| // Stop any in-flight key repeat -- the user can no longer release the held |
| // key for this surface, so repeats would fire forever. (No focus event is |
| // pushed here; see keyboard_enter comment.) |
| auto *w = static_cast<WaylandWindow *>(data); |
| if (w->mRepeatTimerFd >= 0 && w->mRepeatKey != 0) |
| { |
| struct itimerspec ts{}; |
| timerfd_settime(w->mRepeatTimerFd, 0, &ts, nullptr); |
| w->mRepeatKey = 0; |
| } |
| } |
| |
| void WaylandWindow::keyboard_key(void *data, |
| struct wl_keyboard * /*keyboard*/, |
| uint32_t /*serial*/, |
| uint32_t /*time*/, |
| uint32_t key, |
| uint32_t state) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| |
| Event event{}; |
| event.Type = (state == WL_KEYBOARD_KEY_STATE_PRESSED) ? Event::EVENT_KEY_PRESSED |
| : Event::EVENT_KEY_RELEASED; |
| event.Key.Code = LinuxKeyCodeToKey(key); |
| event.Key.Shift = w->mShiftDown; |
| event.Key.Control = w->mControlDown; |
| event.Key.Alt = w->mAltDown; |
| event.Key.System = w->mSuperDown; |
| w->pushEvent(std::move(event)); |
| |
| // Drive key auto-repeat ourselves to match X11/Win32 semantics -- Wayland |
| // does not generate repeat events. Only non-modifier keys repeat (matches |
| // libinput's default behavior and avoids spamming Shift/Ctrl events). |
| if (w->mRepeatTimerFd < 0) |
| { |
| return; |
| } |
| const bool isModifier = (key == KEY_LEFTSHIFT || key == KEY_RIGHTSHIFT || key == KEY_LEFTCTRL || |
| key == KEY_RIGHTCTRL || key == KEY_LEFTALT || key == KEY_RIGHTALT || |
| key == KEY_LEFTMETA || key == KEY_RIGHTMETA); |
| struct itimerspec ts{}; |
| if (state == WL_KEYBOARD_KEY_STATE_PRESSED && !isModifier && w->mRepeatRate > 0) |
| { |
| w->mRepeatKey = key; |
| const int64_t intervalNs = 1'000'000'000LL / w->mRepeatRate; |
| ts.it_value.tv_sec = w->mRepeatDelay / 1000; |
| ts.it_value.tv_nsec = (w->mRepeatDelay % 1000) * 1'000'000; |
| ts.it_interval.tv_sec = static_cast<time_t>(intervalNs / 1'000'000'000LL); |
| ts.it_interval.tv_nsec = static_cast<long>(intervalNs % 1'000'000'000LL); |
| timerfd_settime(w->mRepeatTimerFd, 0, &ts, nullptr); |
| } |
| else if (state == WL_KEYBOARD_KEY_STATE_RELEASED && key == w->mRepeatKey) |
| { |
| // Disarm only on release of the key we were repeating. Releasing some |
| // other key (e.g., Shift while W is still held) leaves W repeating. |
| timerfd_settime(w->mRepeatTimerFd, 0, &ts, nullptr); |
| w->mRepeatKey = 0; |
| } |
| } |
| |
| void WaylandWindow::keyboard_modifiers(void *data, |
| struct wl_keyboard * /*keyboard*/, |
| uint32_t /*serial*/, |
| uint32_t mods_depressed, |
| uint32_t /*mods_latched*/, |
| uint32_t /*mods_locked*/, |
| uint32_t /*group*/) |
| { |
| // wl_keyboard.modifiers reports the active xkb modifier mask. Without a |
| // keymap we don't know which xkb bit is Shift, but compositors using the |
| // default libxkbcommon layout set bit 0=Shift, 2=Control, 3=Alt (Mod1), |
| // 6=Super (Mod4) -- sufficient for non-text-entry key dispatch. |
| auto *w = static_cast<WaylandWindow *>(data); |
| w->mShiftDown = (mods_depressed & (1u << 0)) != 0; |
| w->mControlDown = (mods_depressed & (1u << 2)) != 0; |
| w->mAltDown = (mods_depressed & (1u << 3)) != 0; |
| w->mSuperDown = (mods_depressed & (1u << 6)) != 0; |
| } |
| |
| void WaylandWindow::keyboard_repeat_info(void *data, |
| struct wl_keyboard * /*keyboard*/, |
| int32_t rate, |
| int32_t delay) |
| { |
| // rate is chars/sec; rate == 0 means user disabled key repeat in their |
| // compositor settings. delay is ms before the first repeat fires. |
| auto *w = static_cast<WaylandWindow *>(data); |
| w->mRepeatRate = rate; |
| w->mRepeatDelay = delay; |
| } |
| |
| const struct wl_keyboard_listener WaylandWindow::keyboard_listener = { |
| .keymap = keyboard_keymap, |
| .enter = keyboard_enter, |
| .leave = keyboard_leave, |
| .key = keyboard_key, |
| .modifiers = keyboard_modifiers, |
| .repeat_info = keyboard_repeat_info, |
| }; |
| |
| void WaylandWindow::pointer_enter(void *data, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*serial*/, |
| struct wl_surface * /*surface*/, |
| wl_fixed_t x, |
| wl_fixed_t y) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| w->mPointerX = wl_fixed_to_int(x); |
| w->mPointerY = wl_fixed_to_int(y); |
| Event event{}; |
| event.Type = Event::EVENT_MOUSE_ENTERED; |
| w->pushEvent(std::move(event)); |
| } |
| |
| void WaylandWindow::pointer_leave(void *data, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*serial*/, |
| struct wl_surface * /*surface*/) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| Event event{}; |
| event.Type = Event::EVENT_MOUSE_LEFT; |
| w->pushEvent(std::move(event)); |
| } |
| |
| void WaylandWindow::pointer_motion(void *data, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*time*/, |
| wl_fixed_t x, |
| wl_fixed_t y) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| w->mPointerX = wl_fixed_to_int(x); |
| w->mPointerY = wl_fixed_to_int(y); |
| Event event{}; |
| event.Type = Event::EVENT_MOUSE_MOVED; |
| event.MouseMove.X = w->mPointerX; |
| event.MouseMove.Y = w->mPointerY; |
| w->pushEvent(std::move(event)); |
| } |
| |
| void WaylandWindow::pointer_button(void *data, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*serial*/, |
| uint32_t /*time*/, |
| uint32_t button, |
| uint32_t state) |
| { |
| auto *w = static_cast<WaylandWindow *>(data); |
| Event event{}; |
| event.Type = (state == WL_POINTER_BUTTON_STATE_PRESSED) ? Event::EVENT_MOUSE_BUTTON_PRESSED |
| : Event::EVENT_MOUSE_BUTTON_RELEASED; |
| event.MouseButton.Button = LinuxButtonToMouseButton(button); |
| event.MouseButton.X = w->mPointerX; |
| event.MouseButton.Y = w->mPointerY; |
| w->pushEvent(std::move(event)); |
| } |
| |
| void WaylandWindow::pointer_axis(void *data, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*time*/, |
| uint32_t axis, |
| wl_fixed_t value) |
| { |
| // Compositors typically send 10.0 per discrete vertical wheel notch. |
| // Normalize to +/-1 to match X11's Button4/5 and Win32's WHEEL_DELTA |
| // semantics. Horizontal axis is ignored -- Event::MouseWheelEvent has no |
| // horizontal field. |
| if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) |
| { |
| return; |
| } |
| auto *w = static_cast<WaylandWindow *>(data); |
| const double v = wl_fixed_to_double(value); |
| if (v == 0.0) |
| { |
| return; |
| } |
| Event event{}; |
| event.Type = Event::EVENT_MOUSE_WHEEL_MOVED; |
| event.MouseWheel.Delta = (v < 0.0) ? 1 : -1; // wayland: positive = scroll down |
| w->pushEvent(std::move(event)); |
| } |
| |
| void WaylandWindow::pointer_frame(void * /*data*/, struct wl_pointer * /*pointer*/) {} |
| void WaylandWindow::pointer_axis_source(void * /*data*/, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*source*/) |
| {} |
| void WaylandWindow::pointer_axis_stop(void * /*data*/, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*time*/, |
| uint32_t /*axis*/) |
| {} |
| void WaylandWindow::pointer_axis_discrete(void * /*data*/, |
| struct wl_pointer * /*pointer*/, |
| uint32_t /*axis*/, |
| int32_t /*discrete*/) |
| {} |
| |
| const struct wl_pointer_listener WaylandWindow::pointer_listener = { |
| .enter = pointer_enter, |
| .leave = pointer_leave, |
| .motion = pointer_motion, |
| .button = pointer_button, |
| .axis = pointer_axis, |
| .frame = pointer_frame, |
| .axis_source = pointer_axis_source, |
| .axis_stop = pointer_axis_stop, |
| .axis_discrete = pointer_axis_discrete, |
| // v8+ events. We bind seat at v5, so the compositor will never send these, |
| // but the listener struct requires every field. |
| .axis_value120 = nullptr, |
| .axis_relative_direction = nullptr, |
| #ifdef WL_POINTER_WARP_SINCE_VERSION |
| .warp = nullptr, |
| #endif |
| }; |
| |
| bool IsWaylandWindowAvailable() |
| { |
| if (!gDisplay) |
| { |
| wl_display *display = wl_display_connect(nullptr); |
| if (!display) |
| { |
| return false; |
| } |
| wl_display_flush(display); |
| wl_display_disconnect(display); |
| } |
| return true; |
| } |
| |
| OSWindow *CreateWaylandWindow(void *nativeDisplay) |
| { |
| return new WaylandWindow(nativeDisplay); |
| } |