diff options
| author | Adam Malczewski <[email protected]> | 2026-06-13 17:31:27 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-13 17:31:27 +0900 |
| commit | 55588c486d8b407130e76fc7ebbb32a0368931bc (patch) | |
| tree | 04c04f8adcdb807f062b5a61c8fe74b64b09c937 | |
| parent | c1dbe7494fb88ceb59bc26914e47ef38eba1cf9e (diff) | |
| download | unbox-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.hpp | 38 | ||||
| -rw-r--r-- | packages/ext-xdg-shell/meson.build | 40 | ||||
| -rw-r--r-- | packages/ext-xdg-shell/src/extension.cpp | 55 | ||||
| -rw-r--r-- | packages/ext-xdg-shell/tests/test_minimize.cpp | 325 |
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); +} |
