diff --git a/hyprtester/CMakeLists.txt b/hyprtester/CMakeLists.txt index 5aa644c1c..023c531cb 100644 --- a/hyprtester/CMakeLists.txt +++ b/hyprtester/CMakeLists.txt @@ -109,3 +109,4 @@ clientNew("child-window" PROTOS "xdg-shell") clientNew("xdg-interactive" PROTOS "xdg-shell") clientNew("shortcut-inhibitor" PROTOS "xdg-shell" "keyboard-shortcuts-inhibit-unstable-v1") clientNew("keyboard-modifiers" PROTOS "xdg-shell") +clientNew("fullscreen-with-monitor" PROTOS "xdg-shell") diff --git a/hyprtester/clients/fullscreen-with-monitor.cpp b/hyprtester/clients/fullscreen-with-monitor.cpp new file mode 100644 index 000000000..797881f0f --- /dev/null +++ b/hyprtester/clients/fullscreen-with-monitor.cpp @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +using Hyprutils::Math::Vector2D; +using namespace Hyprutils::Memory; + +struct SState { + wl_display* display; + CSharedPointer registry; + CSharedPointer compositor; + CSharedPointer shm; + CSharedPointer xdgWm; + CSharedPointer output; + + CSharedPointer shmPool; + CSharedPointer shmBuf; + int shmFd = -1; + size_t shmSize = 0; + bool hasXrgb = false; + + CSharedPointer surf; + CSharedPointer xdgSurf; + CSharedPointer toplevel; + Vector2D geom = {1280, 720}; + bool fullscreen = false; +}; + +static bool started = false; +static bool shouldExit = false; + +template +static void clientLog(std::format_string fmt, Args&&... args) { + std::string s = std::format(fmt, std::forward(args)...); + std::println("{}", s); + std::fflush(stdout); +} + +static bool bindGlobals(SState& s) { + s.registry = makeShared((wl_proxy*)wl_display_get_registry(s.display)); + + s.registry->setGlobal([&](CCWlRegistry* r, uint32_t id, const char* iface, uint32_t) { + const std::string n = iface; + if (n == "wl_compositor") + s.compositor = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)r->resource(), id, &wl_compositor_interface, 6)); + else if (n == "wl_shm") + s.shm = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)r->resource(), id, &wl_shm_interface, 1)); + else if (n == "xdg_wm_base") + s.xdgWm = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)r->resource(), id, &xdg_wm_base_interface, 1)); + else if (n == "wl_output" && !s.output) + // only bind the first output; test env has one virtual monitor + s.output = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)r->resource(), id, &wl_output_interface, 4)); + }); + + s.registry->setGlobalRemove([](CCWlRegistry*, uint32_t) {}); + wl_display_roundtrip(s.display); + + return s.compositor && s.shm && s.xdgWm && s.output; +} + +static bool makeShm(SState& s, Vector2D geom) { + if (!s.hasXrgb) + return false; + + size_t stride = (size_t)geom.x * 4; + size_t size = (size_t)geom.y * stride; + + if (!s.shmPool) { + const char* name = "/wl-shm-fs-mon"; + s.shmFd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (s.shmFd < 0) + return false; + if (shm_unlink(name) < 0 || ftruncate(s.shmFd, size) < 0) { + close(s.shmFd); + return false; + } + s.shmPool = makeShared(s.shm->sendCreatePool(s.shmFd, size)); + s.shmSize = size; + } else if (size > s.shmSize) { + if (ftruncate(s.shmFd, size) < 0) + return false; + s.shmPool->sendResize(size); + s.shmSize = size; + } + + if (s.shmBuf) { + s.shmBuf->sendDestroy(); + s.shmBuf.reset(); + } + s.shmBuf = makeShared(s.shmPool->sendCreateBuffer(0, geom.x, geom.y, stride, WL_SHM_FORMAT_XRGB8888)); + return s.shmBuf && s.shmBuf->resource(); +} + +static bool setupSurface(SState& s) { + s.shm->setFormat([&](CCWlShm*, uint32_t fmt) { + if (fmt == WL_SHM_FORMAT_XRGB8888) + s.hasXrgb = true; + }); + + s.xdgWm->setPing([&](CCXdgWmBase* p, uint32_t serial) { p->sendPong(serial); }); + + s.surf = makeShared(s.compositor->sendCreateSurface()); + s.xdgSurf = makeShared(s.xdgWm->sendGetXdgSurface(s.surf->resource())); + s.toplevel = makeShared(s.xdgSurf->sendGetToplevel()); + + if (!s.surf->resource() || !s.xdgSurf->resource() || !s.toplevel->resource()) + return false; + + s.toplevel->setClose([](CCXdgToplevel*) { exit(0); }); + + s.toplevel->setConfigure([&](CCXdgToplevel*, int32_t w, int32_t h, wl_array* states) { + if (w > 0 && h > 0) + s.geom = {(double)w, (double)h}; + + // parse the state array to detect fullscreen + s.fullscreen = false; + auto stateSpan = std::span(static_cast(states->data), states->size / sizeof(uint32_t)); + for (uint32_t st : stateSpan) { + + if (st == XDG_TOPLEVEL_STATE_FULLSCREEN) { + s.fullscreen = true; + break; + } + } + + if (!makeShm(s, s.geom)) + exit(-1); + }); + + s.xdgSurf->setConfigure([&](CCXdgSurface* p, uint32_t serial) { + if (!s.shmBuf) + return; + + p->sendSetWindowGeometry(0, 0, s.geom.x, s.geom.y); + s.surf->sendAttach(s.shmBuf.get(), 0, 0); + s.surf->sendCommit(); + p->sendAckConfigure(serial); + + if (!started) { + started = true; + clientLog("started"); + } + }); + + s.toplevel->sendSetTitle("fullscreen-with-monitor client"); + s.toplevel->sendSetAppId("fullscreen-with-monitor"); + + s.surf->sendAttach(nullptr, 0, 0); + s.surf->sendCommit(); + + return true; +} + +static void handleCmd(SState& s, std::string_view cmd) { + if (cmd.starts_with("unfullscreen")) { + s.toplevel->sendUnsetFullscreen(); + wl_display_flush(s.display); + } else if (cmd.starts_with("fullscreen")) { + s.toplevel->sendSetFullscreen(s.output->resource()); + wl_display_flush(s.display); + } else if (cmd.starts_with("get")) { + clientLog("{}", s.fullscreen ? "1" : "0"); + } else if (cmd.starts_with("exit")) { + shouldExit = true; + } +} + +int main() { + SState s; + + s.display = wl_display_connect(nullptr); + if (!s.display) { + clientLog("connect failed"); + return -1; + } + + if (!bindGlobals(s)) { + clientLog("failed to bind globals (no wl_output?)"); + return -1; + } + + if (!setupSurface(s)) { + clientLog("surface setup failed"); + return -1; + } + + std::array buf; + buf.fill(0); + wl_display_flush(s.display); + + struct pollfd fds[2] = {{.fd = wl_display_get_fd(s.display), .events = POLLIN | POLLOUT}, {.fd = STDIN_FILENO, .events = POLLIN}}; + + while (!shouldExit && poll(fds, 2, 0 - 1) != -1) { + if (fds[0].revents & POLLIN) { + wl_display_flush(s.display); + if (wl_display_prepare_read(s.display) == 0) { + wl_display_read_events(s.display); + wl_display_dispatch_pending(s.display); + } else + wl_display_dispatch(s.display); + + int ret; + do { + ret = wl_display_dispatch_pending(s.display); + wl_display_flush(s.display); + } while (ret > 0); + } + + if (fds[1].revents & POLLIN) { + ssize_t n = read(fds[1].fd, buf.data(), buf.size() - 1); + if (n > 0) { + buf[n] = 0; + handleCmd(s, buf.data()); + } + } + } + + wl_display* display = s.display; + s = {}; + wl_display_disconnect(display); + return 0; +} \ No newline at end of file diff --git a/hyprtester/src/tests/clients/fullscreen-with-monitor.cpp b/hyprtester/src/tests/clients/fullscreen-with-monitor.cpp new file mode 100644 index 000000000..4e3e2573b --- /dev/null +++ b/hyprtester/src/tests/clients/fullscreen-with-monitor.cpp @@ -0,0 +1,156 @@ +// add to includes +#include "hyprtester/src/hyprctlCompat.hpp" +#include "hyprtester/src/shared.hpp" +#include "hyprtester/src/tests/shared.hpp" +#include +#include +#include +#include +#include +#include +#include "tests.hpp" +#include "build.hpp" + +namespace { + using namespace Hyprutils::OS; + using namespace Hyprutils::Memory; +#define SP CSharedPointer + + class CFsMonClient { + SP proc; + std::array readBuf; + CFileDescriptor readFd, writeFd; + struct pollfd fds; + + public: + CFsMonClient(); + ~CFsMonClient(); + bool isFullscreen(); + void requestFullscreen(); + void requestUnFullscreen(); + pid_t pid(); + }; + +#undef SP +} + +CFsMonClient::CFsMonClient() { + Tests::killAllWindows(); + this->proc = makeShared(std::format("{}/fullscreen-with-monitor", binaryDir), std::vector{}); + this->proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + + int pipeFds1[2], pipeFds2[2]; + if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) { + NLog::log("{}CFsMonClient: pipe() failed", Colors::RED); + throw std::exception(); + } + + this->writeFd = CFileDescriptor(pipeFds1[1]); + this->proc->setStdinFD(pipeFds1[0]); + this->readFd = CFileDescriptor(pipeFds2[0]); + this->proc->setStdoutFD(pipeFds2[1]); + + const int COUNT_BEFORE = Tests::windowCount(); + this->proc->runAsync(); + close(pipeFds1[0]); + close(pipeFds2[1]); + + this->fds = {.fd = this->readFd.get(), .events = POLLIN}; + + // wait for "started\n" from the client + if (poll(&this->fds, 1, 2000) != 1 || !(this->fds.revents & POLLIN)) { + NLog::log("{}CFsMonClient: timed out waiting for start", Colors::RED); + throw std::exception(); + } + this->readBuf.fill(0); + if (read(this->readFd.get(), this->readBuf.data(), this->readBuf.size() - 1) == -1) { + NLog::log("{}CFsMonClient: read failed", Colors::RED); + throw std::exception(); + } + if (!std::string_view{this->readBuf.data()}.contains("started")) { + NLog::log("{}CFsMonClient: unexpected startup output: {}", Colors::RED, this->readBuf.data()); + throw std::exception(); + } + + // wait for the window to actually appear in the compositor + int counter = 0; + while (Tests::processAlive(this->proc->pid()) && Tests::windowCount() == COUNT_BEFORE) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (++counter > 50) { + NLog::log("{}CFsMonClient: window never appeared", Colors::RED); + throw std::exception(); + } + } + if (!Tests::processAlive(this->proc->pid())) { + NLog::log("{}CFsMonClient: process died before window appeared", Colors::RED); + throw std::exception(); + } + + NLog::log("{}CFsMonClient ready", Colors::YELLOW); +} + +CFsMonClient::~CFsMonClient() { + std::string cmd = "exit\n"; + write(this->writeFd.get(), cmd.c_str(), cmd.length()); + kill(this->proc->pid(), SIGKILL); + this->proc.reset(); +} + +void CFsMonClient::requestFullscreen() { + std::string cmd = "fullscreen\n"; + write(this->writeFd.get(), cmd.c_str(), cmd.length()); + // give the round-trip time to complete: + // client -> compositor (set_fullscreen with output) -> compositor sends configure -> client acks + std::this_thread::sleep_for(std::chrono::milliseconds(300)); +} + +void CFsMonClient::requestUnFullscreen() { + std::string cmd = "unfullscreen\n"; + write(this->writeFd.get(), cmd.c_str(), cmd.length()); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); +} + +bool CFsMonClient::isFullscreen() { + std::string cmd = "get\n"; + if ((size_t)write(this->writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) + return false; + + if (poll(&this->fds, 1, 1500) != 1 || !(this->fds.revents & POLLIN)) + return false; + + this->readBuf.fill(0); + ssize_t n = read(this->fds.fd, this->readBuf.data(), this->readBuf.size() - 1); + if (n <= 0) + return false; + + this->readBuf[n] = 0; + return std::string{this->readBuf.data()}.contains('1'); +} + +TEST_CASE(fullscreenWithExplicitMonitor) { + NLog::log("{}Testing xdg_toplevel_set_fullscreen with explicit wl_output", Colors::GREEN); + + // move to a special workspace for the test + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'special:A', follow = true })")); + + std::optional client; + try { + client.emplace(); + } catch (...) { FAIL_TEST("Failed to start fullscreen-with-monitor client"); } + + // sanity: window should not be fullscreen before we ask + EXPECT(client->isFullscreen(), false); + + client->requestFullscreen(); + + // client parsed the fullscreen state from the configure wl_array + EXPECT(client->isFullscreen(), true); + + // unFullscreen + client->requestUnFullscreen(); + EXPECT(client->isFullscreen(), false); + + // expect the client to be in the special workspace still + + EXPECT_CONTAINS(getFromSocket("/clients"), "workspace: -98 (special:A)") +} \ No newline at end of file diff --git a/nix/default.nix b/nix/default.nix index c372cce3c..3d5377ffe 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -268,6 +268,7 @@ customStdenv.mkDerivation (finalAttrs: { install hyprtester/pointer-scroll -t $out/bin install hyprtester/shortcut-inhibitor -t $out/bin install hyprtester/keyboard-modifiers -t $out/bin + install hyprtester/fullscreen-with-monitor -t $out/bin install hyprtester/surface-scale-transform -t $out/bin install hyprtester/xdg-interactive -t $out/bin install hyprland_gtests -t $out/bin diff --git a/src/desktop/view/window/Window.cpp b/src/desktop/view/window/Window.cpp index 0f0d819d3..f3822b9da 100644 --- a/src/desktop/view/window/Window.cpp +++ b/src/desktop/view/window/Window.cpp @@ -841,7 +841,7 @@ void CWindow::requestClientFullscreen(const SClientFullscreenRequest& request) { if (monitor) { requestedMonitor = monitor->m_id; if (m_isMapped) { - Desktop::globalWindowController()->moveWindowToWorkspace(m_self.lock(), monitor->m_activeWorkspace); + Desktop::globalWindowController()->moveWindowToWorkspace(m_self.lock(), monitor->getCurrentWorkspace()); Desktop::focusState()->rawMonitorFocus(monitor); } }