#include #include #include #include #include #include #include #include #include // ext-layer-shell glue. The decision core (which edge a surface reserves, how // the usable area shrinks) lives in arrangement.hpp and is exercised without // wlroots; THIS file is the thin effectful edge that binds wlroots signals and // drives wlr_scene_layer_surface_v1_configure. The usable-area model in // arrangement.hpp mirrors what that helper mutates — we keep our own per-output // copy for bookkeeping and as the basis tiling (slice 7) will consume. namespace unbox::ext_layer_shell { namespace { using kernel::Host; using kernel::Listener; // Map a protocol layer to its kernel SceneLayer band. background/bottom/top/ // overlay map 1:1; `normal` is toplevels-only and never a layer-shell band. auto band_for_layer(enum zwlr_layer_shell_v1_layer layer) -> kernel::SceneLayer { switch (layer) { case ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND: return kernel::SceneLayer::background; case ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM: return kernel::SceneLayer::bottom; case ZWLR_LAYER_SHELL_V1_LAYER_TOP: return kernel::SceneLayer::top; case ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY: return kernel::SceneLayer::overlay; } return kernel::SceneLayer::top; // unreachable: protocol validates the enum } class LayerShellExt; // One live layer surface: its scene node and the wlroots signal bindings. // Owned (unique_ptr) by LayerShellExt; destroyed when the wlroots layer surface // is destroyed (its own destroy handler erases it from the owner) or at // shutdown (reverse-declaration teardown of the owning extension). class LayerSurface { public: LayerSurface(LayerShellExt& owner, wlr_layer_surface_v1* surface, wlr_scene_layer_surface_v1* scene); [[nodiscard]] auto wlr() const -> wlr_layer_surface_v1* { return surface_; } [[nodiscard]] auto scene() const -> wlr_scene_layer_surface_v1* { return scene_; } [[nodiscard]] auto output() const -> wlr_output* { return surface_->output; } private: void update_keyboard_focus(); LayerShellExt& owner_; wlr_layer_surface_v1* surface_; // borrow; kernel/wlroots-owned wlr_scene_layer_surface_v1* scene_; // owned node, destroyed via destroy_ // Typed surface->scene-tree association (replaces the dead wlr_surface.data // convention): lets ext-xdg-shell resolve popup parents via // Host::scene_tree_for(). Declared AFTER scene_ so it tears down (releasing // the map entry) before the node reference goes — reverse-declaration order. kernel::SurfaceRegistration host_reg_; Listener commit_; Listener destroy_; Listener new_popup_; }; class LayerShellExt final : public kernel::Extension { public: LayerShellExt() = default; auto manifest() const -> const kernel::Manifest& override { return manifest_; } void activate(Host& host) override { host_ = &host; // The global: version 5 (the vendored protocol XML / wlroots 0.20 cap). shell_ = wlr_layer_shell_v1_create(host.display(), 5); if (shell_ == nullptr) { throw std::runtime_error( "ext-layer-shell: wlr_layer_shell_v1_create failed"); } new_surface_.connect(shell_->events.new_surface, [this](void* data) { on_new_surface(static_cast(data)); }); // Track outputs: assign one to outputless surfaces and re-arrange when // the output set changes. output_added_ = host.subscribe( host.on_output_added(), [this](const kernel::OutputEvent& e) { on_output_added(e.output); }); output_removed_ = host.subscribe( host.on_output_removed(), [this](const kernel::OutputEvent& e) { on_output_removed(e.output); }); // Seed outputs that ALREADY EXIST at activation. Server::create() starts // the backend, so a nested/headless output is added during create() — // BEFORE extensions activate at run(). on_output_added fires only for // outputs added AFTER we subscribe above, so events-only tracking would // miss the pre-existing one forever: an output-less layer surface (e.g. // fuzzel, which passes nil output) would then get no output assigned, no // wlr_scene_layer_surface_v1_configure, no configure event, and we'd // close it. wlr_output_layout retains already-added outputs, so we // enumerate them here. (Contract gap noted in the report: late // subscribers miss kernel state; the kernel could replay or expose an // outputs() borrow. Fixed within the unit with what exists today.) wlr_output_layout_output* lo = nullptr; wl_list_for_each(lo, &host.output_layout()->outputs, link) { track_output(lo->output); } } [[nodiscard]] auto host() -> Host& { return *host_; } // Recompute `output`'s usable area from every live surface on it and push a // configure to each. Called on commit and on output add/remove/change. void arrange(wlr_output* output) { if (output == nullptr || host_ == nullptr) { return; } wlr_box full{}; wlr_output_layout_get_box(host_->output_layout(), output, &full); if (wlr_box_empty(&full)) { full = {.x = 0, .y = 0, .width = output->width, .height = output->height}; } wlr_box usable = full; for (const auto& ls : surfaces_) { if (ls->output() == output) { wlr_scene_layer_surface_v1_configure(ls->scene(), &full, &usable); } } // Mirror the remaining usable area into our pure-core box (the basis a // usable-area service would publish; deliberately not exported as a // hook yet — see the report). usable_area(output) = Box{usable.x, usable.y, usable.width, usable.height}; } // Drop a surface from the owned set (called from its destroy handler as the // LAST action — this deletes the LayerSurface). void erase(LayerSurface* which) { std::erase_if(surfaces_, [which](const std::unique_ptr& p) { return p.get() == which; }); } private: void on_new_surface(wlr_layer_surface_v1* surface) { // Assign an output if the client did not request one (fuzzel passes // nil): fall back to the first tracked output. If NO output exists yet, // defer placement instead of closing the surface — wlroots requires an // output set before the first configure, and the protocol guarantees // the compositor will EVENTUALLY configure an unmapped surface. The // surface is parked in pending_ and placed when an output appears. if (surface->output == nullptr && outputs_.empty()) { park_pending(surface); return; } if (surface->output == nullptr) { surface->output = outputs_.front(); } place(surface); } // Create the scene node and the live LayerSurface for `surface` (which now // has an output assigned). Its commit listener drives the first configure. void place(wlr_layer_surface_v1* surface) { wlr_scene_tree* parent = host_->scene_layer(band_for_layer(surface->current.layer)); wlr_scene_layer_surface_v1* scene = wlr_scene_layer_surface_v1_create(parent, surface); if (scene == nullptr) { wlr_layer_surface_v1_destroy(surface); return; } // The LayerSurface registers surface->tree via Host::host_surface() in // its constructor (the typed surface->scene-tree contract), so xdg // popups parented to this layer surface resolve through // Host::scene_tree_for() — no wlr_surface.data write here. surfaces_.push_back(std::make_unique(*this, surface, scene)); } // Park an output-less surface that arrived before any output exists. We // hold only a destroy listener so a client that gives up before an output // appears is dropped cleanly; placement happens in adopt_pending_surfaces. void park_pending(wlr_layer_surface_v1* surface) { auto pending = std::make_unique(); pending->surface = surface; Pending* raw = pending.get(); pending->destroy.connect(surface->events.destroy, [this, raw](void*) { std::erase_if(pending_, [raw](const std::unique_ptr& p) { return p.get() == raw; // LAST action: deletes the Pending }); }); pending_.push_back(std::move(pending)); } // An output appeared: assign it to every parked surface and place them. void adopt_pending_surfaces(wlr_output* output) { if (pending_.empty()) { return; } // Move the parked surfaces out first: place() binds a fresh destroy // listener via LayerSurface, so the Pending's destroy listener must be // gone before placement to avoid a double binding. std::vector ready; for (auto& p : pending_) { p->surface->output = output; ready.push_back(p->surface); } pending_.clear(); // drops the parking destroy listeners for (wlr_layer_surface_v1* s : ready) { place(s); } } // Start tracking `output` (idempotent — seeding at activate and a later // on_output_added for the same output must not double-insert). Re-arranges // the output so any surface already assigned to it (or pending placement) // gets configured once it exists. void track_output(wlr_output* output) { if (output == nullptr) { return; } if (std::find(outputs_.begin(), outputs_.end(), output) != outputs_.end()) { return; } outputs_.push_back(output); adopt_pending_surfaces(output); arrange(output); } void on_output_added(wlr_output* output) { track_output(output); } void on_output_removed(wlr_output* output) { std::erase(outputs_, output); usable_areas_.erase_output(output); // Surfaces on a vanished output: close them. Snapshot first since each // destroy handler erases from surfaces_. std::vector doomed; for (const auto& ls : surfaces_) { if (ls->output() == output) { doomed.push_back(ls->wlr()); } } for (wlr_layer_surface_v1* s : doomed) { wlr_layer_surface_v1_destroy(s); } } // Per-output usable area (our pure-core mirror). N == #outputs (tiny); a // flat pointer-keyed vector keeps the public header wlroots-free. struct UsableEntry { wlr_output* output; Box area; }; auto usable_area(wlr_output* o) -> Box& { for (auto& e : usable_areas_.entries) { if (e.output == o) { return e.area; } } usable_areas_.entries.push_back({o, Box{}}); return usable_areas_.entries.back().area; } struct UsableAreas { std::vector entries; void erase_output(wlr_output* o) { std::erase_if(entries, [o](const UsableEntry& e) { return e.output == o; }); } }; const kernel::Manifest manifest_{ .id = "layer-shell", .tier = kernel::Tier::core, .depends_on = {}}; Host* host_ = nullptr; wlr_layer_shell_v1* shell_ = nullptr; Listener new_surface_; kernel::Subscription output_added_; kernel::Subscription output_removed_; // A layer surface that arrived before any output existed. Held with only a // destroy listener until an output appears (adopt_pending_surfaces), then // placed. Owned; teardown drops the listener. struct Pending { wlr_layer_surface_v1* surface = nullptr; // borrow Listener destroy; }; std::vector outputs_; // borrows; tracked set UsableAreas usable_areas_; // per-output mirror std::vector> pending_; // owned; pre-output std::vector> surfaces_; // owned }; // ---- LayerSurface ---------------------------------------------------------- LayerSurface::LayerSurface(LayerShellExt& owner, wlr_layer_surface_v1* surface, wlr_scene_layer_surface_v1* scene) : owner_(owner), surface_(surface), scene_(scene) { // Publish the typed surface->scene-tree association so xdg popups parented // to this layer surface resolve to our tree via Host::scene_tree_for(). The // RAII handle is a member; it unregisters when this LayerSurface dies. host_reg_ = owner_.host().host_surface(surface_->surface, scene_->tree); // Re-arrange this surface's output on every commit (covers the mandatory // initial-commit configure and any later anchor/zone/size change), then // re-evaluate keyboard focus. commit_.connect(surface_->surface->events.commit, [this](void*) { owner_.arrange(surface_->output); update_keyboard_focus(); }); // The wlroots layer surface is about to be freed. Do NOT destroy the scene // node here: wlr_scene_layer_surface_v1 installs its OWN layer_surface // destroy listener that tears the scene tree down for us (confirmed by ASan // — touching scene_->tree here is a use-after-free, since signal-emit order // between our listener and wlroots' is unspecified). We only reclaim the // area the surface reserved and erase ourselves. Copy what we need into // locals FIRST: erase(this) deletes *this, after which no member (including // owner_) may be touched, so we re-arrange through a captured owner ref. destroy_.connect(surface_->events.destroy, [this](void*) { LayerShellExt& owner = owner_; wlr_output* out = surface_->output; owner.erase(this); // deletes *this — LAST use of any member owner.arrange(out); }); // Popups parented to this layer surface resolve to our scene tree via the // typed Host surface registration (host_reg_ above); ext-xdg-shell looks it // up with Host::scene_tree_for() and wlroots' scene helper wires the popup // nodes. Bound for completeness; no extra work here. new_popup_.connect(surface_->events.new_popup, [](void*) {}); } // Minimal v1 keyboard interactivity: focus an `exclusive` surface once mapped; // leave `none` alone. `on_demand` is deferred to slice 5's input routing. void LayerSurface::update_keyboard_focus() { if (!surface_->surface->mapped) { return; } if (surface_->current.keyboard_interactive != ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) { return; } wlr_seat* seat = owner_.host().seat(); wlr_keyboard* kbd = wlr_seat_get_keyboard(seat); if (kbd != nullptr) { wlr_seat_keyboard_notify_enter(seat, surface_->surface, kbd->keycodes, kbd->num_keycodes, &kbd->modifiers); } else { wlr_seat_keyboard_notify_enter(seat, surface_->surface, nullptr, 0, nullptr); } } } // namespace auto create() -> std::unique_ptr { return std::make_unique(); } } // namespace unbox::ext_layer_shell