diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 22:44:16 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 22:44:16 +0900 |
| commit | c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (patch) | |
| tree | f6dea2875b939c0f661292d8bfa0d79a96fe67d7 /packages/ext-layer-shell/src/ext_layer_shell.cpp | |
| parent | 6949c3582ed1e480e70aabfcfa3a11b78007cc12 (diff) | |
| download | unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.tar.gz unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.zip | |
Slice 4: extension host + typed bus; xdg-shell/layer-shell extracted to core extensions
The kernel now names NO concrete feature. It owns: the extension host
(install/topological activate, missing-dep/cycle = startup error), the
typed Event/Filter bus (error-isolated: a throwing extension is disabled,
never the session; RAII Subscriptions), the Host API (per-extension
facade: borrows, scene layers, event catalogue, typed services), the
public RAII Listener, and a typed surface→scene-tree registry
(Host::host_surface/scene_tree_for) that replaced the untyped
wlr_surface.data convention both extensions flagged.
- ext-xdg-shell (core): toplevel/popup lifecycle, focus-on-map,
click/tap-to-focus, pointer/touch routing incl. button+axis (the
kernel only moves the cursor and emits — a contract-doc lie caught by
user hands-on), interactive move/resize via pure GrabMachine (fixes
the request-arrives-after-release race: grab requires request ∧
button-down, release always ends it), Alt+F1 cycle,
Ctrl+Alt+Backspace terminate (labwc's default A-Escape=Exit killed
the dev session once — never again; see nested-run skill).
- ext-layer-shell (core): wlr-layer-shell v1 (proto v5) for external
clients; pure doctest-hard arrangement core; fuzzel verified visually
nested (fix: seed outputs from output_layout at activate — events-only
tracking missed pre-activation outputs; plus a scene-node double-free).
- First protocol codegen: vendored wlr-layer-shell XML + wayland-scanner
server-header propagated through kernel_dep; wlr.hpp grew a
namespace→_namespace keyword fix for the generated header.
- Glossary: 'scene layer' (user-approved). New rules earned:
parallel-wave-builds, contract-docs.
- User hands-on verified: typing, click-to-focus, drag-select, scroll,
titlebar drag-move (slow + flick), Alt+F1, fuzzel + arrows, touch tap,
Ctrl+Alt+Backspace. 68 doctest cases green, ASan/UBSan clean (our
code), idle RSS ≈73 MiB.
Diffstat (limited to 'packages/ext-layer-shell/src/ext_layer_shell.cpp')
| -rw-r--r-- | packages/ext-layer-shell/src/ext_layer_shell.cpp | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/packages/ext-layer-shell/src/ext_layer_shell.cpp b/packages/ext-layer-shell/src/ext_layer_shell.cpp new file mode 100644 index 0000000..89a0012 --- /dev/null +++ b/packages/ext-layer-shell/src/ext_layer_shell.cpp @@ -0,0 +1,370 @@ +#include <unbox/ext-layer-shell/ext_layer_shell.hpp> + +#include <unbox/ext-layer-shell/arrangement.hpp> +#include <unbox/kernel/host.hpp> +#include <unbox/kernel/listener.hpp> +#include <unbox/kernel/wlr.hpp> + +#include <algorithm> +#include <memory> +#include <stdexcept> +#include <vector> + +// 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<wlr_layer_surface_v1*>(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<LayerSurface>& 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<LayerSurface>(*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>(); + 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<Pending>& 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<wlr_layer_surface_v1*> 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<wlr_layer_surface_v1*> 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<UsableEntry> 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<wlr_output*> outputs_; // borrows; tracked set + UsableAreas usable_areas_; // per-output mirror + std::vector<std::unique_ptr<Pending>> pending_; // owned; pre-output + std::vector<std::unique_ptr<LayerSurface>> 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<kernel::Extension> { + return std::make_unique<LayerShellExt>(); +} + +} // namespace unbox::ext_layer_shell |
