summaryrefslogtreecommitdiffhomepage
path: root/packages/ext-xdg-shell/src/extension.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ext-xdg-shell/src/extension.cpp')
-rw-r--r--packages/ext-xdg-shell/src/extension.cpp634
1 files changed, 634 insertions, 0 deletions
diff --git a/packages/ext-xdg-shell/src/extension.cpp b/packages/ext-xdg-shell/src/extension.cpp
new file mode 100644
index 0000000..b200a39
--- /dev/null
+++ b/packages/ext-xdg-shell/src/extension.cpp
@@ -0,0 +1,634 @@
+#include <unbox/ext-xdg-shell/ext_xdg_shell.hpp>
+
+#include "policy.hpp"
+#include "probe.hpp"
+
+#include <unbox/kernel/host.hpp>
+#include <unbox/kernel/listener.hpp>
+#include <unbox/kernel/wlr.hpp>
+
+#include <cstdint>
+#include <iterator>
+#include <list>
+#include <memory>
+#include <stdexcept>
+#include <string_view>
+#include <unordered_map>
+
+// The glue. Translates the kernel's input/output catalogue and the raw
+// xdg-shell signals into window-management policy, calls the pure core in
+// policy.hpp for decisions, and exports the typed Service/Events downstream
+// slices consume. Mirrors the former kernel toplevel.cpp/input.cpp tinywl
+// shape, re-expressed against the Host ABI.
+//
+// Everything runs on the single wl_event_loop thread. Every resource is a RAII
+// member of XdgShellExtension; teardown is reverse-declaration destruction (no
+// manual teardown lists — extension-agent.md).
+
+namespace unbox::ext_xdg_shell {
+namespace {
+
+using kernel::Host;
+using kernel::Listener;
+using kernel::Subscription;
+
+class XdgShellExtension;
+
+// One managed application window. Implements the public Toplevel handle; the
+// extension hands borrows of `Entry*` (upcast to Toplevel*) in event payloads.
+struct ToplevelEntry final : Toplevel {
+ XdgShellExtension* ext = nullptr;
+ wlr_xdg_toplevel* xdg_toplevel = nullptr;
+ wlr_scene_tree* scene_tree = nullptr;
+ bool mapped = false;
+
+ // Typed surface->scene-tree association (replaces the old .data
+ // convention); held here so it unregisters when the toplevel is destroyed.
+ kernel::SurfaceRegistration surface_reg;
+
+ Listener map;
+ Listener unmap;
+ Listener commit;
+ Listener destroy;
+ Listener request_move;
+ Listener request_resize;
+ Listener request_maximize;
+ Listener request_fullscreen;
+
+ // ---- public Toplevel contract ----
+ [[nodiscard]] auto title() const -> std::string_view override {
+ const char* t = xdg_toplevel != nullptr ? xdg_toplevel->title : nullptr;
+ return t != nullptr ? std::string_view{t} : std::string_view{};
+ }
+ [[nodiscard]] auto app_id() const -> std::string_view override {
+ const char* a = xdg_toplevel != nullptr ? xdg_toplevel->app_id : nullptr;
+ return a != nullptr ? std::string_view{a} : std::string_view{};
+ }
+ void focus() override; // defined after XdgShellExtension
+ void close() override {
+ if (xdg_toplevel != nullptr) {
+ wlr_xdg_toplevel_send_close(xdg_toplevel);
+ }
+ }
+};
+
+struct PopupEntry {
+ wlr_xdg_popup* xdg_popup = nullptr;
+ // Typed association for THIS popup's surface, so a nested popup parented to
+ // it resolves via Host::scene_tree_for().
+ kernel::SurfaceRegistration surface_reg;
+ Listener commit;
+ Listener destroy;
+};
+
+class XdgShellExtension final : public kernel::Extension, public ActivationProbe {
+public:
+ XdgShellExtension()
+ : manifest_{"xdg-shell", kernel::Tier::core, {}} {}
+
+ [[nodiscard]] auto manifest() const -> const kernel::Manifest& override { return manifest_; }
+
+ void activate(Host& host) override {
+ host_ = &host;
+ service_.ext = this;
+
+ // Export our hooks through the kernel's isolation registry so a
+ // throwing subscriber disables the SUBSCRIBER, not us.
+ host.adopt(on_mapped_);
+ host.adopt(on_unmapped_);
+ host.adopt(on_focused_);
+
+ // The xdg-shell v3 global lives HERE now (extension-creates-the-global
+ // split). wlr_xdg_shell is owned by the display; it dies with it.
+ xdg_shell_ = wlr_xdg_shell_create(host.display(), 3);
+ if (xdg_shell_ == nullptr) {
+ throw std::runtime_error("ext-xdg-shell: wlr_xdg_shell_create failed");
+ }
+ new_toplevel_.connect(xdg_shell_->events.new_toplevel, [this](void* data) {
+ handle_new_toplevel(static_cast<wlr_xdg_toplevel*>(data));
+ });
+ new_popup_.connect(xdg_shell_->events.new_popup, [this](void* data) {
+ handle_new_popup(static_cast<wlr_xdg_popup*>(data));
+ });
+
+ // Pointer routing is ENTIRELY ours: the kernel moves the cursor and
+ // emits, but forwards NOTHING to clients (host.hpp catalogue). We
+ // hit-test the scene and call wlr_seat_pointer_notify_enter/motion/
+ // button/axis/frame, plus focus + grab handling. Grabs suppress the
+ // forward simply by not notifying while a move/resize is in flight.
+ sub_motion_ = host.subscribe(host.on_pointer_motion(),
+ [this](const kernel::PointerMotionEvent& e) {
+ process_pointer_motion(e.lx, e.ly, e.time_msec);
+ });
+ sub_button_ = host.subscribe(host.on_pointer_button(),
+ [this](const kernel::PointerButtonEvent& e) {
+ process_pointer_button(e);
+ });
+ sub_axis_ = host.subscribe(host.on_pointer_axis(),
+ [this](const kernel::PointerAxisEvent& e) {
+ process_pointer_axis(e);
+ });
+ sub_frame_ = host.subscribe(host.on_pointer_frame(),
+ [this] { wlr_seat_pointer_notify_frame(host_->seat()); });
+
+ // Touch (slice-2 parity): tap-to-focus + down/up/motion routing.
+ sub_touch_down_ = host.subscribe(host.on_touch_down(),
+ [this](const kernel::TouchDownEvent& e) {
+ process_touch_down(e);
+ });
+ sub_touch_motion_ = host.subscribe(host.on_touch_motion(),
+ [this](const kernel::TouchMotionEvent& e) {
+ process_touch_motion(e);
+ });
+ sub_touch_up_ = host.subscribe(host.on_touch_up(),
+ [this](const kernel::TouchUpEvent& e) {
+ process_touch_up(e);
+ });
+ sub_touch_frame_ = host.subscribe(host.on_touch_frame(),
+ [this] { wlr_seat_touch_notify_frame(host_->seat()); });
+
+ // Keyboard policy: consume Alt+Escape / Alt+F1, pass everything else.
+ sub_key_ = host.subscribe(host.key_filter(), [this](kernel::KeyEvent ev) {
+ return filter_key(ev);
+ });
+
+ // Register the typed service so downstream slices link against us.
+ host.provide_service<Service>(&service_);
+
+ activated_ = true;
+ }
+
+ // Probe used by the headless integration test to confirm activation ran.
+ [[nodiscard]] auto activated() const -> bool override { return activated_; }
+
+ // ---- exported hooks (also reached through service_) ----
+ [[nodiscard]] auto on_mapped() -> kernel::Event<const ToplevelEvent&>& { return on_mapped_; }
+ [[nodiscard]] auto on_unmapped() -> kernel::Event<const ToplevelEvent&>& {
+ return on_unmapped_;
+ }
+ [[nodiscard]] auto on_focused() -> kernel::Event<const ToplevelEvent&>& { return on_focused_; }
+
+ // ---- focus / hit-test (used by ToplevelEntry::focus and the glue) ----
+ void focus_toplevel(ToplevelEntry* entry) {
+ if (entry == nullptr || !entry->mapped) {
+ return;
+ }
+ wlr_seat* seat = host_->seat();
+ wlr_surface* surface = entry->xdg_toplevel->base->surface;
+ wlr_surface* prev = seat->keyboard_state.focused_surface;
+ if (prev == surface) {
+ return;
+ }
+ if (prev != nullptr) {
+ if (wlr_xdg_toplevel* p = wlr_xdg_toplevel_try_from_wlr_surface(prev)) {
+ wlr_xdg_toplevel_set_activated(p, false);
+ }
+ }
+ wlr_scene_node_raise_to_top(&entry->scene_tree->node);
+
+ // Move to front of focus order (front = focused).
+ focus_order_.remove(entry);
+ focus_order_.push_front(entry);
+
+ wlr_xdg_toplevel_set_activated(entry->xdg_toplevel, true);
+ if (wlr_keyboard* kb = wlr_seat_get_keyboard(seat)) {
+ wlr_seat_keyboard_notify_enter(seat, surface, kb->keycodes, kb->num_keycodes,
+ &kb->modifiers);
+ }
+ emit(on_focused_, entry);
+ }
+
+private:
+ // ---- toplevel lifecycle ----
+ void handle_new_toplevel(wlr_xdg_toplevel* xdg_toplevel) {
+ auto owned = std::make_unique<ToplevelEntry>();
+ ToplevelEntry* entry = owned.get();
+ entry->ext = this;
+ entry->xdg_toplevel = xdg_toplevel;
+ entry->scene_tree = 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;
+ // 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);
+ toplevels_.emplace(xdg_toplevel, std::move(owned));
+
+ entry->map.connect(xdg_toplevel->base->surface->events.map, [this, entry](void*) {
+ entry->mapped = true;
+ focus_order_.push_front(entry);
+ focus_toplevel(entry);
+ emit(on_mapped_, entry);
+ });
+ entry->unmap.connect(xdg_toplevel->base->surface->events.unmap, [this, entry](void*) {
+ emit(on_unmapped_, entry);
+ if (entry == grabbed_) {
+ grab_.on_grab_target_lost();
+ end_grab();
+ }
+ entry->mapped = false;
+ focus_order_.remove(entry);
+ });
+ entry->commit.connect(xdg_toplevel->base->surface->events.commit, [entry](void*) {
+ if (entry->xdg_toplevel->base->initial_commit) {
+ // 0x0 configure: the client picks its own dimensions (tinywl).
+ wlr_xdg_toplevel_set_size(entry->xdg_toplevel, 0, 0);
+ }
+ });
+ entry->destroy.connect(xdg_toplevel->events.destroy, [this, entry](void*) {
+ // Last action: erases `entry` (and its Listeners with it).
+ toplevels_.erase(entry->xdg_toplevel);
+ });
+ entry->request_move.connect(xdg_toplevel->events.request_move, [this, entry](void*) {
+ begin_move(entry);
+ });
+ entry->request_resize.connect(xdg_toplevel->events.request_resize,
+ [this, entry](void* data) {
+ const auto* ev =
+ static_cast<wlr_xdg_toplevel_resize_event*>(data);
+ begin_resize(entry, ev->edges);
+ });
+ entry->request_maximize.connect(xdg_toplevel->events.request_maximize, [entry](void*) {
+ // Unsupported, but xdg-shell demands a configure reply.
+ if (entry->xdg_toplevel->base->initialized) {
+ wlr_xdg_surface_schedule_configure(entry->xdg_toplevel->base);
+ }
+ });
+ entry->request_fullscreen.connect(xdg_toplevel->events.request_fullscreen,
+ [entry](void*) {
+ if (entry->xdg_toplevel->base->initialized) {
+ wlr_xdg_surface_schedule_configure(
+ entry->xdg_toplevel->base);
+ }
+ });
+ }
+
+ // ---- popup lifecycle (typed surface->tree registry; .data convention dead)
+ void handle_new_popup(wlr_xdg_popup* xdg_popup) {
+ auto owned = std::make_unique<PopupEntry>();
+ PopupEntry* popup = owned.get();
+ popup->xdg_popup = xdg_popup;
+ popups_.emplace(xdg_popup, std::move(owned));
+
+ // Parent scene tree resolved through the typed kernel contract — works
+ // uniformly for an xdg parent (our toplevel/ancestor popup) AND a
+ // non-xdg parent (a layer surface registered by ext-layer-shell). No
+ // wlr_surface.data / wlr_xdg_surface.data agreement anymore.
+ wlr_scene_tree* parent_tree =
+ xdg_popup->parent != nullptr ? host_->scene_tree_for(xdg_popup->parent) : nullptr;
+ if (parent_tree != nullptr) {
+ wlr_scene_tree* popup_tree =
+ wlr_scene_xdg_surface_create(parent_tree, xdg_popup->base);
+ // Register this popup's surface so a nested popup parented to it can
+ // resolve through the same typed contract.
+ popup->surface_reg =
+ host_->host_surface(xdg_popup->base->surface, popup_tree);
+ }
+
+ popup->commit.connect(xdg_popup->base->surface->events.commit, [popup](void*) {
+ if (popup->xdg_popup->base->initial_commit) {
+ wlr_xdg_surface_schedule_configure(popup->xdg_popup->base);
+ }
+ });
+ popup->destroy.connect(xdg_popup->events.destroy, [this, popup](void*) {
+ // Last action: erases `popup` (and its Listeners with it).
+ popups_.erase(popup->xdg_popup);
+ });
+ }
+
+ // ---- scene hit-test (mirrors the former kernel toplevel_at) ----
+ auto toplevel_at(double lx, double ly, wlr_surface** surface, double* sx, double* sy)
+ -> ToplevelEntry* {
+ wlr_scene_node* node = wlr_scene_node_at(&host_->scene()->tree.node, lx, ly, sx, sy);
+ if (node == nullptr || node->type != WLR_SCENE_NODE_BUFFER) {
+ return nullptr;
+ }
+ wlr_scene_buffer* scene_buffer = wlr_scene_buffer_from_node(node);
+ wlr_scene_surface* scene_surface = wlr_scene_surface_try_from_buffer(scene_buffer);
+ if (scene_surface == nullptr) {
+ return nullptr;
+ }
+ *surface = scene_surface->surface;
+ // Walk up to the tree whose data we set: the ToplevelEntry root.
+ wlr_scene_tree* tree = node->parent;
+ while (tree != nullptr && tree->node.data == nullptr) {
+ tree = tree->node.parent;
+ }
+ if (tree == nullptr) {
+ return nullptr;
+ }
+ // Only our toplevel trees carry a ToplevelEntry* (popups carry a
+ // wlr_scene_tree* in base->data, not node.data, so they read null and
+ // are skipped here — same as the former kernel).
+ return static_cast<ToplevelEntry*>(tree->node.data);
+ }
+
+ // ---- interactive grabs ----
+ // Tear down the grab: clear the grab target and restore the default cursor.
+ // Called when the pure GrabMachine reports the grab ended (button release)
+ // or the grabbed toplevel vanished.
+ void end_grab() {
+ grabbed_ = nullptr;
+ wlr_cursor_set_xcursor(host_->cursor(), host_->cursor_manager(), "default");
+ }
+
+ // The client requested an interactive move. The pure machine decides
+ // whether a grab actually engages (only while the triggering button is
+ // held — a request with no button down is ignored, which is the fix for
+ // the "drags without clicking after release" bug). Geometry is captured
+ // only when the grab truly engages.
+ void begin_move(ToplevelEntry* entry) {
+ if (!grab_.on_request_move()) {
+ return; // no button held: do not start an unclicked drag
+ }
+ wlr_cursor* cursor = host_->cursor();
+ grabbed_ = entry;
+ grab_x_ = cursor->x - entry->scene_tree->node.x;
+ grab_y_ = cursor->y - entry->scene_tree->node.y;
+ }
+
+ void begin_resize(ToplevelEntry* entry, std::uint32_t edges) {
+ if (!grab_.on_request_resize()) {
+ return;
+ }
+ wlr_cursor* cursor = host_->cursor();
+ grabbed_ = entry;
+ wlr_box* geo = &entry->xdg_toplevel->base->geometry;
+ const double border_x = (entry->scene_tree->node.x + geo->x) +
+ ((edges & WLR_EDGE_RIGHT) != 0 ? geo->width : 0);
+ const double border_y = (entry->scene_tree->node.y + geo->y) +
+ ((edges & WLR_EDGE_BOTTOM) != 0 ? geo->height : 0);
+ grab_x_ = cursor->x - border_x;
+ grab_y_ = cursor->y - border_y;
+ grab_geobox_ = *geo;
+ grab_geobox_.x += entry->scene_tree->node.x;
+ grab_geobox_.y += entry->scene_tree->node.y;
+ resize_edges_ = edges;
+ }
+
+ void process_cursor_move() {
+ wlr_cursor* cursor = host_->cursor();
+ wlr_scene_node_set_position(&grabbed_->scene_tree->node,
+ static_cast<int>(cursor->x - grab_x_),
+ static_cast<int>(cursor->y - grab_y_));
+ }
+
+ void process_cursor_resize() {
+ wlr_cursor* cursor = host_->cursor();
+ const double border_x = cursor->x - grab_x_;
+ const double border_y = cursor->y - grab_y_;
+ int new_left = grab_geobox_.x;
+ int new_right = grab_geobox_.x + grab_geobox_.width;
+ int new_top = grab_geobox_.y;
+ int new_bottom = grab_geobox_.y + grab_geobox_.height;
+
+ if ((resize_edges_ & WLR_EDGE_TOP) != 0) {
+ new_top = static_cast<int>(border_y);
+ if (new_top >= new_bottom) {
+ new_top = new_bottom - 1;
+ }
+ } else if ((resize_edges_ & WLR_EDGE_BOTTOM) != 0) {
+ new_bottom = static_cast<int>(border_y);
+ if (new_bottom <= new_top) {
+ new_bottom = new_top + 1;
+ }
+ }
+ if ((resize_edges_ & WLR_EDGE_LEFT) != 0) {
+ new_left = static_cast<int>(border_x);
+ if (new_left >= new_right) {
+ new_left = new_right - 1;
+ }
+ } else if ((resize_edges_ & WLR_EDGE_RIGHT) != 0) {
+ new_right = static_cast<int>(border_x);
+ if (new_right <= new_left) {
+ new_right = new_left + 1;
+ }
+ }
+ wlr_box* geo = &grabbed_->xdg_toplevel->base->geometry;
+ wlr_scene_node_set_position(&grabbed_->scene_tree->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);
+ }
+
+ // ---- pointer routing ----
+ void process_pointer_motion(double lx, double ly, std::uint32_t time_msec) {
+ switch (grab_.on_motion()) {
+ case policy::GrabAction::move_toplevel:
+ process_cursor_move();
+ return; // suppress client passthrough during a grab
+ case policy::GrabAction::resize_toplevel:
+ process_cursor_resize();
+ return;
+ case policy::GrabAction::end_grab:
+ case policy::GrabAction::none:
+ break; // passthrough below
+ }
+ double sx = 0;
+ double sy = 0;
+ wlr_surface* surface = nullptr;
+ ToplevelEntry* entry = toplevel_at(lx, ly, &surface, &sx, &sy);
+ if (entry == nullptr) {
+ // Over nothing: draw the default cursor ourselves.
+ wlr_cursor_set_xcursor(host_->cursor(), host_->cursor_manager(), "default");
+ }
+ wlr_seat* seat = host_->seat();
+ if (surface != nullptr) {
+ wlr_seat_pointer_notify_enter(seat, surface, sx, sy);
+ wlr_seat_pointer_notify_motion(seat, time_msec, sx, sy);
+ } else {
+ wlr_seat_pointer_clear_focus(seat);
+ }
+ }
+
+ void process_pointer_button(const kernel::PointerButtonEvent& e) {
+ // Forwarding the button to the focused client is OURS (host.hpp: the
+ // kernel forwards nothing). Feed the press/release to the pure grab
+ // machine FIRST: it tracks button-down (so a later request_move only
+ // grabs while held) and reports when a release ends a grab.
+ const bool was_grabbing = grab_.grabbing();
+ const policy::GrabAction action = grab_.on_button(e.pressed);
+ if (action == policy::GrabAction::end_grab) {
+ end_grab(); // release ended the grab; consume it, no client notify
+ return;
+ }
+ if (was_grabbing) {
+ return; // still grabbing (shouldn't happen on a button, but be safe)
+ }
+
+ const auto state =
+ e.pressed ? WL_POINTER_BUTTON_STATE_PRESSED : WL_POINTER_BUTTON_STATE_RELEASED;
+ wlr_seat_pointer_notify_button(host_->seat(), e.time_msec, e.button, state);
+ if (e.pressed) {
+ // Click-to-focus on press.
+ double sx = 0;
+ double sy = 0;
+ wlr_surface* surface = nullptr;
+ focus_toplevel(toplevel_at(e.lx, e.ly, &surface, &sx, &sy));
+ }
+ }
+
+ void process_pointer_axis(const kernel::PointerAxisEvent& e) {
+ // Scroll forwarding is OURS (host.hpp: kernel forwards nothing).
+ // Suppress during a grab, consistent with button/motion.
+ if (grab_.grabbing()) {
+ return;
+ }
+ wlr_seat_pointer_notify_axis(host_->seat(), e.time_msec, e.orientation, e.delta,
+ e.delta_discrete, e.source,
+ WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL);
+ }
+
+ // ---- touch routing (slice-2 parity) ----
+ void process_touch_down(const kernel::TouchDownEvent& e) {
+ double sx = 0;
+ double sy = 0;
+ wlr_surface* surface = nullptr;
+ ToplevelEntry* entry = toplevel_at(e.lx, e.ly, &surface, &sx, &sy);
+ if (entry != nullptr) {
+ focus_toplevel(entry); // tap raises + focuses
+ }
+ if (surface != nullptr) {
+ // Record the surface's layout origin (lx - sx) so motion can derive
+ // surface-local coords. Known layout-origin-during-grab skew is
+ // accepted until slice 5 (see package doc).
+ touch_points_.insert_or_assign(e.touch_id,
+ TouchPoint{surface, e.lx - sx, e.ly - sy});
+ wlr_seat_touch_notify_down(host_->seat(), surface, e.time_msec, e.touch_id, sx, sy);
+ }
+ }
+
+ void process_touch_motion(const kernel::TouchMotionEvent& e) {
+ auto it = touch_points_.find(e.touch_id);
+ if (it == touch_points_.end()) {
+ return; // down landed on no surface; nothing grabbed
+ }
+ wlr_seat_touch_notify_motion(host_->seat(), e.time_msec, e.touch_id,
+ e.lx - it->second.origin_x, e.ly - it->second.origin_y);
+ }
+
+ void process_touch_up(const kernel::TouchUpEvent& e) {
+ touch_points_.erase(e.touch_id);
+ wlr_seat_touch_notify_up(host_->seat(), e.time_msec, e.touch_id);
+ }
+
+ // ---- keyboard policy (consume-or-pass filter) ----
+ auto filter_key(kernel::KeyEvent ev) -> kernel::KeyEvent {
+ if (ev.handled) {
+ return ev; // an earlier link already consumed it
+ }
+ switch (policy::match_keybinding(ev.keysym, ev.modifiers, ev.pressed)) {
+ case policy::KeyAction::terminate:
+ wl_display_terminate(host_->display());
+ ev.handled = true;
+ break;
+ case policy::KeyAction::cycle_focus: {
+ const std::size_t idx = policy::cycle_next(focus_order_.size());
+ if (idx != policy::no_selection) {
+ auto it = focus_order_.begin();
+ std::advance(it, idx);
+ focus_toplevel(*it);
+ }
+ ev.handled = true; // consume the binding key even with <2 windows
+ break;
+ }
+ case policy::KeyAction::none:
+ break;
+ }
+ return ev;
+ }
+
+ void emit(kernel::Event<const ToplevelEvent&>& ev, ToplevelEntry* entry) {
+ const ToplevelEvent payload{entry};
+ ev.emit(payload);
+ }
+
+ // ---- the Service the public header exposes ----
+ struct ServiceImpl final : Service {
+ XdgShellExtension* ext = nullptr;
+ [[nodiscard]] auto on_toplevel_mapped()
+ -> kernel::Event<const ToplevelEvent&>& override {
+ return ext->on_mapped();
+ }
+ [[nodiscard]] auto on_toplevel_unmapped()
+ -> kernel::Event<const ToplevelEvent&>& override {
+ return ext->on_unmapped();
+ }
+ [[nodiscard]] auto on_toplevel_focused()
+ -> kernel::Event<const ToplevelEvent&>& override {
+ return ext->on_focused();
+ }
+ };
+
+ struct TouchPoint {
+ wlr_surface* surface = nullptr;
+ double origin_x = 0.0;
+ double origin_y = 0.0;
+ };
+
+ kernel::Manifest manifest_;
+ Host* host_ = nullptr;
+ wlr_xdg_shell* xdg_shell_ = nullptr;
+ bool activated_ = false;
+
+ // Exported hooks (adopt()ed; stable members — never moved).
+ kernel::Event<const ToplevelEvent&> on_mapped_;
+ kernel::Event<const ToplevelEvent&> on_unmapped_;
+ kernel::Event<const ToplevelEvent&> on_focused_;
+ ServiceImpl service_{};
+
+ // Window/popup ownership.
+ std::unordered_map<wlr_xdg_toplevel*, std::unique_ptr<ToplevelEntry>> toplevels_;
+ std::unordered_map<wlr_xdg_popup*, std::unique_ptr<PopupEntry>> popups_;
+ // Focus order: front = focused; MAPPED toplevels only.
+ std::list<ToplevelEntry*> focus_order_;
+
+ // Grab state (one at a time). The pure machine owns the move/resize/none
+ // mode + button-down tracking; these hold the geometry for the active grab.
+ policy::GrabMachine grab_;
+ ToplevelEntry* grabbed_ = nullptr;
+ double grab_x_ = 0.0;
+ double grab_y_ = 0.0;
+ wlr_box grab_geobox_{};
+ std::uint32_t resize_edges_ = 0;
+
+ // Touch implicit grabs (touch_id -> origin surface + layout origin).
+ std::unordered_map<std::int32_t, TouchPoint> touch_points_;
+
+ // Raw xdg-shell signal listeners (RAII).
+ Listener new_toplevel_;
+ Listener new_popup_;
+
+ // Kernel-catalogue subscriptions (RAII; dropped on destruction).
+ Subscription sub_motion_;
+ Subscription sub_button_;
+ Subscription sub_axis_;
+ Subscription sub_frame_;
+ Subscription sub_touch_down_;
+ Subscription sub_touch_motion_;
+ Subscription sub_touch_up_;
+ Subscription sub_touch_frame_;
+ Subscription sub_key_;
+
+ friend struct ServiceImpl;
+};
+
+void ToplevelEntry::focus() { ext->focus_toplevel(this); }
+
+} // namespace
+
+auto make_extension() -> std::unique_ptr<kernel::Extension> {
+ return std::make_unique<XdgShellExtension>();
+}
+
+auto make_extension_with_probe() -> ExtensionWithProbe {
+ auto ext = std::make_unique<XdgShellExtension>();
+ ActivationProbe* probe = ext.get();
+ return {std::move(ext), probe};
+}
+
+} // namespace unbox::ext_xdg_shell