From ea42041f936d5810c5cfa45d6bece12dde2fd9b6 Mon Sep 17 00:00:00 2001 From: Ikalco <73481042+ikalco@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:16:40 -0500 Subject: [PATCH] protocols: implement pointer-warp-v1 (#11469) --- CMakeLists.txt | 3 +- hyprtester/CMakeLists.txt | 70 ++++ hyprtester/clients/pointer-warp.cpp | 317 ++++++++++++++++++ hyprtester/protocols/.gitignore | 2 + hyprtester/src/main.cpp | 9 + hyprtester/src/tests/clients/.gitignore | 1 + hyprtester/src/tests/clients/pointer-warp.cpp | 180 ++++++++++ hyprtester/src/tests/clients/tests.hpp | 12 + nix/hyprtester.nix | 8 + protocols/meson.build | 3 +- src/managers/ProtocolManager.cpp | 3 + src/managers/SeatManager.cpp | 5 +- src/managers/SeatManager.hpp | 2 +- src/protocols/PointerWarp.cpp | 53 +++ src/protocols/PointerWarp.hpp | 21 ++ src/protocols/core/Seat.cpp | 9 + src/protocols/core/Seat.hpp | 6 + 17 files changed, 699 insertions(+), 5 deletions(-) create mode 100644 hyprtester/clients/pointer-warp.cpp create mode 100644 hyprtester/protocols/.gitignore create mode 100644 hyprtester/src/tests/clients/.gitignore create mode 100644 hyprtester/src/tests/clients/pointer-warp.cpp create mode 100644 hyprtester/src/tests/clients/tests.hpp create mode 100644 src/protocols/PointerWarp.cpp create mode 100644 src/protocols/PointerWarp.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 643f7dc6a..b98e7e374 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 10907ce7a..34e62603d 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 \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 000000000..2d3624d52 --- /dev/null +++ b/hyprtester/clients/pointer-warp.cpp @@ -0,0 +1,317 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +using Hyprutils::Math::Vector2D; +using namespace Hyprutils::Memory; + +struct SWlState { + wl_display* display; + CSharedPointer registry; + + // protocols + CSharedPointer wlCompositor; + CSharedPointer wlSeat; + CSharedPointer wlShm; + CSharedPointer xdgShell; + CSharedPointer pointerWarp; + + // shm/buffer stuff + CSharedPointer shmPool; + CSharedPointer shmBuf; + int shmFd; + size_t shmBufSize; + bool xrgb8888_support = false; + + // surface/toplevel stuff + CSharedPointer surf; + CSharedPointer xdgSurf; + CSharedPointer xdgToplevel; + Vector2D geom; + + // pointer + CSharedPointer pointer; + uint32_t enterSerial; +}; + +static bool debug, started, shouldExit; + +template +//NOLINTNEXTLINE +static void clientLog(std::format_string fmt, Args&&... args) { + std::println("{}", std::vformat(fmt.get(), std::make_format_args(args...))); + std::fflush(stdout); +} + +template +//NOLINTNEXTLINE +static void debugLog(std::format_string 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((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((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((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((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((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((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(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(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(state.wlCompositor->sendCreateSurface()); + if (!state.surf->resource()) + return false; + + state.xdgSurf = makeShared(state.xdgShell->sendGetXdgSurface(state.surf->resource())); + if (!state.xdgSurf->resource()) + return false; + + state.xdgToplevel = makeShared(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(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 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 000000000..d6b7ef32c --- /dev/null +++ b/hyprtester/protocols/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/hyprtester/src/main.cpp b/hyprtester/src/main.cpp index f7f2c1981..a1bb7d9a9 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 @@ -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 000000000..77a34c556 --- /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 000000000..643da796e --- /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 +#include + +#include +#include +#include + +using namespace Hyprutils::OS; +using namespace Hyprutils::Memory; + +#define SP CSharedPointer + +struct SClient { + SP proc; + std::array readBuf; + CFileDescriptor readFd, writeFd; + struct pollfd fds; +}; + +static int ret; + +static bool startClient(SClient& client) { + client.proc = makeShared(binaryDir + "/pointer-warp", std::vector{}); + + 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 000000000..31746fe56 --- /dev/null +++ b/hyprtester/src/tests/clients/tests.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +inline std::vector> 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 042228c78..cf9a9c747 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 b52c43498..dc23eaa17 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 b70d3b7d7..c9690abad 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(&xdg_system_bell_v1_interface, 1, "XDGBell"); PROTO::extWorkspace = makeUnique(&ext_workspace_manager_v1_interface, 1, "ExtWorkspace"); PROTO::extDataDevice = makeUnique(&ext_data_control_manager_v1_interface, 1, "ExtDataDevice"); + PROTO::pointerWarp = makeUnique(&wp_pointer_warp_v1_interface, 1, "PointerWarp"); if (*PENABLECM) PROTO::colorManagement = makeUnique(&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 bc4f2dfc6..15316dc62 100644 --- a/src/managers/SeatManager.cpp +++ b/src/managers/SeatManager.cpp @@ -58,7 +58,7 @@ uint32_t CSeatManager::nextSerial(SP seatResource) { return serial; } -bool CSeatManager::serialValid(SP seatResource, uint32_t serial) { +bool CSeatManager::serialValid(SP seatResource, uint32_t serial, bool erase) { if (!seatResource) return false; @@ -68,7 +68,8 @@ bool CSeatManager::serialValid(SP 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 2d73e0875..fe11f9308 100644 --- a/src/managers/SeatManager.hpp +++ b/src/managers/SeatManager.hpp @@ -76,7 +76,7 @@ class CSeatManager { uint32_t nextSerial(SP seatResource); // pops the serial if it was valid, meaning it is consumed. - bool serialValid(SP seatResource, uint32_t serial); + bool serialValid(SP seatResource, uint32_t serial, bool erase = true); void onSetCursor(SP seatResource, uint32_t serial, SP surf, const Vector2D& hotspot); diff --git a/src/protocols/PointerWarp.cpp b/src/protocols/PointerWarp.cpp new file mode 100644 index 000000000..bde0b9132 --- /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(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& resource) { return resource.get() == manager; }); +} diff --git a/src/protocols/PointerWarp.hpp b/src/protocols/PointerWarp.hpp new file mode 100644 index 000000000..d1ce0062f --- /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> m_managers; +}; + +namespace PROTO { + inline UP pointerWarp; +}; diff --git a/src/protocols/core/Seat.cpp b/src/protocols/core/Seat.cpp index e89c49aec..0b356c6f9 100644 --- a/src/protocols/core/Seat.cpp +++ b/src/protocols/core/Seat.cpp @@ -109,6 +109,8 @@ CWLPointerResource::CWLPointerResource(SP resource_, SPsetData(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::fromResource(wl_resource* res) { + auto data = sc(sc(wl_resource_get_user_data(res))->data()); + return data ? data->m_self.lock() : nullptr; +} + void CWLPointerResource::sendEnter(SP surface, const Vector2D& local) { if (!m_owner || m_currentSurface == surface || !surface->getResource()->resource()) return; @@ -439,6 +446,8 @@ CWLSeatResource::CWLSeatResource(SP 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 8b8f6004c..29399a27d 100644 --- a/src/protocols/core/Seat.hpp +++ b/src/protocols/core/Seat.hpp @@ -88,15 +88,21 @@ class CWLPointerResource { WP m_owner; + // + static SP fromResource(wl_resource* res); + private: SP m_resource; WP m_currentSurface; + WP m_self; std::vector m_pressedButtons; struct { CHyprSignalListener destroySurface; } m_listeners; + + friend class CWLSeatResource; }; class CWLKeyboardResource {