summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-13 17:31:27 +0900
committerAdam Malczewski <[email protected]>2026-06-13 17:31:27 +0900
commit55588c486d8b407130e76fc7ebbb32a0368931bc (patch)
tree04c04f8adcdb807f062b5a61c8fe74b64b09c937
parentc1dbe7494fb88ceb59bc26914e47ef38eba1cf9e (diff)
downloadunbox-55588c486d8b407130e76fc7ebbb32a0368931bc.tar.gz
unbox-55588c486d8b407130e76fc7ebbb32a0368931bc.zip
Slice 10 b1: ext-xdg-shell Toplevel minimize mechanism
Neutral compositor-side mechanism the stage dock drives to minimize a window (the "minimized" state/policy stays in ext-stage-dock). Adds to Toplevel: geometry() -> wlr_box (size the preview + restore position), scene_tree() -> wlr_scene_tree* (feed UiSubstrate::create_preview + the node hide/show toggles), and hide()/show() — disable/enable the scene node so the client stops compositing and frame callbacks WITHOUT unmapping (no on_toplevel_unmapped, no focus change), idempotent. Verified against wlroots 0.20 that a disabled node quiesces frames. A real in-process wayland client test (test_minimize.cpp) drives it on the headless backend. ext-xdg-shell suite green on build + build-asan. Edits confined to packages/ext-xdg-shell/.
-rw-r--r--packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp38
-rw-r--r--packages/ext-xdg-shell/meson.build40
-rw-r--r--packages/ext-xdg-shell/src/extension.cpp55
-rw-r--r--packages/ext-xdg-shell/tests/test_minimize.cpp325
4 files changed, 445 insertions, 13 deletions
diff --git a/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp b/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp
index a830a4b..2c499d8 100644
--- a/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp
+++ b/packages/ext-xdg-shell/include/unbox/ext-xdg-shell/ext_xdg_shell.hpp
@@ -2,6 +2,7 @@
#include <unbox/kernel/extension.hpp>
#include <unbox/kernel/hooks.hpp>
+#include <unbox/kernel/wlr.hpp> // wlr_box, wlr_scene_tree (the mechanism types below)
#include <memory>
#include <string_view>
@@ -59,6 +60,43 @@ public:
// its own unmap/destroy then fires normally.
virtual void close() = 0;
+ // ---- Minimize mechanism (slice 10 / stage dock) ----------------------
+ //
+ // Neutral compositor-side mechanism the stage dock drives to minimize a
+ // window. The "minimized" STATE and dock placement are ext-stage-dock
+ // policy — none of it is tracked here; this surface only hides/shows the
+ // scene node and reports geometry.
+
+ // The window's current on-screen box in LAYOUT coordinates: its scene-node
+ // position plus the size of its current xdg window geometry. Valid only for
+ // the call. The dock uses it to size the preview snapshot and to restore
+ // the window to where it was. Returns sane values only for a MAPPED
+ // toplevel; for an unmapped one the box reflects the last committed
+ // geometry at the node's last position and should not be relied upon (call
+ // it while mapped, e.g. from on_toplevel_mapped onward and before unmap).
+ [[nodiscard]] virtual auto geometry() const -> wlr_box = 0;
+
+ // The scene tree hosting this toplevel's surfaces — the SAME tree this
+ // extension created and registered via Host::host_surface (so it equals
+ // Host::scene_tree_for(this toplevel's wl_surface)). A BORROW owned by THIS
+ // extension; valid only while the Toplevel borrow is live (drop it on
+ // unmap). Never destroy it. The dock feeds it to
+ // UiSubstrate::create_preview() to snapshot the window, and it is the node
+ // hide()/show() enable/disable.
+ [[nodiscard]] virtual auto scene_tree() -> wlr_scene_tree* = 0;
+
+ // Compositor-side HIDE / SHOW: disable / enable the toplevel's scene node
+ // so it is not composited and the client stops receiving frame callbacks
+ // (wlr_scene withholds them from a non-visible node), WITHOUT unmapping it
+ // — the client stays mapped and NO on_toplevel_unmapped fires. Idempotent
+ // (double hide/show is fine). Does NOT change keyboard focus or raise: if
+ // the focused window is hidden, the caller drives focus to whatever should
+ // be focused next (via focus() on that toplevel). "minimized" is the
+ // caller's concept, not tracked here. A hidden toplevel still unmaps /
+ // destroys normally (on_toplevel_unmapped fires) when its client closes it.
+ virtual void hide() = 0;
+ virtual void show() = 0;
+
protected:
Toplevel() = default;
};
diff --git a/packages/ext-xdg-shell/meson.build b/packages/ext-xdg-shell/meson.build
index 6fb1543..dc0d917 100644
--- a/packages/ext-xdg-shell/meson.build
+++ b/packages/ext-xdg-shell/meson.build
@@ -33,3 +33,43 @@ ext_xdg_shell_test = executable(
dependencies: [ext_xdg_shell_dep, doctest_dep],
)
test('ext-xdg-shell', ext_xdg_shell_test, suite: 'ext-xdg-shell')
+
+# slice 10 / b1: a real in-process wayland CLIENT maps an xdg_toplevel so we can
+# drive the minimize MECHANISM (Toplevel::hide/show/geometry/scene_tree) on the
+# wlr headless backend. Needs CLIENT-side xdg-shell bindings, generated from the
+# canonical xdg-shell.xml the same way ext-layer-shell generates its protocol
+# code (header + private-code-as-header, #included once by the single C++ TU).
+wayland_client_dep = dependency('wayland-client')
+wayland_protocols_dir = dependency('wayland-protocols').get_variable('pkgdatadir')
+xdg_shell_xml = wayland_protocols_dir / 'stable' / 'xdg-shell' / 'xdg-shell.xml'
+
+xdg_shell_client_header = custom_target(
+ 'xdg-shell-client-header',
+ input: xdg_shell_xml,
+ output: 'xdg-shell-client-protocol.h',
+ command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'],
+)
+# private-code emitted as a .h so the C++ TU #includes it exactly once (the root
+# project declares only C++; a generated .c would have no compiler).
+xdg_shell_client_code = custom_target(
+ 'xdg-shell-client-code-impl',
+ input: xdg_shell_xml,
+ output: 'xdg-shell-client-protocol-code.h',
+ command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'],
+)
+
+ext_xdg_shell_client_test = executable(
+ 'ext-xdg-shell-client-tests',
+ 'tests/test_minimize.cpp',
+ xdg_shell_client_header,
+ xdg_shell_client_code,
+ dependencies: [ext_xdg_shell_dep, wayland_client_dep, doctest_dep],
+)
+test(
+ 'ext-xdg-shell-client',
+ ext_xdg_shell_client_test,
+ suite: 'ext-xdg-shell',
+ # Real socket handshake + cooperative event-loop pump; generous timeout so a
+ # slow CI box does not flake (the test fails fast on its own logic).
+ timeout: 60,
+)
diff --git a/packages/ext-xdg-shell/src/extension.cpp b/packages/ext-xdg-shell/src/extension.cpp
index 003c4b9..68c517f 100644
--- a/packages/ext-xdg-shell/src/extension.cpp
+++ b/packages/ext-xdg-shell/src/extension.cpp
@@ -37,7 +37,7 @@ class XdgShellExtension;
struct ToplevelEntry final : Toplevel {
XdgShellExtension* ext = nullptr;
wlr_xdg_toplevel* xdg_toplevel = nullptr;
- wlr_scene_tree* scene_tree = nullptr;
+ wlr_scene_tree* scene = nullptr;
bool mapped = false;
// Typed surface->scene-tree association (replaces the old .data
@@ -68,6 +68,35 @@ struct ToplevelEntry final : Toplevel {
wlr_xdg_toplevel_send_close(xdg_toplevel);
}
}
+
+ // ---- minimize mechanism (slice 10 / b1) ----
+ [[nodiscard]] auto geometry() const -> wlr_box override {
+ // Layout origin = scene-node position offset by the window-geometry
+ // origin (same decomposition begin_resize uses); size = current xdg
+ // window geometry (wlroots falls this back to the surface extent when
+ // the client set no explicit geometry).
+ wlr_box box{};
+ if (scene == nullptr || xdg_toplevel == nullptr) {
+ return box;
+ }
+ const wlr_box& geo = xdg_toplevel->base->geometry;
+ box.x = scene->node.x + geo.x;
+ box.y = scene->node.y + geo.y;
+ box.width = geo.width;
+ box.height = geo.height;
+ return box;
+ }
+ [[nodiscard]] auto scene_tree() -> wlr_scene_tree* override { return scene; }
+ void hide() override {
+ if (scene != nullptr) {
+ wlr_scene_node_set_enabled(&scene->node, false);
+ }
+ }
+ void show() override {
+ if (scene != nullptr) {
+ wlr_scene_node_set_enabled(&scene->node, true);
+ }
+ }
};
struct PopupEntry {
@@ -181,7 +210,7 @@ public:
wlr_xdg_toplevel_set_activated(p, false);
}
}
- wlr_scene_node_raise_to_top(&entry->scene_tree->node);
+ wlr_scene_node_raise_to_top(&entry->scene->node);
wlr_xdg_toplevel_set_activated(entry->xdg_toplevel, true);
if (wlr_keyboard* kb = wlr_seat_get_keyboard(seat)) {
@@ -198,17 +227,17 @@ private:
ToplevelEntry* entry = owned.get();
entry->ext = this;
entry->xdg_toplevel = xdg_toplevel;
- entry->scene_tree = wlr_scene_xdg_surface_create(
+ entry->scene = wlr_scene_xdg_surface_create(
host_->scene_layer(kernel::SceneLayer::normal), xdg_toplevel->base);
// PRIVATE bookkeeping: our own hit-test recovers the entry from the
// tree node's data (an intra-unit use of .data, which the registry
// contract explicitly still permits). The CROSS-UNIT surface->tree
// coupling goes through the typed registry below, never .data.
- entry->scene_tree->node.data = entry;
+ entry->scene->node.data = entry;
// Typed surface->scene-tree association so popups (ours or descendants)
// resolve this toplevel's tree via Host::scene_tree_for().
entry->surface_reg =
- host_->host_surface(xdg_toplevel->base->surface, entry->scene_tree);
+ host_->host_surface(xdg_toplevel->base->surface, entry->scene);
toplevels_.emplace(xdg_toplevel, std::move(owned));
entry->map.connect(xdg_toplevel->base->surface->events.map, [this, entry](void*) {
@@ -341,8 +370,8 @@ private:
double ly = 0;
grab_driver_layout(&lx, &ly);
grabbed_ = entry;
- grab_x_ = lx - entry->scene_tree->node.x;
- grab_y_ = ly - entry->scene_tree->node.y;
+ grab_x_ = lx - entry->scene->node.x;
+ grab_y_ = ly - entry->scene->node.y;
}
void begin_resize(ToplevelEntry* entry, std::uint32_t edges) {
@@ -354,15 +383,15 @@ private:
grab_driver_layout(&lx, &ly);
grabbed_ = entry;
wlr_box* geo = &entry->xdg_toplevel->base->geometry;
- const double border_x = (entry->scene_tree->node.x + geo->x) +
+ const double border_x = (entry->scene->node.x + geo->x) +
((edges & WLR_EDGE_RIGHT) != 0 ? geo->width : 0);
- const double border_y = (entry->scene_tree->node.y + geo->y) +
+ const double border_y = (entry->scene->node.y + geo->y) +
((edges & WLR_EDGE_BOTTOM) != 0 ? geo->height : 0);
grab_x_ = lx - border_x;
grab_y_ = ly - border_y;
grab_geobox_ = *geo;
- grab_geobox_.x += entry->scene_tree->node.x;
- grab_geobox_.y += entry->scene_tree->node.y;
+ grab_geobox_.x += entry->scene->node.x;
+ grab_geobox_.y += entry->scene->node.y;
resize_edges_ = edges;
}
@@ -380,7 +409,7 @@ private:
}
void process_cursor_move(double lx, double ly) {
- wlr_scene_node_set_position(&grabbed_->scene_tree->node,
+ wlr_scene_node_set_position(&grabbed_->scene->node,
static_cast<int>(lx - grab_x_),
static_cast<int>(ly - grab_y_));
}
@@ -416,7 +445,7 @@ private:
}
}
wlr_box* geo = &grabbed_->xdg_toplevel->base->geometry;
- wlr_scene_node_set_position(&grabbed_->scene_tree->node, new_left - geo->x,
+ wlr_scene_node_set_position(&grabbed_->scene->node, new_left - geo->x,
new_top - geo->y);
wlr_xdg_toplevel_set_size(grabbed_->xdg_toplevel, new_right - new_left,
new_bottom - new_top);
diff --git a/packages/ext-xdg-shell/tests/test_minimize.cpp b/packages/ext-xdg-shell/tests/test_minimize.cpp
new file mode 100644
index 0000000..2e60799
--- /dev/null
+++ b/packages/ext-xdg-shell/tests/test_minimize.cpp
@@ -0,0 +1,325 @@
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include <doctest/doctest.h>
+
+#include <unbox/ext-xdg-shell/ext_xdg_shell.hpp>
+#include <unbox/kernel/host.hpp>
+#include <unbox/kernel/server.hpp>
+#include <unbox/kernel/wlr.hpp>
+
+#include <wayland-client.h>
+
+// xdg-shell CLIENT bindings, generated from the canonical xdg-shell.xml the
+// same way ext-layer-shell generates its protocol code (a single C++ TU
+// #includes the header + the private-code-as-header exactly once). This lets an
+// in-process real client map an xdg_toplevel so we can drive the slice-10 b1
+// minimize mechanism (hide/show/geometry/scene_tree) end-to-end on the wlr
+// headless backend.
+#include "xdg-shell-client-protocol.h"
+#include "xdg-shell-client-protocol-code.h"
+
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <memory>
+#include <poll.h>
+#include <unistd.h>
+
+// slice 10 / b1 — Toplevel minimize mechanism, integration-tested with a real
+// client. We install ext-xdg-shell plus a tiny OBSERVER extension that depends
+// on it, fetches its Service, and captures the live Toplevel* borrow + Host on
+// on_toplevel_mapped (and notes any on_toplevel_unmapped). A real client maps a
+// toplevel; we then exercise hide()/show()/geometry()/scene_tree() against the
+// borrow and assert the scene node enable-bit flips, no unmap fired, geometry
+// is non-empty, and scene_tree() resolves to the same tree as
+// Host::scene_tree_for(the focused surface). Lenient ext-tier glue per AGENTS.md
+// (the strict correctness lives in the pure-core test_policy.cpp).
+
+namespace {
+
+using unbox::ext_xdg_shell::Service;
+using unbox::ext_xdg_shell::Toplevel;
+using unbox::ext_xdg_shell::ToplevelEvent;
+
+// ---- the observer extension (test-only, depends on xdg-shell) --------------
+//
+// Holds onto the kernel Host so the test can resolve scene_tree_for() the way a
+// real consumer (ext-stage-dock) would. Captures the mapped Toplevel borrow;
+// the borrow is valid until on_toplevel_unmapped, which is exactly when we stop
+// touching it. No ownership of anything kernel-owned.
+class Observer final : public unbox::kernel::Extension {
+public:
+ [[nodiscard]] auto manifest() const -> const unbox::kernel::Manifest& override {
+ return manifest_;
+ }
+
+ void activate(unbox::kernel::Host& host) override {
+ host_ = &host;
+ // Typed cross-extension coupling: a missing xdg-shell is a nullptr here
+ // (and a link error on the Service type), never a string lookup.
+ Service* svc = host.service<Service>();
+ REQUIRE(svc != nullptr);
+ sub_mapped_ = host.subscribe(svc->on_toplevel_mapped(),
+ [this](const ToplevelEvent& e) {
+ mapped_ = e.toplevel;
+ ++mapped_count_;
+ });
+ sub_unmapped_ = host.subscribe(svc->on_toplevel_unmapped(),
+ [this](const ToplevelEvent& e) {
+ if (e.toplevel == mapped_) {
+ mapped_ = nullptr;
+ }
+ ++unmapped_count_;
+ });
+ }
+
+ [[nodiscard]] auto host() -> unbox::kernel::Host* { return host_; }
+ [[nodiscard]] auto mapped() -> Toplevel* { return mapped_; }
+ [[nodiscard]] auto mapped_count() const -> int { return mapped_count_; }
+ [[nodiscard]] auto unmapped_count() const -> int { return unmapped_count_; }
+
+private:
+ unbox::kernel::Manifest manifest_{"test-observer", unbox::kernel::Tier::standard,
+ {"xdg-shell"}};
+ unbox::kernel::Host* host_ = nullptr;
+ Toplevel* mapped_ = nullptr;
+ int mapped_count_ = 0;
+ int unmapped_count_ = 0;
+ unbox::kernel::Subscription sub_mapped_;
+ unbox::kernel::Subscription sub_unmapped_;
+};
+
+// ---- minimal xdg-shell client ----------------------------------------------
+
+struct Client {
+ wl_display* display = nullptr;
+ wl_registry* registry = nullptr;
+ wl_compositor* compositor = nullptr;
+ wl_shm* shm = nullptr;
+ xdg_wm_base* wm_base = nullptr;
+
+ wl_surface* surface = nullptr;
+ xdg_surface* xsurface = nullptr;
+ xdg_toplevel* toplevel = nullptr;
+
+ bool configured = false;
+ std::uint32_t last_configure_serial = 0;
+};
+
+void wm_base_ping(void*, xdg_wm_base* wm, std::uint32_t serial) {
+ xdg_wm_base_pong(wm, serial);
+}
+const xdg_wm_base_listener kWmBaseListener{wm_base_ping};
+
+void registry_global(void* data, wl_registry* reg, std::uint32_t name,
+ const char* iface, std::uint32_t version) {
+ auto* c = static_cast<Client*>(data);
+ if (std::strcmp(iface, wl_compositor_interface.name) == 0) {
+ c->compositor = static_cast<wl_compositor*>(
+ wl_registry_bind(reg, name, &wl_compositor_interface, 4));
+ } else if (std::strcmp(iface, wl_shm_interface.name) == 0) {
+ c->shm = static_cast<wl_shm*>(wl_registry_bind(reg, name, &wl_shm_interface, 1));
+ } else if (std::strcmp(iface, xdg_wm_base_interface.name) == 0) {
+ c->wm_base = static_cast<xdg_wm_base*>(wl_registry_bind(
+ reg, name, &xdg_wm_base_interface, version < 3 ? version : 3));
+ xdg_wm_base_add_listener(c->wm_base, &kWmBaseListener, c);
+ }
+}
+void registry_global_remove(void*, wl_registry*, std::uint32_t) {}
+const wl_registry_listener kRegistryListener{registry_global, registry_global_remove};
+
+void xsurface_configure(void* data, xdg_surface* xs, std::uint32_t serial) {
+ auto* c = static_cast<Client*>(data);
+ c->last_configure_serial = serial;
+ xdg_surface_ack_configure(xs, serial);
+ c->configured = true;
+}
+const xdg_surface_listener kXSurfaceListener{xsurface_configure};
+
+void toplevel_configure(void*, xdg_toplevel*, std::int32_t, std::int32_t, wl_array*) {}
+void toplevel_close(void*, xdg_toplevel*) {}
+// Trailing members (configure_bounds, wm_capabilities — xdg-shell v4/v5) are
+// value-initialized to nullptr; we bind at most v3 so they are never invoked.
+const xdg_toplevel_listener kToplevelListener{toplevel_configure, toplevel_close, {}, {}};
+
+// Cooperative single-thread pump (the standard libwayland guarded-read dance).
+void pump(unbox::kernel::Server& server, wl_display* client) {
+ wl_display_flush(client);
+ server.dispatch(5);
+ while (wl_display_prepare_read(client) != 0) {
+ wl_display_dispatch_pending(client);
+ }
+ wl_display_flush(client);
+ pollfd pfd{wl_display_get_fd(client), POLLIN, 0};
+ if (poll(&pfd, 1, 5) > 0 && (pfd.revents & POLLIN)) {
+ wl_display_read_events(client);
+ } else {
+ wl_display_cancel_read(client);
+ }
+ wl_display_dispatch_pending(client);
+}
+
+auto make_headless_server() -> std::unique_ptr<unbox::kernel::Server> {
+ setenv("WLR_BACKENDS", "headless", 1);
+ setenv("WLR_RENDERER", "pixman", 1);
+ setenv("WLR_HEADLESS_OUTPUTS", "1", 1);
+ return unbox::kernel::Server::create({});
+}
+
+// A 256x256 ARGB shm buffer so the surface actually maps (a toplevel maps on
+// its first buffer-bearing commit after the initial configure).
+auto make_buffer(Client& c, int w, int h) -> wl_buffer* {
+ const int stride = w * 4;
+ const int size = stride * h;
+ char name[] = "/tmp/unbox-xdg-test-XXXXXX";
+ int fd = mkstemp(name);
+ REQUIRE(fd >= 0);
+ unlink(name);
+ REQUIRE(ftruncate(fd, size) == 0);
+ REQUIRE(c.shm != nullptr);
+ wl_shm_pool* pool = wl_shm_create_pool(c.shm, fd, size);
+ wl_buffer* buffer =
+ wl_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
+ wl_shm_pool_destroy(pool);
+ close(fd);
+ return buffer;
+}
+
+} // namespace
+
+TEST_CASE("slice 10/b1: hide/show/geometry/scene_tree on a real mapped toplevel") {
+ auto server = make_headless_server();
+ server->install(unbox::ext_xdg_shell::create());
+ auto observer_owned = std::make_unique<Observer>();
+ Observer* observer = observer_owned.get();
+ server->install(std::move(observer_owned));
+ server->activate_extensions();
+ REQUIRE(!server->socket_name().empty());
+
+ Client c;
+ c.display = wl_display_connect(server->socket_name().c_str());
+ REQUIRE(c.display != nullptr);
+ c.registry = wl_display_get_registry(c.display);
+ wl_registry_add_listener(c.registry, &kRegistryListener, &c);
+
+ for (int i = 0; i < 50 &&
+ (c.compositor == nullptr || c.wm_base == nullptr || c.shm == nullptr);
+ ++i) {
+ pump(*server, c.display);
+ }
+ REQUIRE(c.compositor != nullptr);
+ REQUIRE(c.wm_base != nullptr);
+ REQUIRE(c.shm != nullptr);
+
+ // Map a toplevel: create surface -> xdg_surface -> xdg_toplevel, the
+ // mandatory empty initial commit, wait for the configure, then attach a
+ // buffer and commit to actually map it.
+ c.surface = wl_compositor_create_surface(c.compositor);
+ c.xsurface = xdg_wm_base_get_xdg_surface(c.wm_base, c.surface);
+ xdg_surface_add_listener(c.xsurface, &kXSurfaceListener, &c);
+ c.toplevel = xdg_surface_get_toplevel(c.xsurface);
+ xdg_toplevel_add_listener(c.toplevel, &kToplevelListener, &c);
+ xdg_toplevel_set_title(c.toplevel, "unbox-test");
+ wl_surface_commit(c.surface); // initial empty commit -> expect configure
+
+ for (int i = 0; i < 200 && !c.configured; ++i) {
+ pump(*server, c.display);
+ }
+ REQUIRE(c.configured);
+
+ wl_buffer* buffer = make_buffer(c, 256, 256);
+ REQUIRE(buffer != nullptr);
+ wl_surface_attach(c.surface, buffer, 0, 0);
+ wl_surface_damage(c.surface, 0, 0, 256, 256);
+ wl_surface_commit(c.surface);
+
+ // Pump until the server-side map fires and the observer captures the borrow.
+ for (int i = 0; i < 200 && observer->mapped() == nullptr; ++i) {
+ pump(*server, c.display);
+ }
+
+ Toplevel* tl = observer->mapped();
+ REQUIRE(tl != nullptr);
+ REQUIRE(observer->mapped_count() == 1);
+ REQUIRE(observer->unmapped_count() == 0);
+
+ // scene_tree(): non-null, a borrow, and equal to what the kernel registry
+ // resolves the toplevel's surface to (the typed surface->tree contract the
+ // dock relies on). We recover the toplevel's wl_surface by walking the
+ // buffer nodes under the returned tree (the public Toplevel contract does
+ // not expose the surface; the kernel registry keys on it), then confirm the
+ // round-trip scene_tree_for(surface) == scene_tree().
+ wlr_scene_tree* tree = tl->scene_tree();
+ REQUIRE(tree != nullptr);
+ struct SurfaceCatch {
+ wlr_surface* surface = nullptr;
+ } caught;
+ wlr_scene_node_for_each_buffer(
+ &tree->node,
+ [](wlr_scene_buffer* buf, int, int, void* data) {
+ auto* sc = static_cast<SurfaceCatch*>(data);
+ if (sc->surface == nullptr) {
+ if (wlr_scene_surface* ss = wlr_scene_surface_try_from_buffer(buf)) {
+ sc->surface = ss->surface;
+ }
+ }
+ },
+ &caught);
+ REQUIRE(caught.surface != nullptr);
+ CHECK(observer->host()->scene_tree_for(caught.surface) == tree);
+
+ // geometry(): a non-empty box for a mapped toplevel.
+ wlr_box box = tl->geometry();
+ CHECK(box.width > 0);
+ CHECK(box.height > 0);
+
+ // hide(): disables the scene node (no compositing, frame callbacks cease),
+ // WITHOUT unmapping — no on_toplevel_unmapped, client stays mapped.
+ CHECK(tree->node.enabled == true);
+ tl->hide();
+ CHECK(tree->node.enabled == false);
+ tl->hide(); // idempotent
+ CHECK(tree->node.enabled == false);
+ // Pump a few turns: the client must NOT be unmapped by the hide.
+ for (int i = 0; i < 10; ++i) {
+ pump(*server, c.display);
+ }
+ CHECK(observer->unmapped_count() == 0);
+ CHECK(observer->mapped() == tl); // still the live borrow
+
+ // show(): re-enables the node. Idempotent.
+ tl->show();
+ CHECK(tree->node.enabled == true);
+ tl->show();
+ CHECK(tree->node.enabled == true);
+
+ // A hidden toplevel must still unmap normally when the client closes it:
+ // hide it again, then destroy client-side; on_toplevel_unmapped MUST fire.
+ tl->hide();
+ xdg_toplevel_destroy(c.toplevel);
+ xdg_surface_destroy(c.xsurface);
+ wl_surface_destroy(c.surface);
+ c.toplevel = nullptr;
+ c.xsurface = nullptr;
+ c.surface = nullptr;
+ for (int i = 0; i < 200 && observer->unmapped_count() == 0; ++i) {
+ pump(*server, c.display);
+ }
+ CHECK(observer->unmapped_count() == 1);
+
+ wl_buffer_destroy(buffer);
+ if (c.wm_base != nullptr) {
+ xdg_wm_base_destroy(c.wm_base);
+ }
+ if (c.shm != nullptr) {
+ wl_shm_destroy(c.shm);
+ }
+ if (c.compositor != nullptr) {
+ wl_compositor_destroy(c.compositor);
+ }
+ if (c.registry != nullptr) {
+ wl_registry_destroy(c.registry);
+ }
+ wl_display_flush(c.display);
+ pump(*server, c.display);
+ wl_display_disconnect(c.display);
+}