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/include | |
| 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/include')
| -rw-r--r-- | packages/ext-layer-shell/include/unbox/ext-layer-shell/arrangement.hpp | 170 | ||||
| -rw-r--r-- | packages/ext-layer-shell/include/unbox/ext-layer-shell/ext_layer_shell.hpp | 38 |
2 files changed, 208 insertions, 0 deletions
diff --git a/packages/ext-layer-shell/include/unbox/ext-layer-shell/arrangement.hpp b/packages/ext-layer-shell/include/unbox/ext-layer-shell/arrangement.hpp new file mode 100644 index 0000000..638739a --- /dev/null +++ b/packages/ext-layer-shell/include/unbox/ext-layer-shell/arrangement.hpp @@ -0,0 +1,170 @@ +#pragma once + +#include <cstdint> + +// Arrangement math — the PURE CORE of ext-layer-shell. Zero wlroots/GL/RMLUi +// types: a self-contained model of the wlr-layer-shell usable-area bookkeeping +// the compositor must keep as anchored, exclusive-zone surfaces are arranged on +// an output. Input -> output only; doctest-covered hard. wlroots' own +// wlr_scene_layer_surface_v1_configure performs the actual scene positioning +// and mutates a usable_area box identically; this model is the independent, +// testable mirror we keep for everything DOWNSTREAM of the scene helper (the +// per-output usable area tiling, slice 7, will consume). +// +// Coordinate convention matches wlr_box: x/y are the box's top-left in output- +// local pixels, growing right/down. All math is integer (pixel) arithmetic. +// +// No allocation, no threads, no I/O. Pure value types. + +namespace unbox::ext_layer_shell { + +// An axis-aligned integer pixel box (mirrors wlr_box, but wlroots-free so this +// header stays a pure contract). width/height are >= 0 for a valid box. +struct Box { + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t width = 0; + std::int32_t height = 0; + + friend constexpr auto operator==(const Box&, const Box&) -> bool = default; +}; + +// Anchor bitfield — the wlr-layer-shell v1 `anchor` enum values verbatim +// (top=1, bottom=2, left=4, right=8), combinable. A surface anchored to two +// opposite edges spans that axis; anchored to all four it fills the output +// (minus margins). Kept as named constants so the pure core never needs the +// generated protocol header. +namespace anchor { +inline constexpr std::uint32_t top = 1; +inline constexpr std::uint32_t bottom = 2; +inline constexpr std::uint32_t left = 4; +inline constexpr std::uint32_t right = 8; +} // namespace anchor + +// Which single edge a positive exclusive zone reserves space on. `none` means +// the surface reserves nothing (its exclusive zone is non-positive, or its +// anchoring makes a positive zone meaningless per the protocol — see +// exclusive_edge() below). +enum class Edge : std::uint8_t { none, top, bottom, left, right }; + +// The subset of a layer surface's committed state the arrangement math needs. +// `exclusive_edge` is the protocol's optional explicit override (v5): when +// Edge::none the edge is deduced from `anchor`; otherwise it forces the edge a +// corner-anchored surface reserves on. Margins are per-edge insets from the +// anchored edges. +struct SurfaceState { + std::uint32_t anchor = 0; // OR of anchor::* bits + std::int32_t exclusive_zone = 0; // <0 = "stretch over"; 0 = avoid; >0 = reserve + std::int32_t margin_top = 0; + std::int32_t margin_right = 0; + std::int32_t margin_bottom = 0; + std::int32_t margin_left = 0; + Edge exclusive_edge = Edge::none; // explicit override; none = deduce +}; + +// The edge a positive exclusive zone is applied to, or Edge::none if the zone +// reserves nothing. Mirrors wlr_layer_surface_v1_get_exclusive_edge(): +// +// * a non-positive exclusive_zone reserves nothing; +// * an explicit exclusive_edge wins if it is one of the anchored edges; +// * otherwise the edge is deducible only when the surface is anchored to +// exactly one edge, OR to one edge plus its two perpendicular edges (a +// full-width/height strip). Anchoring to a bare corner, to two parallel +// edges, or to all four edges makes a positive zone meaningless -> none. +[[nodiscard]] constexpr auto exclusive_edge(const SurfaceState& s) -> Edge { + if (s.exclusive_zone <= 0) { + return Edge::none; + } + const bool t = (s.anchor & anchor::top) != 0; + const bool b = (s.anchor & anchor::bottom) != 0; + const bool l = (s.anchor & anchor::left) != 0; + const bool r = (s.anchor & anchor::right) != 0; + + // Candidate edge from anchoring: a strip is anchored to one edge and + // (optionally) both of the perpendicular edges, but NOT to the opposite + // edge. Anchoring to all four, or to two parallel edges, yields no edge. + Edge deduced = Edge::none; + if (t && !b && (l == r)) { + deduced = Edge::top; + } else if (b && !t && (l == r)) { + deduced = Edge::bottom; + } else if (l && !r && (t == b)) { + deduced = Edge::left; + } else if (r && !l && (t == b)) { + deduced = Edge::right; + } + + if (s.exclusive_edge != Edge::none) { + // Honor the explicit override only if it is an edge the surface is + // actually anchored to (the protocol raises invalid_exclusive_edge + // otherwise; wlroots clamps — we treat a non-anchored override as a + // no-op deduction so a misbehaving client cannot warp the usable area). + const bool anchored = + (s.exclusive_edge == Edge::top && t) || + (s.exclusive_edge == Edge::bottom && b) || + (s.exclusive_edge == Edge::left && l) || + (s.exclusive_edge == Edge::right && r); + // Still require the override to be on a strip (deduced != none) or a + // corner where the override disambiguates a single anchored edge. + if (anchored) { + return s.exclusive_edge; + } + return Edge::none; + } + return deduced; +} + +// Reduce `usable` by the space ONE surface reserves, returning the smaller box +// the NEXT lower surface in the same arrangement pass may use. Mirrors the +// usable_area mutation wlr_scene_layer_surface_v1_configure performs: +// +// * a non-reserving surface (exclusive_edge() == none) leaves `usable` as-is; +// * a reserving surface shrinks `usable` on its edge by +// (exclusive_zone + the margin on that edge), clamped so width/height never +// go negative. +// +// Apply this in protocol z-order (overlay first within a pass is irrelevant to +// the area; what matters is each surface sees the area left by those processed +// before it — drive the surfaces in a stable order and the result is the +// remaining usable area for non-exclusive content and for tiling). +[[nodiscard]] constexpr auto apply_exclusive(Box usable, const SurfaceState& s) -> Box { + const Edge edge = exclusive_edge(s); + if (edge == Edge::none) { + return usable; + } + switch (edge) { + case Edge::top: { + const std::int32_t d = s.exclusive_zone + s.margin_top; + usable.y += d; + usable.height -= d; + break; + } + case Edge::bottom: { + const std::int32_t d = s.exclusive_zone + s.margin_bottom; + usable.height -= d; + break; + } + case Edge::left: { + const std::int32_t d = s.exclusive_zone + s.margin_left; + usable.x += d; + usable.width -= d; + break; + } + case Edge::right: { + const std::int32_t d = s.exclusive_zone + s.margin_right; + usable.width -= d; + break; + } + case Edge::none: + break; + } + if (usable.width < 0) { + usable.width = 0; + } + if (usable.height < 0) { + usable.height = 0; + } + return usable; +} + +} // namespace unbox::ext_layer_shell diff --git a/packages/ext-layer-shell/include/unbox/ext-layer-shell/ext_layer_shell.hpp b/packages/ext-layer-shell/include/unbox/ext-layer-shell/ext_layer_shell.hpp new file mode 100644 index 0000000..3288d84 --- /dev/null +++ b/packages/ext-layer-shell/include/unbox/ext-layer-shell/ext_layer_shell.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include <unbox/kernel/extension.hpp> + +#include <memory> + +// ext-layer-shell — wlr-layer-shell-unstable-v1 (version 5) for EXTERNAL +// clients: panels, launchers, wallpapers, on-screen keyboards, and the +// crash-isolation escape hatch (notes/plan.md §2). unbox's OWN ui substrate +// does NOT go through layer-shell. +// +// Tier: core. Manifest id "layer-shell", no dependencies. The extension owns +// the wlr_layer_shell_v1 global (created on host.display() in activate()), maps +// each protocol layer 1:1 onto a kernel SceneLayer band (background/bottom/top/ +// overlay; `normal` is toplevels-only and never used here), and keeps each +// output's usable area up to date as anchored/exclusive surfaces come and go. +// +// This header is the unit's WHOLE cross-extension contract: a factory only. The +// arrangement math (anchors, margins, exclusive-zone accumulation) is the pure +// core in <unbox/ext-layer-shell/arrangement.hpp> — depend on THAT, not on this, +// if you only need the usable-area model (tiling, slice 7). +// +// Single wl_event_loop thread throughout. Ownership of the returned extension +// transfers to the caller (host-bin installs it into the Server). + +namespace unbox::ext_layer_shell { + +// Construct the extension. Cheap and side-effect free (per the Extension +// contract); ALL wiring — global creation, signal binding, output tracking — +// happens in activate(). host-bin installs the returned object via +// Server::install(); the kernel calls activate() in topological order. +// +// Ownership: unique_ptr = transfer to the caller. The object must outlive the +// session and is destroyed (RAII teardown of the global, listeners, and scene +// nodes) at shutdown. +[[nodiscard]] auto create() -> std::unique_ptr<kernel::Extension>; + +} // namespace unbox::ext_layer_shell |
