diff --git a/hyprtester/CMakeLists.txt b/hyprtester/CMakeLists.txt index 0b445b0ac..d771c6584 100644 --- a/hyprtester/CMakeLists.txt +++ b/hyprtester/CMakeLists.txt @@ -99,3 +99,4 @@ protocolnew("stable/xdg-shell" "xdg-shell" false) clientNew("pointer-warp" PROTOS "pointer-warp-v1" "xdg-shell") clientNew("pointer-scroll" PROTOS "xdg-shell") +clientNew("child-window" PROTOS "xdg-shell") \ No newline at end of file diff --git a/hyprtester/clients/child-window.cpp b/hyprtester/clients/child-window.cpp new file mode 100644 index 000000000..beb86dc32 --- /dev/null +++ b/hyprtester/clients/child-window.cpp @@ -0,0 +1,332 @@ +#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; + + // shm/buffer stuff + CSharedPointer shmPool; + CSharedPointer shmBuf; + CSharedPointer shmBuf2; + 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; +}; + +bool debug, shouldExit, started; + +template +//NOLINTNEXTLINE +static void clientLog(std::format_string fmt, Args&&... args) { + std::string text = std::vformat(fmt.get(), std::make_format_args(args...)); + std::println("{}", text); + std::fflush(stdout); +} + +template +//NOLINTNEXTLINE +static void debugLog(std::format_string fmt, Args&&... args) { + std::string text = std::vformat(fmt.get(), std::make_format_args(args...)); + if (!debug) + return; + std::println("{}", text); + 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)); + } + }); + 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) { + 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 * 2) < 0) { + close(state.shmFd); + return false; + } + + state.shmPool = makeShared(state.wlShm->sendCreatePool(state.shmFd, size * 2)); + 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 * 2); + 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; + + buf = makeShared(state.shmPool->sendCreateBuffer(size, geom.x, geom.y, stride, WL_SHM_FORMAT_XRGB8888)); + if (!buf->resource()) { + clientLog("Failed to create child buffer"); + return false; + } + if (state.shmBuf2) { + state.shmBuf2->sendDestroy(); + state.shmBuf2.reset(); + } + state.shmBuf2 = 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("child-test parent"); + state.xdgToplevel->sendSetAppId("child-test-parent"); + + 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; +} + +struct SChildWindow { + CSharedPointer surface; + CSharedPointer xSurface; + CSharedPointer toplevel; +}; + +static void parseRequest(SWlState& state, std::string str, SChildWindow& window) { + if (str.starts_with("exit")) { + shouldExit = true; + return; + } + + size_t index = str.find_first_of('\n'); + str = str.substr(0, index); + + if (str == "toplevel") { + window.surface = makeShared(state.wlCompositor->sendCreateSurface()); + window.xSurface = makeShared(state.xdgShell->sendGetXdgSurface(window.surface->resource())); + + window.xSurface->setConfigure([&](CCXdgSurface* p, uint32_t serial) { + if (!state.shmBuf) + debugLog("xdgSurf configure but no buf made yet?"); + + window.xSurface->sendSetWindowGeometry(0, 0, state.geom.x, state.geom.y); + window.surface->sendAttach(state.shmBuf2.get(), 0, 0); + window.surface->sendCommit(); + + window.xSurface->sendAckConfigure(serial); + }); + + window.toplevel = makeShared(window.xSurface->sendGetToplevel()); + + window.toplevel->sendSetTitle("child-test child"); + window.toplevel->sendSetAppId("child-test-child"); + window.toplevel->sendSetParent(state.xdgToplevel.get()); + + window.surface->sendAttach(nullptr, 0, 0); + window.surface->sendCommit(); + clientLog("child started"); + return; + } +} + +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; + SChildWindow window; + + // 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()}, window); + } + } + + wl_display* display = state.display; + state = {}; + window = {}; + + wl_display_disconnect(display); + return 0; +} \ No newline at end of file diff --git a/hyprtester/src/tests/clients/child-window.cpp b/hyprtester/src/tests/clients/child-window.cpp new file mode 100644 index 000000000..fa29f75fe --- /dev/null +++ b/hyprtester/src/tests/clients/child-window.cpp @@ -0,0 +1,124 @@ +#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 = 0; + +static bool startClient(SClient& client) { + client.proc = makeShared(binaryDir + "/child-window", std::vector{}); + + client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + + int procInPipeFd[2], procOutPipeFd[2]; + if (pipe(procInPipeFd) != 0 || pipe(procOutPipeFd) != 0) { + NLog::log("{}Unable to open pipe to client", Colors::RED); + return false; + } + + client.writeFd = CFileDescriptor(procInPipeFd[1]); + client.proc->setStdinFD(procInPipeFd[0]); + + client.readFd = CFileDescriptor(procOutPipeFd[0]); + client.proc->setStdoutFD(procOutPipeFd[1]); + + client.proc->runAsync(); + + close(procInPipeFd[0]); + close(procOutPipeFd[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 startStr = std::string{client.readBuf.data()}; + if (startStr.find("started") == std::string::npos) { + NLog::log("{}Failed to start child-window client, read {}", Colors::RED, startStr); + return false; + } + + // wait for window to appear + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + + NLog::log("{}Started child-window 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(); +} + +static bool createChild(SClient& client) { + std::string cmd = "toplevel\n"; + 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(); + + // wait for window to appear + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + if (getFromSocket("/dispatch focuswindow class:child-test-child") != "ok") { + NLog::log("{}Failed to focus child window", Colors::RED); + return false; + } + + + return true; +} + +static bool test() { + SClient client; + + OK(getFromSocket("/keyword windowrule float, pin, class:child-test-parent")); + if (!startClient(client)) + return false; + + createChild(client); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "pinned: 1"); + + stopClient(client); + NLog::log("{}Reloading config", Colors::YELLOW); + OK(getFromSocket("/reload")); + Tests::killAllWindows(); + EXPECT(Tests::windowCount(), 0); + + return !ret; +} + +REGISTER_CLIENT_TEST_FN(test); \ No newline at end of file