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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
# tasks.md — live status
> The orchestrator updates this after EVERY milestone. Keep it terse:
> slice status + the single next action. History lives in git.
## Now
**Just landed — screenshots + wallpaper (user-driven, real-seat/nested VERIFIED):**
- **Screenshots (grim):** kernel creates `wlr_screencopy_manager_v1` +
`wlr_xdg_output_manager_v1` (policy-free plumbing, like data-device). grim
captures the standard `wlr_scene_output_commit` composite (RML docs are
scene-buffer nodes → captured). Verified nested: `grim` → valid 1280×720 PNG.
- **Arbitrary image decode:** vendored `stb_image` (user-approved); substrate
`LoadTexture` now decodes PNG/JPEG/… (was TGA-only), and a SubstrateSystemInterface
`JoinPath` override stops RmlUi stripping the leading `/` of an absolute path, so
`decorator: image('/abs')` AND `<img src='/abs'>` both load. Kernel ui_pixel tests.
- **`ext-wallpaper` (NEW, standard tier):** `[wallpaper]` in unbox.toml
(path/fit/color), shown in the `background` layer, hot-reloaded (drop+recreate
inline doc); bundled default `assets/ext-wallpaper/default.jpg` when no path set.
Needed a new kernel **`UiSurfaceSpec::input_transparent`** flag (a full-screen
background surface must not steal clicks — substrate press-ownership is per-rect,
pre-bus). Verified nested: configured image + bundled default both render; input
passes through. swaybg (layer-shell) still works too. GAP: multi-output (primary
only). Commits on feat/rml-compositing.
**ACTIVE (core, user-driven) — Slice 13: RML COMPOSITING (Phase 0 GO → Phase 2 impl).** Big direction
change: RMLUi becomes the content compositor — toplevels + layer-shell (incl.
wallpaper) + chrome are RML elements backed by LIVE, SHARED GL textures, with
layout/animation/3D effects in RCSS; wlroots stays foundation + cursor plane +
(deferred) fullscreen scanout bypass. Lost wlr_scene damage/scanout is mitigated
by OUR dirty-gated rendering (NOT a RMLUi built-in) + a deferred scanout bypass.
GATED BY A SPIKE before commit. Full spec + acceptance criteria:
`notes/rml-compositing.md`; decision row in `notes/plan.md` §2.
SPIKE RESULT: **PHASE 0 CLOSED — GO, real-seat CONFIRMED.** All 7 criteria
`ALL PASS` headless on Haswell+crocus (CF-AX3 GPU class); on the real seat:
input accurate through the 3D transform (after the `Element::Project()` routing
fix), and ~30fps under the 4-window `--demo` load. Stage-0 instrumentation
(per-phase split + GPU timer) shows it is **fill-bound** (~10–15ms whole-output
composite, ~2ms CPU) → damage limiting is the recovery lever, built properly in
Phase 1 (not the throwaway). Surface trees = **per-subsurface elements** (RTT
hook); present = FBO→dmabuf swapchain→wlr_scene_buffer + EGL fence. Throwaway
target `packages/kernel/rml-compositing-spike` (`--verify`/`--run`/`--demo`),
out of the shipped binary. **CONTRACT DECISION (user): RCSS is the single source
of truth for ALL layout + animation; C++ drives the document via a TYPED
substrate API.** PHASE 2 on `feat/rml-compositing` (off main; spike sources carried
as in-tree reference, `build_by_default:false`, deleted when the waves land).
**Waves 1 + 1b DONE + verified** (kernel): `SurfaceElement` (live sibling of
`Preview`) — zero-copy seq-gated import, frame-callback duty, dirty-gate, public
`create_surface_element(wlr_surface*)`; **surface trees** (subsurface/popup child
elements, whole-tree frame-done, parent-relative placement); **input-back**
(pointer/touch → surface-local via `Element::Project`, pure inversion core
doctested) + `SurfaceElement::focus_keyboard()` primitive. Kernel suite (72c/375a)
+ asan green; test-only `wayland-client`/xdg-shell-client codegen accepted
(kernel-tests scope). Wave plan: **W1/W1b** done; **W2** = ext-xdg-shell (ADD
`Toplevel::wl_surface()`; keep scene compositing for now — retire behind the flag
in W3) + ext-layer-shell (expose its surface); **W3** = NEW `ext-window-field`
(window list + RCSS layout + focus policy via `focus_keyboard()`, flips the flag);
**W4** = ext-stage-dock; **W5** = damage limiting (Option B) + scanout bypass.
**Waves 1b/2/3 + click-to-focus DONE + verified + committed.** Core RML
compositing is FUNCTIONAL behind `--rml-compositing` / `UNBOX_RML_COMPOSITING`:
- W2 ext-xdg-shell: `Toplevel::wl_surface()` (additive; scene compositing intact).
- Kernel: fixed the W1 test-seam listener-lifetime bug (root cause of the
`ext-stage-dock-glue`/`ext-xdg-shell-client` teardown aborts — both now GREEN).
- W3 NEW `ext-window-field` (core, `--rml-compositing`-gated): toplevels become
RCSS surface elements in ONE window-field ui surface; `bind_list("wins")` +
RCSS flex layout (`assets/ext-window-field/field.{rml,rcss}`); `Toplevel::hide()`
takes them out of wlr_scene; focus via `on_toplevel_focused`.
- Click/tap-to-focus: kernel `SurfaceElement::on_pressed` + window-field wires it
to `Toplevel::focus()`.
All unit suites + build-asan green; no regressions.
**POST-W3 polish + W3.5 (user-driven, real-seat CONFIRMED on the CF-AX3):**
- **Resize-to-tile** (real-seat verified, "resolution is great"): kernel
`SurfaceElement::rendered_width()/height()` (reads back the RCSS-resolved <img>
box — substrate already computes it for popup placement), ext-xdg-shell
`Toplevel::set_size()`, and an ext-window-field frame-pumped feedback loop that
configures each client to its on-screen box so the live texture maps 1:1.
Policy is config-driven (`unbox.toml [window-field] resize_mode` =
off|settle|continuous|debounced, hot-reloaded; pure doctested core).
- **ROOT-CAUSE FIX:** the field now shows each window via `<img data-attr-src>`,
NOT a `data-style-decorator`. RmlUi DOES bind `src` (data-attr); the decorator
had silently disabled everything the substrate keys off the `src` attribute —
pointer/touch input-back, click-to-focus, popup/subsurface placement, AND size
readback. (The dock keeps its decorator: frozen previews need none of that.)
- **W3.5 — FLOATING WINDOWS** (this wave, inserted before the dock per user): the
field is now a floating desktop — move (titlebar drag), resize (two bottom
corner grips), and a close button per window. New kernel primitive
`UiSurface::bind_list_drag` (per-row drag, the list analogue of bind_drag).
ext-window-field: per-window geometry STATE (x,y,w,h,z) bound + applied as RCSS
data-style (C++ owns interactive state, RCSS renders — contract-clean); pure
doctested geometry core (`src/geometry.cpp`: move / resize_bl / resize_br with
anchored-opposite-edge + min-size, field clamp); z-order raise on focus; cascade
placement. The earlier focused-dominates TILING is dropped — tiled / sidebar
containers return LATER as containers windows migrate between (captured-state
animations), per the user's staged plan. All suites + build-asan green.
- **SERVER-SIDE DECORATIONS** (CSD fix for floating windows): ext-xdg-shell now
advertises the xdg-decoration manager and forces SERVER_SIDE when RML
compositing is on (host-bin passes the flag to create()), so clients drop their
own titlebars and only the field's RML chrome shows. Classic path keeps CSD.
Kernel wrapper exposes wlr_xdg_decoration_v1. (Caveat: GTK/libadwaita ignore
SSD and always draw CSD — a client limitation.)
NEXT ACTIONS (need USER): (1) **real-seat check of floating windows** —
`UNBOX_RML_COMPOSITING=1 UNBOX_ASSET_DIR=<repo>/assets ./build/packages/host-bin/unbox`
on the CF-AX3: drag titlebars to move, drag bottom grips to resize, tap × to
close. (2) **Wave 4 (now W5) decision** — how minimize-to-dock coordinates with
the floating window-field. (3) **Wave 6 decision** — damage limiting has real
unknowns (RmlUi exposes no per-element screen damage), may need a spike.
DEFERRED: Wave 3b (layer-shell wallpaper as surface element — wallpaper/panels
already work via wlr_scene background/overlay bands); spike-target deletion +
final doc reconcile (after W5).
Tiling (slice 7) is DEFERRED behind this (becomes RCSS over surface elements;
pure layout core in `notes/tiling-spec.md` carries over). Stage dock (slice 10)
real-seat feel check is paused under this pivot.
**DEV WORKFLOW (RML/RCSS hot-reload — use this):** UI documents are external assets
under `assets/<unit>/` (e.g. `assets/ext-stage-dock/dock.rml` + `dock.rcss`), loaded
via `UiSurfaceSpec::rml_path`. Launch unbox with
`UNBOX_ASSET_DIR=<repo>/assets UNBOX_DEV=1` (reads the source tree + arms an inotify
watcher). Then editing a .rml/.rcss and saving HOT-RELOADS the live surface — NO
recompile, NO restart (bindings/geometry preserved; a broken file keeps the old doc).
Real-seat verified. Installed builds find assets via `-DUNBOX_ASSET_DIR_DEFAULT`.
(9c0c0bf kernel, f852141 dock+build)
**CONFIG HOT-RELOAD (always-on, user feature):** editing `~/.config/unbox/unbox.toml`
re-applies keybindings LIVE — no restart. Backed by a general kernel primitive
`Host::watch_file(path, cb) -> FileWatch` (RAII, coalesced, editor-save/create-safe,
error-isolated; ONE session inotify also backs the UI hot-reload above). A
malformed/mid-edit file keeps the current bindings + logs a warning (keys never
drop). Real-seat verified. (3c1bde9 kernel watch_file, 9f7dc09 ext-keybindings)
**Just landed — usability slice (user-driven, real-seat verified on the CF-AX3):**
`ext-keybindings` (new core ext) reads keybindings from `unbox.toml`: tap-Super →
spawn fuzzel, Alt+Tab / Alt+Shift+Tab → stable focus rotation over all toplevels,
Alt+F1, Ctrl+Alt+Backspace → quit. ext-xdg-shell's hardcoded keybinds migrated
out. Kernel now exports `WAYLAND_DISPLAY` so extension-spawned clients reach unbox
(was the fuzzel "no monitors" root cause). build + build-asan both green.
Follow-up: Ctrl+Alt+F1..F12 VT switching is now kernel-hardwired (the session
escape hatch).
**Active — Slice 10: stage dock** (user-driven; supersedes slice 6 as the next
UI work). The Stage-Manager-style left-edge dock of minimized-window **previews**,
revealed by a left-edge **swipe**. **Fork B** (see plan.md §2): previews are
toplevel snapshots imported as textures into the RMLUi context, shown as `<img>`
in one RML document. Waves (a number per wave runs in parallel = disjoint units):
**Landed a1–d1 (committed; code-complete, all green build + build-asan 10/10 suites;
real-seat feel pending):**
- a1 kernel SPIKE — Fork-B GO on crocus: `Preview` + `create_preview(wlr_scene_tree*)`,
wlr pixels → dmabuf → EGLImage → sampled RMLUi texture → `<img>`. (7fed564)
- b1 ext-xdg-shell — `Toplevel::hide()/show()` (≠ unmap) + `geometry()` + `scene_tree()`. (bdce81a)
- b2 kernel — UiSurface list bindings (`bind_list`/`bind_list_string`/`bind_list_event`). (74c8071)
- b4 ext-stage-dock (new unit) — skeleton + pure cores (reveal recognizer, dock layout). (d6535e8)
- c2 ext-stage-dock + host-bin — Super+M minimize → preview slot → hide; tap → restore. (3376100)
- d1 ext-stage-dock — RCSS dock slide-in + per-slot settle; restore instant. (b578327)
- fix ext-stage-dock — dock previews were blank: `data-attr-src` (RmlUi binds
attrs, not `{{}}`) + font `Noto Sans` + valid `transform-origin`. REAL-SEAT
VERIFIED: minimizing 2 foot windows shows 2 live preview snapshots in the dock;
Super+M repeats with >1 window. (5ebd45a)
- TRANSPARENCY + usability pass (REAL-SEAT VERIFIED): kernel — ui surfaces now
composite per-pixel alpha (stray opaque `Clear()` removed) AND `set_size` resizes
the render target (was logical-only; the slice-5 change-request) so a surface can
grow. (f1e12a3). ext-stage-dock — strip background transparent (windows show
through; cards keep their panel), surface hugs the card stack (no full-height
input capture), and re-minimize-after-empty fixed (stale `focused_`: restore now
sets it directly since a non-defocused window's `focus()` is a seat no-op). (661166a)
- CARD = ROUNDED THUMBNAIL (REAL-SEAT VERIFIED): the card IS the window preview,
rounded on all four corners — a full-bleed `image(... cover center)` decorator on
a child of a rounded `overflow:hidden` slot (RmlUi won't clip an element's OWN
decorator to its OWN radius → decorator lives on the clipped child). First use of
the substrate's RmlUi clipping path (scissor + stencil clip-mask); kernel verified
it correct + added 4 regression tests (6519ebf). Title overlay parked
(`display:none`, binding kept) for a later text redesign — user's call. (a743f44)
**NEXT (needs user):**
1. REAL-SEAT feel check (covers c2+d1): `~/start-unbox.sh -s foot`, Super+M minimizes
foot → its preview card slides into the 240px left dock; tap the card → foot
restores; minimizing the last window slides the dock out.
2. BOUNDARY DECISION — the full cross-screen "window flies into the dock" flight (and
e1's drag-out grow-back) needs exactly ONE new kernel primitive: an
**input-transparent UiSurface flag** on `UiSurfaceSpec` (d1 proved animation-end is
already observable via `bind_event`; no timer/frame-tick needed). Approve it →
then c1 gesture-claim + e1 (edge reveal + drag-out), the config-driven
minimize-keybind migration (ext-keybindings action + a stage-dock Service), and
favicon (needs an XDG icon-theme dep) follow.
Slice 6 re-scoped: the **window-list taskbar is CUT** (overlaps the stage dock,
conflicts with the touch/iPad direction; its contract-exercise purpose was met by
the stage dock). Replaced by two future, not-yet-designed features sequenced
AFTER slice 7 (tiling): **status bar** (slice 11) and **home screen** (slice 12)
— ideas + open questions captured in `notes/status-bar-home-screen.md`. Launching
is covered by fuzzel today (and the home screen later).
Still queued whenever UI work resumes: keyboard-into-ui-surfaces, removing the
deprecated no-op `Options::ui_spike`, retiring host-bin's demo ui.
## Slices
| # | Slice | Status | Acceptance |
|---|---|---|---|
| 0 | Harness skeleton | **DONE** 2026-06-12 | All harness md files in place |
| 1 | Bootstrap: toolchain, Meson skeleton, RMLUi subproject compiles, empty kernel links wlroots-0.20 from C++ via the extern-"C" wrapper | **DONE** 2026-06-12 | met: build green; tests 1/1; binary prints wlroots 0.20.1 + RmlUi 6.2, exits 0 |
| 2 | tinywl port: kernel skeleton runs nested under labwc | **DONE** 2026-06-12 | met: nested output WL-1, foot toplevel mapped+focused, GLES2 renderer; touch handlers added (tinywl lacks them); headless boot test green |
| 3 | **THE SPIKE:** RMLUi→scene bridge | **DONE — GO** 2026-06-12 | met: Plan A (dmabuf FBO→wlr_buffer→wlr_scene_buffer) verified nested+headless on HD 4400; Plan B fallback verified; orientation fixed + position-aware guard; input proof on-screen; RSS ≈83 MiB; ASan/UBSan clean in our code (known noise: Mesa leak reports + 2 benign UBSan downcasts inside vendored RMLUi). glFinish→fence and format negotiation deferred to the real substrate (slice 4+) |
| 4 | Extension host + contracts: bus, manifests, static registration; xdg-shell/layer-shell refactored OUT of kernel into core extensions | **DONE** 2026-06-12 | met: kernel boots featureless (names no feature); typed Event/Filter bus error-isolated + topo activation; ext-xdg-shell (toplevels, focus, grabs via pure GrabMachine, button/axis routing, Ctrl+Alt+Backspace quit) + ext-layer-shell (fuzzel verified, pure arrangement core) pass suites; typed surface→scene-tree registry replaced the data-field convention; first protocol codegen (wlr-layer-shell XML vendored); user hands-on: all input paths verified incl. touch; 68 cases green + ASan clean; idle RSS ≈73 MiB |
| 5 | Input routing + ergonomics contract: unified pointer/touch→RMLUi events, keybinding filter chain, touch-mode RCSS variables | **DONE** 2026-06-13 | met (user hands-on): real ui substrate (`Host::ui()` → UiSurface, scalar+event bindings, dmabuf+fence+swapchain); same demo surface driven by mouse AND finger; consume-or-pass with implicit-grab ownership (press owner gets release, per touch point too); touch-mode = state+notification only, NO visual scaling (user decision); touch-initiated grabs incl. pointer/touch alternation (seat release-leak fixed); keybinding chain satisfied by slice-4 Filter (ext-keybindings deferred); 113 doctest cases green, ASan clean, idle RSS ≈78 MiB |
| 5b | Usability: `ext-keybindings` (config-driven `unbox.toml`) — Super→fuzzel, Alt+Tab focus rotation; ext-xdg-shell keybinds migrated; kernel exports `WAYLAND_DISPLAY` for spawned clients | **DONE** 2026-06-13 | met (real-seat, user-confirmed): fuzzel opens on Super, Alt+Tab cycles all windows, quit works; build + build-asan both green (3rd-party Mesa/RmlUi sanitizer noise suppressed; a real libwayland leak in the layer-shell client test fixed) |
| 6 | ~~ext-taskbar~~ + ext-launcher | **taskbar CUT / re-scoped** | window-list taskbar dropped (overlaps the stage dock + conflicts with the touch/iPad direction). Replaced by slices 11–12. Launching is covered by fuzzel today and the home screen later. The contract-exercise purpose was met by the stage dock. |
| 7 | ext-window-tiling: pure layout core + thin scene glue | **DEFERRED pending slice 13** (baseline designed) | layout math 100% doctest-covered, zero wlroots types in core. Baseline = `primary` (right) + `stack` (left), auto-tile, 1=full/2=50-50/3+=stack-left; see `notes/tiling-spec.md` (+ `notes/tiling-layouts-reference.md`). Held until RML compositing (slice 13) lands — tiling then becomes RCSS layout over surface elements; the pure core is renderer-agnostic and carries over. (`Toplevel::set_box` prototype reverted; recreate from `prompts/ext-xdg-shell.md` when tiling resumes.) |
| 8 | ext-osk: RML keyboard ui surface injecting via wlr_seat | pending | type into foot via touch only; auto-show on text-input focus |
| 9 | Session hardening: s6 user service, TTY launch on seat0, layout persistence (append-only state + pure reconcile on boot) | pending | survives `kill -9` + s6 restart with workspaces restored |
| 10 | **Stage dock** (ext-stage-dock): minimized-window previews on a left-edge swipe (Fork B) | **a1–d1 landed; previews real-seat-verified** | DONE: Super+M minimize→RMLUi-imported preview snapshot→dock slot→hide (previews confirmed rendering on hardware); RCSS dock slide-in + slot settle. NEXT: confirm tap-to-restore + animation feel; 1 boundary call (input-transparent UiSurface flag) → c1 gesture-claim → e1 gesture reveal/drag-out; then config-driven minimize keybind + favicon (XDG icon dep) |
| 11 | **Status bar** (tent. ext-statusbar): iPad/iOS top bar — clock (left), configurable left/middle/right sections, tray (right) wifi/volume/battery | **IDEA — needs design** | sequenced AFTER slice 7 (tiling); replaces cut taskbar. Details + open questions: `notes/status-bar-home-screen.md` |
| 12 | **Home screen** (tent. ext-home, iPad springboard): app grid; tap = launch-or-raise (instance picker if >1 open); add/remove apps; swipe-up-from-bottom to enter | **IDEA — needs design** | sequenced AFTER slice 7 (tiling); replaces cut taskbar. Details + open questions: `notes/status-bar-home-screen.md` |
| 13 | **RML compositing** — RMLUi becomes the content compositor (toplevels + layer-shell incl. wallpaper + chrome = RML elements backed by LIVE, SHARED GL textures; layout/animation/3D effects in RCSS). wlroots = foundation + cursor plane + (deferred) fullscreen scanout bypass. | **Phase 0 spike CLOSED — GO, real-seat CONFIRMED; Phase 1 design DONE (`notes/rml-compositing-phase1.md`); Phase 2 impl NEXT** | All 7 criteria `ALL PASS` headless on Haswell+crocus: (1) zero-copy live dmabuf texture (cached when unchanged); (2) RCSS perspective+rotateY on live pixels (readback); (3) screen→surface-local inversion through the transform = 0.000000px; (4) surface tree composited → **per-subsurface elements** (RTT hook for tree-spanning effects); (5) wallpaper via identical import path; (6) idle dirty-gate = 0 idle renders / 1-per-commit (frame-time @load = real-seat); (7) FBO→dmabuf→wlr_scene_buffer + EGL fence. Spike target `rml-compositing-spike` (`--verify`/`--run`). Report + runbook: `reports/rml-compositing-spike.md`. |
## Deferred decisions (decide when reached — see notes/plan.md §7)
dlopen extensions · remote builds on a fast box · xwayland default ·
OSK virtual-keyboard protocol vs direct seat injection · workspace model ·
clang-format style details
|