core/fullscreen: fix not accounting for special workspaces when client requests FS with a monitor request attached (#15806)

This commit is contained in:
erStarr
2026-08-23 19:21:04 +02:00
committed by GitHub
parent 45a79e5e84
commit 951ab550e7
5 changed files with 394 additions and 1 deletions
+1
View File
@@ -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")
@@ -0,0 +1,235 @@
#include <cstring>
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <thread>
#include <unistd.h>
#include <print>
#include <format>
#include <string>
#include <wayland-client.h>
#include <wayland.hpp>
#include <xdg-shell.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/math/Vector2D.hpp>
using Hyprutils::Math::Vector2D;
using namespace Hyprutils::Memory;
struct SState {
wl_display* display;
CSharedPointer<CCWlRegistry> registry;
CSharedPointer<CCWlCompositor> compositor;
CSharedPointer<CCWlShm> shm;
CSharedPointer<CCXdgWmBase> xdgWm;
CSharedPointer<CCWlOutput> output;
CSharedPointer<CCWlShmPool> shmPool;
CSharedPointer<CCWlBuffer> shmBuf;
int shmFd = -1;
size_t shmSize = 0;
bool hasXrgb = false;
CSharedPointer<CCWlSurface> surf;
CSharedPointer<CCXdgSurface> xdgSurf;
CSharedPointer<CCXdgToplevel> toplevel;
Vector2D geom = {1280, 720};
bool fullscreen = false;
};
static bool started = false;
static bool shouldExit = false;
template <typename... Args>
static void clientLog(std::format_string<Args...> fmt, Args&&... args) {
std::string s = std::format(fmt, std::forward<Args>(args)...);
std::println("{}", s);
std::fflush(stdout);
}
static bool bindGlobals(SState& s) {
s.registry = makeShared<CCWlRegistry>((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<CCWlCompositor>((wl_proxy*)wl_registry_bind((wl_registry*)r->resource(), id, &wl_compositor_interface, 6));
else if (n == "wl_shm")
s.shm = makeShared<CCWlShm>((wl_proxy*)wl_registry_bind((wl_registry*)r->resource(), id, &wl_shm_interface, 1));
else if (n == "xdg_wm_base")
s.xdgWm = makeShared<CCXdgWmBase>((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<CCWlOutput>((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<CCWlShmPool>(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<CCWlBuffer>(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<CCWlSurface>(s.compositor->sendCreateSurface());
s.xdgSurf = makeShared<CCXdgSurface>(s.xdgWm->sendGetXdgSurface(s.surf->resource()));
s.toplevel = makeShared<CCXdgToplevel>(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<const uint32_t>(static_cast<const uint32_t*>(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<char, 1024> 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;
}
@@ -0,0 +1,156 @@
// add to includes
#include "hyprtester/src/hyprctlCompat.hpp"
#include "hyprtester/src/shared.hpp"
#include "hyprtester/src/tests/shared.hpp"
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <optional>
#include <sys/poll.h>
#include <csignal>
#include <thread>
#include "tests.hpp"
#include "build.hpp"
namespace {
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
class CFsMonClient {
SP<CProcess> proc;
std::array<char, 1024> 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<CProcess>(std::format("{}/fullscreen-with-monitor", binaryDir), std::vector<std::string>{});
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<CFsMonClient> 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)")
}
+1
View File
@@ -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
+1 -1
View File
@@ -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);
}
}