summaryrefslogtreecommitdiffhomepage
path: root/packages/ext-layer-shell/include
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 22:44:16 +0900
committerAdam Malczewski <[email protected]>2026-06-12 22:44:16 +0900
commitc102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (patch)
treef6dea2875b939c0f661292d8bfa0d79a96fe67d7 /packages/ext-layer-shell/include
parent6949c3582ed1e480e70aabfcfa3a11b78007cc12 (diff)
downloadunbox-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.hpp170
-rw-r--r--packages/ext-layer-shell/include/unbox/ext-layer-shell/ext_layer_shell.hpp38
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