summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIkalco <73481042+ikalco@users.noreply.github.com>2025-08-29 15:16:40 -0500
committerGitHub <noreply@github.com>2025-08-29 22:16:40 +0200
commitea42041f936d5810c5cfa45d6bece12dde2fd9b6 (patch)
treed600f5a6a4fcdee8f3e24b666d1ea57ccc88cdd6
parent05a1c0aa7395d19213e587c83089ecbd7b92085c (diff)
protocols: implement pointer-warp-v1 (#11469)
-rw-r--r--CMakeLists.txt3
-rw-r--r--hyprtester/CMakeLists.txt70
-rw-r--r--hyprtester/clients/pointer-warp.cpp317
-rw-r--r--hyprtester/protocols/.gitignore2
-rw-r--r--hyprtester/src/main.cpp9
-rw-r--r--hyprtester/src/tests/clients/.gitignore1
-rw-r--r--hyprtester/src/tests/clients/pointer-warp.cpp180
-rw-r--r--hyprtester/src/tests/clients/tests.hpp12
-rw-r--r--nix/hyprtester.nix8
-rw-r--r--protocols/meson.build3
-rw-r--r--src/managers/ProtocolManager.cpp3
-rw-r--r--src/managers/SeatManager.cpp5
-rw-r--r--src/managers/SeatManager.hpp2
-rw-r--r--src/protocols/PointerWarp.cpp53
-rw-r--r--src/protocols/PointerWarp.hpp21
-rw-r--r--src/protocols/core/Seat.cpp9
-rw-r--r--src/protocols/core/Seat.hpp6
17 files changed, 699 insertions, 5 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 643f7dc6..b98e7e37 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -132,7 +132,7 @@ pkg_check_modules(
xkbcommon
uuid
wayland-server>=1.22.90
- wayland-protocols>=1.43
+ wayland-protocols>=1.45
cairo
pango
pangocairo
@@ -383,6 +383,7 @@ protocolnew("staging/xdg-toplevel-tag" "xdg-toplevel-tag-v1" false)
protocolnew("staging/xdg-system-bell" "xdg-system-bell-v1" false)
protocolnew("staging/ext-workspace" "ext-workspace-v1" false)
protocolnew("staging/ext-data-control" "ext-data-control-v1" false)
+protocolnew("staging/pointer-warp" "pointer-warp-v1" false)
protocolwayland()
diff --git a/hyprtester/CMakeLists.txt b/hyprtester/CMakeLists.txt
index 10907ce7..34e62603 100644
--- a/hyprtester/CMakeLists.txt
+++ b/hyprtester/CMakeLists.txt
@@ -5,6 +5,7 @@ project(hyprtester DESCRIPTION "Hyprland test suite")
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 26)
+set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
find_package(PkgConfig REQUIRED)
@@ -28,3 +29,72 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/test.conf
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugin/hyprtestplugin.so
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib)
+
+file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/src/tests/clients/build.hpp
+ "#include <string>\n"
+ "static const std::string binaryDir = \"${CMAKE_CURRENT_BINARY_DIR}\";"
+)
+
+######## wayland protocols testing stuff
+
+if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
+ set(CMAKE_EXECUTABLE_ENABLE_EXPORTS TRUE)
+endif()
+
+find_package(hyprwayland-scanner 0.4.0 REQUIRED)
+pkg_check_modules(
+ protocols_deps
+ REQUIRED
+ IMPORTED_TARGET
+ hyprutils>=0.8.0
+ wayland-client
+ wayland-protocols
+)
+
+pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
+message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}")
+pkg_get_variable(WAYLAND_SCANNER_PKGDATA_DIR wayland-scanner pkgdatadir)
+message(STATUS "Found wayland-scanner pkgdatadir at ${WAYLAND_SCANNER_PKGDATA_DIR}")
+
+# gen core wayland stuff
+add_custom_command(
+ OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.hpp
+ COMMAND hyprwayland-scanner --wayland-enums --client
+ ${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_CURRENT_SOURCE_DIR}/protocols/
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+function(protocolNew protoPath protoName external)
+ if(external)
+ set(path ${CMAKE_CURRENT_SOURCE_DIR}/${protoPath})
+ else()
+ set(path ${WAYLAND_PROTOCOLS_DIR}/${protoPath})
+ endif()
+
+ add_custom_command(
+ OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.hpp
+ COMMAND hyprwayland-scanner --client ${path}/${protoName}.xml
+ ${CMAKE_CURRENT_SOURCE_DIR}/protocols/
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+endfunction()
+function(clientNew sourceName)
+ cmake_parse_arguments(PARSE_ARGV 1 ARG "" "" "PROTOS")
+
+ add_executable(${sourceName} clients/${sourceName}.cpp)
+
+ target_include_directories(${sourceName} BEFORE PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
+ target_link_libraries(${sourceName} PUBLIC PkgConfig::protocols_deps)
+
+ target_sources(${sourceName} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.hpp)
+
+ foreach(protoName IN LISTS ARG_PROTOS)
+ target_sources(${sourceName} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.hpp)
+ endforeach()
+endfunction()
+
+protocolnew("staging/pointer-warp" "pointer-warp-v1" false)
+protocolnew("stable/xdg-shell" "xdg-shell" false)
+
+clientNew("pointer-warp" PROTOS "pointer-warp-v1" "xdg-shell")
diff --git a/hyprtester/clients/pointer-warp.cpp b/hyprtester/clients/pointer-warp.cpp
new file mode 100644
index 00000000..2d3624d5
--- /dev/null
+++ b/hyprtester/clients/pointer-warp.cpp
@@ -0,0 +1,317 @@
+#include <cstring>
+#include <sys/poll.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <print>
+#include <format>
+#include <string>
+
+#include <wayland-client.h>
+#include <wayland.hpp>
+#include <xdg-shell.hpp>
+#include <pointer-warp-v1.hpp>
+
+#include <hyprutils/memory/SharedPtr.hpp>
+#include <hyprutils/math/Vector2D.hpp>
+#include <hyprutils/os/FileDescriptor.hpp>
+
+using Hyprutils::Math::Vector2D;
+using namespace Hyprutils::Memory;
+
+struct SWlState {
+ wl_display* display;
+ CSharedPointer<CCWlRegistry> registry;
+
+ // protocols
+ CSharedPointer<CCWlCompositor> wlCompositor;
+ CSharedPointer<CCWlSeat> wlSeat;
+ CSharedPointer<CCWlShm> wlShm;
+ CSharedPointer<CCXdgWmBase> xdgShell;
+ CSharedPointer<CCWpPointerWarpV1> pointerWarp;
+
+ // shm/buffer stuff
+ CSharedPointer<CCWlShmPool> shmPool;
+ CSharedPointer<CCWlBuffer> shmBuf;
+ int shmFd;
+ size_t shmBufSize;
+ bool xrgb8888_support = false;
+
+ // surface/toplevel stuff
+ CSharedPointer<CCWlSurface> surf;
+ CSharedPointer<CCXdgSurface> xdgSurf;
+ CSharedPointer<CCXdgToplevel> xdgToplevel;
+ Vector2D geom;
+
+ // pointer
+ CSharedPointer<CCWlPointer> pointer;
+ uint32_t enterSerial;
+};
+
+static bool debug, started, shouldExit;
+
+template <typename... Args>
+//NOLINTNEXTLINE
+static void clientLog(std::format_string<Args...> fmt, Args&&... args) {
+ std::println("{}", std::vformat(fmt.get(), std::make_format_args(args...)));
+ std::fflush(stdout);
+}
+
+template <typename... Args>
+//NOLINTNEXTLINE
+static void debugLog(std::format_string<Args...> fmt, Args&&... args) {
+ if (!debug)
+ return;
+ std::println("{}", std::vformat(fmt.get(), std::make_format_args(args...)));
+ std::fflush(stdout);
+}
+
+static bool bindRegistry(SWlState& state) {
+ state.registry = makeShared<CCWlRegistry>((wl_proxy*)wl_display_get_registry(state.display));
+
+ state.registry->setGlobal([&](CCWlRegistry* r, uint32_t id, const char* name, uint32_t version) {
+ const std::string NAME = name;
+ if (NAME == "wl_compositor") {
+ debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
+ state.wlCompositor = makeShared<CCWlCompositor>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_compositor_interface, 6));
+ } else if (NAME == "wl_shm") {
+ debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
+ state.wlShm = makeShared<CCWlShm>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_shm_interface, 1));
+ } else if (NAME == "wl_seat") {
+ debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
+ state.wlSeat = makeShared<CCWlSeat>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_seat_interface, 9));
+ } else if (NAME == "xdg_wm_base") {
+ debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
+ state.xdgShell = makeShared<CCXdgWmBase>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &xdg_wm_base_interface, 1));
+ } else if (NAME == "wp_pointer_warp_v1") {
+ debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
+ state.pointerWarp = makeShared<CCWpPointerWarpV1>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wp_pointer_warp_v1_interface, 1));
+ }
+ });
+ state.registry->setGlobalRemove([](CCWlRegistry* r, uint32_t id) { debugLog("Global {} removed", id); });
+
+ wl_display_roundtrip(state.display);
+
+ if (!state.wlCompositor || !state.wlShm || !state.wlSeat || !state.xdgShell || !state.pointerWarp) {
+ clientLog("Failed to get protocols from Hyprland");
+ return false;
+ }
+
+ return true;
+}
+
+static bool createShm(SWlState& state, Vector2D geom) {
+ if (!state.xrgb8888_support)
+ return false;
+
+ size_t stride = geom.x * 4;
+ size_t size = geom.y * stride;
+ if (!state.shmPool) {
+ const char* name = "/wl-shm-pointer-warp";
+ state.shmFd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (state.shmFd < 0)
+ return false;
+
+ if (shm_unlink(name) < 0 || ftruncate(state.shmFd, size) < 0) {
+ close(state.shmFd);
+ return false;
+ }
+
+ state.shmPool = makeShared<CCWlShmPool>(state.wlShm->sendCreatePool(state.shmFd, size));
+ if (!state.shmPool->resource()) {
+ close(state.shmFd);
+ state.shmFd = -1;
+ state.shmPool.reset();
+ return false;
+ }
+ state.shmBufSize = size;
+ } else if (size > state.shmBufSize) {
+ if (ftruncate(state.shmFd, size) < 0) {
+ close(state.shmFd);
+ state.shmFd = -1;
+ state.shmPool.reset();
+ return false;
+ }
+
+ state.shmPool->sendResize(size);
+ state.shmBufSize = size;
+ }
+
+ auto buf = makeShared<CCWlBuffer>(state.shmPool->sendCreateBuffer(0, geom.x, geom.y, stride, WL_SHM_FORMAT_XRGB8888));
+ if (!buf->resource())
+ return false;
+
+ if (state.shmBuf) {
+ state.shmBuf->sendDestroy();
+ state.shmBuf.reset();
+ }
+
+ state.shmBuf = buf;
+
+ return true;
+}
+
+static bool setupToplevel(SWlState& state) {
+ state.wlShm->setFormat([&](CCWlShm* p, uint32_t format) {
+ if (format == WL_SHM_FORMAT_XRGB8888)
+ state.xrgb8888_support = true;
+ });
+
+ state.xdgShell->setPing([&](CCXdgWmBase* p, uint32_t serial) { state.xdgShell->sendPong(serial); });
+
+ state.surf = makeShared<CCWlSurface>(state.wlCompositor->sendCreateSurface());
+ if (!state.surf->resource())
+ return false;
+
+ state.xdgSurf = makeShared<CCXdgSurface>(state.xdgShell->sendGetXdgSurface(state.surf->resource()));
+ if (!state.xdgSurf->resource())
+ return false;
+
+ state.xdgToplevel = makeShared<CCXdgToplevel>(state.xdgSurf->sendGetToplevel());
+ if (!state.xdgToplevel->resource())
+ return false;
+
+ state.xdgToplevel->setClose([&](CCXdgToplevel* p) { exit(0); });
+
+ state.xdgToplevel->setConfigure([&](CCXdgToplevel* p, int32_t w, int32_t h, wl_array* arr) {
+ state.geom = {1280, 720};
+
+ if (!createShm(state, state.geom))
+ exit(-1);
+ });
+
+ state.xdgSurf->setConfigure([&](CCXdgSurface* p, uint32_t serial) {
+ if (!state.shmBuf)
+ debugLog("xdgSurf configure but no buf made yet?");
+
+ state.xdgSurf->sendSetWindowGeometry(0, 0, state.geom.x, state.geom.y);
+ state.surf->sendAttach(state.shmBuf.get(), 0, 0);
+ state.surf->sendCommit();
+
+ state.xdgSurf->sendAckConfigure(serial);
+
+ if (!started) {
+ started = true;
+ clientLog("started");
+ }
+ });
+
+ state.xdgToplevel->sendSetTitle("pointer-warp test client");
+ state.xdgToplevel->sendSetAppId("pointer-warp");
+
+ state.surf->sendAttach(nullptr, 0, 0);
+ state.surf->sendCommit();
+
+ return true;
+}
+
+static bool setupSeat(SWlState& state) {
+ state.pointer = makeShared<CCWlPointer>(state.wlSeat->sendGetPointer());
+ if (!state.pointer->resource())
+ return false;
+
+ state.pointer->setEnter([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf, wl_fixed_t x, wl_fixed_t y) {
+ debugLog("Got pointer enter event, serial {}, x {}, y {}", serial, x, y);
+ state.enterSerial = serial;
+ });
+
+ state.pointer->setLeave([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf) { debugLog("Got pointer leave event, serial {}", serial); });
+
+ state.pointer->setMotion([&](CCWlPointer* p, uint32_t serial, wl_fixed_t x, wl_fixed_t y) { debugLog("Got pointer motion event, serial {}, x {}, y {}", serial, x, y); });
+
+ return true;
+}
+
+// format is like below
+// "warp 20 20\n" would ask to warp cursor to x=20,y=20 in surface local coords
+static void parseRequest(SWlState& state, std::string req) {
+ if (req.contains("exit")) {
+ shouldExit = true;
+ return;
+ }
+
+ if (!req.starts_with("warp "))
+ return;
+
+ auto it = req.find_first_of('\n');
+ if (it == std::string::npos)
+ return;
+
+ req = req.substr(0, it);
+
+ it = req.find_first_of(' ');
+ if (it == std::string::npos)
+ return;
+
+ req = req.substr(it + 1);
+
+ it = req.find_first_of(' ');
+
+ int x = std::stoi(req.substr(0, it));
+ int y = std::stoi(req.substr(it + 1));
+
+ state.pointerWarp->sendWarpPointer(state.surf->resource(), state.pointer->resource(), wl_fixed_from_int(x), wl_fixed_from_int(y), state.enterSerial);
+
+ // sync the request then reply
+ wl_display_roundtrip(state.display);
+
+ clientLog("parsed request to move to x:{}, y:{}", x, y);
+}
+
+int main(int argc, char** argv) {
+ if (argc != 1 && argc != 2)
+ clientLog("Only the \"--debug\" switch is allowed, it turns on debug logs.");
+
+ if (argc == 2 && std::string{argv[1]} == "--debug")
+ debug = true;
+
+ SWlState state;
+
+ // WAYLAND_DISPLAY env should be set to the correct one
+ state.display = wl_display_connect(nullptr);
+ if (!state.display) {
+ clientLog("Failed to connect to wayland display");
+ return -1;
+ }
+
+ if (!bindRegistry(state) || !setupSeat(state) || !setupToplevel(state))
+ return -1;
+
+ std::array<char, 1024> readBuf;
+ readBuf.fill(0);
+
+ wl_display_flush(state.display);
+
+ struct pollfd fds[2] = {{.fd = wl_display_get_fd(state.display), .events = POLLIN | POLLOUT}, {.fd = STDIN_FILENO, .events = POLLIN}};
+ while (!shouldExit && poll(fds, 2, 0) != -1) {
+ if (fds[0].revents & POLLIN) {
+ wl_display_flush(state.display);
+
+ if (wl_display_prepare_read(state.display) == 0) {
+ wl_display_read_events(state.display);
+ wl_display_dispatch_pending(state.display);
+ } else
+ wl_display_dispatch(state.display);
+
+ int ret = 0;
+ do {
+ ret = wl_display_dispatch_pending(state.display);
+ wl_display_flush(state.display);
+ } while (ret > 0);
+ }
+
+ if (fds[1].revents & POLLIN) {
+ ssize_t bytesRead = read(fds[1].fd, readBuf.data(), 1023);
+ if (bytesRead == -1)
+ continue;
+ readBuf[bytesRead] = 0;
+
+ parseRequest(state, std::string{readBuf.data()});
+ }
+ }
+
+ wl_display* display = state.display;
+ state = {};
+
+ wl_display_disconnect(display);
+ return 0;
+}
diff --git a/hyprtester/protocols/.gitignore b/hyprtester/protocols/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/hyprtester/protocols/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/hyprtester/src/main.cpp b/hyprtester/src/main.cpp
index f7f2c198..a1bb7d9a 100644
--- a/hyprtester/src/main.cpp
+++ b/hyprtester/src/main.cpp
@@ -17,6 +17,7 @@
#include "shared.hpp"
#include "hyprctlCompat.hpp"
#include "tests/main/tests.hpp"
+#include "tests/clients/tests.hpp"
#include "tests/plugin/plugin.hpp"
#include <filesystem>
@@ -227,10 +228,18 @@ int main(int argc, char** argv, char** envp) {
NLog::log("{}Loaded plugin", Colors::YELLOW);
+ NLog::log("{}Running main tests", Colors::YELLOW);
+
for (const auto& fn : testFns) {
EXPECT(fn(), true);
}
+ NLog::log("{}Running protocol client tests", Colors::YELLOW);
+
+ for (const auto& fn : clientTestFns) {
+ EXPECT(fn(), true);
+ }
+
NLog::log("{}running plugin test", Colors::YELLOW);
EXPECT(testPlugin(), true);
diff --git a/hyprtester/src/tests/clients/.gitignore b/hyprtester/src/tests/clients/.gitignore
new file mode 100644
index 00000000..77a34c55
--- /dev/null
+++ b/hyprtester/src/tests/clients/.gitignore
@@ -0,0 +1 @@
+build.hpp
diff --git a/hyprtester/src/tests/clients/pointer-warp.cpp b/hyprtester/src/tests/clients/pointer-warp.cpp
new file mode 100644
index 00000000..643da796
--- /dev/null
+++ b/hyprtester/src/tests/clients/pointer-warp.cpp
@@ -0,0 +1,180 @@
+#include "../../shared.hpp"
+#include "../../hyprctlCompat.hpp"
+#include "../shared.hpp"
+#include "tests.hpp"
+#include "build.hpp"
+
+#include <hyprutils/os/FileDescriptor.hpp>
+#include <hyprutils/os/Process.hpp>
+
+#include <sys/poll.h>
+#include <csignal>
+#include <thread>
+
+using namespace Hyprutils::OS;
+using namespace Hyprutils::Memory;
+
+#define SP CSharedPointer
+
+struct SClient {
+ SP<CProcess> proc;
+ std::array<char, 1024> readBuf;
+ CFileDescriptor readFd, writeFd;
+ struct pollfd fds;
+};
+
+static int ret;
+
+static bool startClient(SClient& client) {
+ client.proc = makeShared<CProcess>(binaryDir + "/pointer-warp", std::vector<std::string>{});
+
+ client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
+
+ int pipeFds1[2], pipeFds2[2];
+ if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) {
+ NLog::log("{}Unable to open pipe to client", Colors::RED);
+ return false;
+ }
+
+ client.writeFd = CFileDescriptor(pipeFds1[1]);
+ client.proc->setStdinFD(pipeFds1[0]);
+
+ client.readFd = CFileDescriptor(pipeFds2[0]);
+ client.proc->setStdoutFD(pipeFds2[1]);
+
+ client.proc->runAsync();
+
+ close(pipeFds1[0]);
+ close(pipeFds2[1]);
+
+ client.fds = {.fd = client.readFd.get(), .events = POLLIN};
+ if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN))
+ return false;
+
+ client.readBuf.fill(0);
+ if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1)
+ return false;
+
+ std::string ret = std::string{client.readBuf.data()};
+ if (ret.find("started") == std::string::npos) {
+ NLog::log("{}Failed to start pointer-warp client, read {}", Colors::RED, ret);
+ return false;
+ }
+
+ // wait for window to appear
+ std::this_thread::sleep_for(std::chrono::milliseconds(5000));
+
+ if (getFromSocket(std::format("/dispatch setprop pid:{} noanim 1", client.proc->pid())) != "ok") {
+ NLog::log("{}Failed to disable animations for client window", Colors::RED, ret);
+ return false;
+ }
+
+ if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") {
+ NLog::log("{}Failed to focus pointer-warp client", Colors::RED, ret);
+ return false;
+ }
+
+ NLog::log("{}Started pointer-warp client", Colors::YELLOW);
+
+ return true;
+}
+
+static void stopClient(SClient& client) {
+ std::string cmd = "exit\n";
+ write(client.writeFd.get(), cmd.c_str(), cmd.length());
+
+ kill(client.proc->pid(), SIGKILL);
+ client.proc.reset();
+}
+
+// format is like below
+// "warp 20 20\n" would ask to warp cursor to x=20,y=20 in surface local coords
+static bool sendWarp(SClient& client, int x, int y) {
+ std::string cmd = std::format("warp {} {}\n", x, y);
+ if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length())
+ return false;
+
+ if (poll(&client.fds, 1, 1500) != 1 || !(client.fds.revents & POLLIN))
+ return false;
+ ssize_t bytesRead = read(client.fds.fd, client.readBuf.data(), 1023);
+ if (bytesRead == -1)
+ return false;
+
+ client.readBuf[bytesRead] = 0;
+ std::string recieved = std::string{client.readBuf.data()};
+ recieved.pop_back();
+
+ return true;
+}
+
+static bool isCursorPos(int x, int y) {
+ // TODO: add a better way to do this using test plugin?
+ std::string res = getFromSocket("/cursorpos");
+ if (res == "error") {
+ NLog::log("{}Cursorpos err'd: {}", Colors::RED, res);
+ return false;
+ }
+
+ auto it = res.find_first_of(' ');
+ if (res.at(it - 1) != ',') {
+ NLog::log("{}Cursorpos err'd: {}", Colors::RED, res);
+ return false;
+ }
+
+ int cursorX = std::stoi(res.substr(0, it - 1));
+ int cursorY = std::stoi(res.substr(it + 1));
+
+ // somehow this is always gives 1 less than surfbox->pos()??
+ res = getFromSocket("/activewindow");
+ it = res.find("at: ") + 4;
+ res = res.substr(it, res.find_first_of('\n', it) - it);
+
+ it = res.find_first_of(',');
+ int clientX = cursorX - std::stoi(res.substr(0, it)) + 1;
+ int clientY = cursorY - std::stoi(res.substr(it + 1)) + 1;
+
+ return clientX == x && clientY == y;
+}
+
+static bool test() {
+ SClient client;
+
+ if (!startClient(client))
+ return false;
+
+ EXPECT(sendWarp(client, 100, 100), true);
+ EXPECT(isCursorPos(100, 100), true);
+
+ EXPECT(sendWarp(client, 0, 0), true);
+ EXPECT(isCursorPos(0, 0), true);
+
+ EXPECT(sendWarp(client, 200, 200), true);
+ EXPECT(isCursorPos(200, 200), true);
+
+ EXPECT(sendWarp(client, 100, -100), true);
+ EXPECT(isCursorPos(200, 200), true);
+
+ EXPECT(sendWarp(client, 234, 345), true);
+ EXPECT(isCursorPos(234, 345), true);
+
+ EXPECT(sendWarp(client, -1, -1), true);
+ EXPECT(isCursorPos(234, 345), true);
+
+ EXPECT(sendWarp(client, 1, -1), true);
+ EXPECT(isCursorPos(234, 345), true);
+
+ EXPECT(sendWarp(client, 13, 37), true);
+ EXPECT(isCursorPos(13, 37), true);
+
+ EXPECT(sendWarp(client, -100, 100), true);
+ EXPECT(isCursorPos(13, 37), true);
+
+ EXPECT(sendWarp(client, -1, 1), true);
+ EXPECT(isCursorPos(13, 37), true);
+
+ stopClient(client);
+
+ return true;
+}
+
+REGISTER_CLIENT_TEST_FN(test);
diff --git a/hyprtester/src/tests/clients/tests.hpp b/hyprtester/src/tests/clients/tests.hpp
new file mode 100644
index 00000000..31746fe5
--- /dev/null
+++ b/hyprtester/src/tests/clients/tests.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <vector>
+#include <functional>
+
+inline std::vector<std::function<bool()>> clientTestFns;
+
+#define REGISTER_CLIENT_TEST_FN(fn) \
+ static auto _register_fn = [] { \
+ clientTestFns.emplace_back(fn); \
+ return 1; \
+ }();
diff --git a/nix/hyprtester.nix b/nix/hyprtester.nix
index 042228c7..cf9a9c74 100644
--- a/nix/hyprtester.nix
+++ b/nix/hyprtester.nix
@@ -40,12 +40,20 @@ in
buildInputs = hyprland.buildInputs;
preConfigure = ''
+ substituteInPlace hyprtester/CMakeLists.txt --replace-fail \
+ "\''${CMAKE_CURRENT_BINARY_DIR}" \
+ "${placeholder "out"}/bin"
+
cmake -S . -B .
cmake --build . --target generate-protocol-headers -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
cd hyprtester
'';
+ postInstall = ''
+ install pointer-warp -t $out/bin
+ '';
+
cmakeBuildType = "Debug";
cmakeFlags = [(cmakeBool "TESTS" true)];
diff --git a/protocols/meson.build b/protocols/meson.build
index b52c4349..dc23eaa1 100644
--- a/protocols/meson.build
+++ b/protocols/meson.build
@@ -1,6 +1,6 @@
wayland_protos = dependency(
'wayland-protocols',
- version: '>=1.43',
+ version: '>=1.45',
fallback: 'wayland-protocols',
default_options: ['tests=false'],
)
@@ -77,6 +77,7 @@ protocols = [
wayland_protocol_dir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml',
wayland_protocol_dir / 'staging/ext-workspace/ext-workspace-v1.xml',
wayland_protocol_dir / 'staging/ext-data-control/ext-data-control-v1.xml',
+ wayland_protocol_dir / 'staging/pointer-warp/pointer-warp-v1.xml',
]
wl_protocols = []
diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp
index b70d3b7d..c9690aba 100644
--- a/src/managers/ProtocolManager.cpp
+++ b/src/managers/ProtocolManager.cpp
@@ -64,6 +64,7 @@
#include "../protocols/XDGBell.hpp"
#include "../protocols/ExtWorkspace.hpp"
#include "../protocols/ExtDataDevice.hpp"
+#include "../protocols/PointerWarp.hpp"
#include "../helpers/Monitor.hpp"
#include "../render/Renderer.hpp"
@@ -192,6 +193,7 @@ CProtocolManager::CProtocolManager() {
PROTO::xdgBell = makeUnique<CXDGSystemBellProtocol>(&xdg_system_bell_v1_interface, 1, "XDGBell");
PROTO::extWorkspace = makeUnique<CExtWorkspaceProtocol>(&ext_workspace_manager_v1_interface, 1, "ExtWorkspace");
PROTO::extDataDevice = makeUnique<CExtDataDeviceProtocol>(&ext_data_control_manager_v1_interface, 1, "ExtDataDevice");
+ PROTO::pointerWarp = makeUnique<CPointerWarpProtocol>(&wp_pointer_warp_v1_interface, 1, "PointerWarp");
if (*PENABLECM)
PROTO::colorManagement = makeUnique<CColorManagementProtocol>(&wp_color_manager_v1_interface, 1, "ColorManagement", *PDEBUGCM);
@@ -295,6 +297,7 @@ CProtocolManager::~CProtocolManager() {
PROTO::xdgBell.reset();
PROTO::extWorkspace.reset();
PROTO::extDataDevice.reset();
+ PROTO::pointerWarp.reset();
for (auto& [_, lease] : PROTO::lease) {
lease.reset();
diff --git a/src/managers/SeatManager.cpp b/src/managers/SeatManager.cpp
index bc4f2dfc..15316dc6 100644
--- a/src/managers/SeatManager.cpp
+++ b/src/managers/SeatManager.cpp
@@ -58,7 +58,7 @@ uint32_t CSeatManager::nextSerial(SP<CWLSeatResource> seatResource) {
return serial;
}
-bool CSeatManager::serialValid(SP<CWLSeatResource> seatResource, uint32_t serial) {
+bool CSeatManager::serialValid(SP<CWLSeatResource> seatResource, uint32_t serial, bool erase) {
if (!seatResource)
return false;
@@ -68,7 +68,8 @@ bool CSeatManager::serialValid(SP<CWLSeatResource> seatResource, uint32_t serial
for (auto it = container->serials.begin(); it != container->serials.end(); ++it) {
if (*it == serial) {
- container->serials.erase(it);
+ if (erase)
+ container->serials.erase(it);
return true;
}
}
diff --git a/src/managers/SeatManager.hpp b/src/managers/SeatManager.hpp
index 2d73e087..fe11f930 100644
--- a/src/managers/SeatManager.hpp
+++ b/src/managers/SeatManager.hpp
@@ -76,7 +76,7 @@ class CSeatManager {
uint32_t nextSerial(SP<CWLSeatResource> seatResource);
// pops the serial if it was valid, meaning it is consumed.
- bool serialValid(SP<CWLSeatResource> seatResource, uint32_t serial);
+ bool serialValid(SP<CWLSeatResource> seatResource, uint32_t serial, bool erase = true);
void onSetCursor(SP<CWLSeatResource> seatResource, uint32_t serial, SP<CWLSurfaceResource> surf, const Vector2D& hotspot);
diff --git a/src/protocols/PointerWarp.cpp b/src/protocols/PointerWarp.cpp
new file mode 100644
index 00000000..bde0b913
--- /dev/null
+++ b/src/protocols/PointerWarp.cpp
@@ -0,0 +1,53 @@
+#include "PointerWarp.hpp"
+#include "core/Compositor.hpp"
+#include "core/Seat.hpp"
+#include "../desktop/WLSurface.hpp"
+#include "../managers/SeatManager.hpp"
+#include "../managers/PointerManager.hpp"
+#include "../desktop/Window.hpp"
+
+CPointerWarpProtocol::CPointerWarpProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) {
+ ;
+}
+
+void CPointerWarpProtocol::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) {
+ const auto& RESOURCE = m_managers.emplace_back(makeUnique<CWpPointerWarpV1>(client, ver, id));
+
+ if UNLIKELY (!RESOURCE->resource()) {
+ wl_client_post_no_memory(client);
+ m_managers.pop_back();
+ return;
+ }
+
+ RESOURCE->setOnDestroy([this](CWpPointerWarpV1* pMgr) { destroyManager(pMgr); });
+ RESOURCE->setDestroy([this](CWpPointerWarpV1* pMgr) { destroyManager(pMgr); });
+
+ RESOURCE->setWarpPointer([](CWpPointerWarpV1* pMgr, wl_resource* surface, wl_resource* pointer, wl_fixed_t x, wl_fixed_t y, uint32_t serial) {
+ const auto PSURFACE = CWLSurfaceResource::fromResource(surface);
+ if (g_pSeatManager->m_state.pointerFocus != PSURFACE)
+ return;
+
+ auto SURFBOXV = CWLSurface::fromResource(PSURFACE)->getSurfaceBoxGlobal();
+ if (!SURFBOXV.has_value())
+ return;
+
+ const auto SURFBOX = SURFBOXV->expand(1);
+ const auto LOCALPOS = Vector2D{wl_fixed_to_double(x), wl_fixed_to_double(y)};
+ const auto GLOBALPOS = LOCALPOS + SURFBOX.pos();
+ if (!SURFBOX.containsPoint(GLOBALPOS))
+ return;
+
+ const auto PSEAT = CWLPointerResource::fromResource(pointer)->m_owner.lock();
+ if (!g_pSeatManager->serialValid(PSEAT, serial, false))
+ return;
+
+ LOGM(LOG, "warped pointer to {}", GLOBALPOS);
+
+ g_pPointerManager->warpTo(GLOBALPOS);
+ g_pSeatManager->sendPointerMotion(Time::millis(Time::steadyNow()), LOCALPOS);
+ });
+}
+
+void CPointerWarpProtocol::destroyManager(CWpPointerWarpV1* manager) {
+ std::erase_if(m_managers, [&](const UP<CWpPointerWarpV1>& resource) { return resource.get() == manager; });
+}
diff --git a/src/protocols/PointerWarp.hpp b/src/protocols/PointerWarp.hpp
new file mode 100644
index 00000000..d1ce0062
--- /dev/null
+++ b/src/protocols/PointerWarp.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "WaylandProtocol.hpp"
+#include "pointer-warp-v1.hpp"
+
+class CPointerWarpProtocol : public IWaylandProtocol {
+ public:
+ CPointerWarpProtocol(const wl_interface* iface, const int& ver, const std::string& name);
+
+ virtual void bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id);
+
+ private:
+ void destroyManager(CWpPointerWarpV1* manager);
+
+ //
+ std::vector<UP<CWpPointerWarpV1>> m_managers;
+};
+
+namespace PROTO {
+ inline UP<CPointerWarpProtocol> pointerWarp;
+};
diff --git a/src/protocols/core/Seat.cpp b/src/protocols/core/Seat.cpp
index e89c49ae..0b356c6f 100644
--- a/src/protocols/core/Seat.cpp
+++ b/src/protocols/core/Seat.cpp
@@ -109,6 +109,8 @@ CWLPointerResource::CWLPointerResource(SP<CWlPointer> resource_, SP<CWLSeatResou
if UNLIKELY (!good())
return;
+ m_resource->setData(this);
+
m_resource->setRelease([this](CWlPointer* r) { PROTO::seat->destroyResource(this); });
m_resource->setOnDestroy([this](CWlPointer* r) { PROTO::seat->destroyResource(this); });
@@ -145,6 +147,11 @@ bool CWLPointerResource::good() {
return m_resource->resource();
}
+SP<CWLPointerResource> CWLPointerResource::fromResource(wl_resource* res) {
+ auto data = sc<CWLPointerResource*>(sc<CWlPointer*>(wl_resource_get_user_data(res))->data());
+ return data ? data->m_self.lock() : nullptr;
+}
+
void CWLPointerResource::sendEnter(SP<CWLSurfaceResource> surface, const Vector2D& local) {
if (!m_owner || m_currentSurface == surface || !surface->getResource()->resource())
return;
@@ -439,6 +446,8 @@ CWLSeatResource::CWLSeatResource(SP<CWlSeat> resource_) : m_resource(resource_)
return;
}
+ RESOURCE->m_self = RESOURCE;
+
m_pointers.emplace_back(RESOURCE);
});
diff --git a/src/protocols/core/Seat.hpp b/src/protocols/core/Seat.hpp
index 8b8f6004..29399a27 100644
--- a/src/protocols/core/Seat.hpp
+++ b/src/protocols/core/Seat.hpp
@@ -88,15 +88,21 @@ class CWLPointerResource {
WP<CWLSeatResource> m_owner;
+ //
+ static SP<CWLPointerResource> fromResource(wl_resource* res);
+
private:
SP<CWlPointer> m_resource;
WP<CWLSurfaceResource> m_currentSurface;
+ WP<CWLPointerResource> m_self;
std::vector<uint32_t> m_pressedButtons;
struct {
CHyprSignalListener destroySurface;
} m_listeners;
+
+ friend class CWLSeatResource;
};
class CWLKeyboardResource {