summaryrefslogtreecommitdiffhomepage
path: root/packages/ext-layer-shell/ext-layer-shell.md
blob: 719199b5e86e0eb208e09f25fb1ac9b79cc4219a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# ext-layer-shell

wlr-layer-shell-unstable-v1 (protocol **version 5**, the wlroots-0.20 cap) for
**external** clients: panels, launchers, wallpapers, on-screen keyboards, and
the crash-isolation escape hatch from `notes/plan.md` §2. unbox's own RMLUi ui
substrate does **not** go through layer-shell — this protocol exists so foreign
processes can paint the desktop's edges.

Tier `core`, manifest id `layer-shell`, no dependencies. `activate(Host&)`
creates the `wlr_layer_shell_v1` global on `host.display()`.

## Why it exists
The kernel names no concrete protocol. Layer-shell is shell *policy* (which
edge a panel reserves, which z-band it lands in), so it is an extension. The
extension-creates-the-global split keeps the kernel featureless.

## Side-effect graph
- **Creates:** the `wlr_layer_shell_v1` global (one, at activation).
- **Subscribes (kernel events):** `on_output_added` / `on_output_removed` —
  to track the output set (assign one to outputless surfaces; re-arrange and
  evict on output loss). Plus a one-shot enumeration of already-existing outputs
  (`host.output_layout()->outputs`) at activate, since outputs predate
  activation (see Gotchas). Also `on_pointer_button` + `on_touch_down` for
  on_demand keyboard focus (see Keyboard interactivity).
- **Binds (wlroots signals, via RAII `Listener`):** shell `new_surface`; per
  surface its `wlr_surface.commit`, layer-surface `destroy`, and `new_popup`.
- **Drives:** `wlr_scene_layer_surface_v1_configure` on every commit and output
  change, attaching each surface's scene node under the kernel `SceneLayer`
  band matching its protocol layer (background/bottom/top/overlay map 1:1;
  `normal` is toplevels-only and never used here).
- **Emits hooks:** none yet (see *Deferred*).

## Surface → scene-tree association (typed kernel contract)
For each layer surface we register its `wlr_surface` → our `wlr_scene_tree` via
`Host::host_surface()`, holding the move-only `SurfaceRegistration` as a member
of the `LayerSurface` (it unregisters on destruction). ext-xdg-shell resolves a
popup's parent surface to our tree via `Host::scene_tree_for()`, so xdg popups
parented to a layer surface attach correctly. This is the kernel-owned **typed**
replacement for the old `wlr_surface.data` convention (now dead) — cross-unit
surface→tree coupling routes through this contract, never through `.data`.

## Pure core
`include/unbox/ext-layer-shell/arrangement.hpp` — `Box`, `SurfaceState`,
`exclusive_edge()`, `apply_exclusive()`. Zero wlroots types; the independent,
doctest-hard mirror of the usable-area bookkeeping that
`wlr_scene_layer_surface_v1_configure` performs. It is what tiling (slice 7)
will read for per-output usable area. The glue keeps a per-output `Box` updated
from the helper's `usable_area` out-param using this model's coordinate
convention.

## Keyboard interactivity
All three zwlr v4/v5 modes are honored (the global advertises v5; on_demand
exists since v4):
- **`exclusive`** — focus the surface when it maps (on commit, `update_keyboard_focus`).
- **`on_demand`** — focus the surface when the user clicks or taps it. We
  subscribe `on_pointer_button` (press) and `on_touch_down`, resolve the hit
  with `wlr_scene_node_at` on `host.scene()`, map the hit `wlr_surface` back to
  one of our tracked layer surfaces, and focus it if it requests on_demand. We
  only ever TAKE focus on a hit to our own surface; we never steal it back, so
  clicking elsewhere lets focus move away normally. Coexists with
  ext-xdg-shell's toplevel-focusing handler on the same N-subscriber Events —
  the hits are disjoint (its toplevels vs our layer surfaces).
- **`none`** — left alone.

## What was deferred (intentional)
- **A typed usable-area service / `usable-area-changed` Event:** not exported.
  The per-output `Box` is computed and held internally; publishing it is left
  to the consumer that actually needs it (tiling) so the contract is shaped by
  a real caller, not guessed. Noted as a deliberate deferral.
- **Popup glue beyond the registration:** `new_popup` is bound but does no
  extra work; wlroots' scene helper wires popup nodes once a consumer resolves
  the parent via `Host::scene_tree_for()` against our `host_surface()`
  registration.

## Gotchas
- **Seed outputs at activate, do not rely on events alone.** `Server::create()`
  starts the backend, so outputs exist BEFORE extensions activate; their
  `on_output_added` already fired. We enumerate `host.output_layout()->outputs`
  in `activate()` to catch them — events-only tracking left `outputs_` empty and
  silently broke every output-less client (the fuzzel "no configure" bug). The
  underlying Host contract gap (late subscribers miss state) is a standing
  change-request in `reports/ext-layer-shell.md`.
- **Never destroy the scene node in the layer-surface destroy handler.**
  `wlr_scene_layer_surface_v1` installs its own internal destroy listener that
  frees the scene tree; calling `wlr_scene_node_destroy` ourselves is a
  use-after-free (signal-emit order between the two listeners is unspecified).
  Our destroy handler only reclaims the usable area and erases the
  `LayerSurface`.
- An output-less surface arriving when **no** output exists yet is **parked**
  (`pending_`, destroy-listener only) and placed once an output appears — not
  closed. We only close on a hard failure (`wlr_scene_layer_surface_v1_create`
  returning null).
- The destroy handler's **last** action is `owner.erase(this)`, which deletes
  the `LayerSurface`; copy any needed value (output, owner ref) into locals
  first — nothing may touch members afterwards (listener-lifetime).