summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-14 21:04:51 +0900
committerAdam Malczewski <[email protected]>2026-06-14 21:04:51 +0900
commit5437b5e33c542c801bdc557be2ae93bfec6e153d (patch)
treeecb808fb7e5c228d30936f6269ef28853c950839
parente008b9a078c91b07947fba7748aae7fab8758a2f (diff)
downloadunbox-5437b5e33c542c801bdc557be2ae93bfec6e153d.tar.gz
unbox-5437b5e33c542c801bdc557be2ae93bfec6e153d.zip
docs: plan RML compositing direction + roadmap re-scope
Major architecture direction (gated by a spike): make the RMLUi substrate the content compositor — toplevels, layer-shell clients (incl. wallpaper), and UI chrome become RML elements backed by live, shared GL textures, with layout, animation, and 3D effects expressed in RCSS. wlroots stays the foundation + hardware cursor plane + a deferred fullscreen-video scanout bypass. Lost wlr_scene damage/scanout is mitigated by our own dirty-gated rendering (not a RMLUi built-in) + the deferred scanout bypass. - notes/rml-compositing.md: full architecture, the de-risking already done (shared EGLDisplay; RmlUi transform-aware hit-testing + RTT/filters/shaders; slice 3 + stage dock proved dmabuf/EGLImage), division of labour, perf posture, Phase 0 spike acceptance criteria, Phase 1/2, naming proposal. - notes/plan.md §2: decision row (reopens the compositing half of row 51) with rationale + explicit spike fallback. - tasks.md: slice 13 (RML compositing spike) = ACTIVE core; slice 7 (tiling) DEFERRED behind it; "Now" leads with the pivot. Roadmap re-scope (earlier this session): - notes/tiling-spec.md: baseline tiling design (primary/stack, configurable side + new-window insertion, hot-reloadable [tiling] config) — deferred. - notes/tiling-layouts-reference.md: dwm layout catalogue for later. - notes/status-bar-home-screen.md: status bar + iPad-style home screen ideas (replace the cut window-list taskbar); tasks.md slices 6 (cut), 11, 12.
-rw-r--r--notes/plan.md1
-rw-r--r--notes/rml-compositing.md129
-rw-r--r--notes/status-bar-home-screen.md79
-rw-r--r--notes/tiling-layouts-reference.md78
-rw-r--r--notes/tiling-spec.md164
-rw-r--r--tasks.md28
6 files changed, 475 insertions, 4 deletions
diff --git a/notes/plan.md b/notes/plan.md
index db92d58..b0ea7cd 100644
--- a/notes/plan.md
+++ b/notes/plan.md
@@ -68,6 +68,7 @@ solves), and the trigger that would reopen it.
| **VT switching (Ctrl+Alt+Fn) is kernel-hardwired** before the key_filter (`wlr_session_change_vt`) | It is the session escape hatch — must work even if an extension throws or greedily consumes keys; not a rebindable feature (user decision) | — |
| **Stage dock** (ext-stage-dock, standard) = the Stage-Manager-style left-edge dock of minimized-window **previews**, revealed by a left-edge **swipe**. **Fork B**: previews are toplevel snapshots imported as textures INTO the ui substrate's RMLUi context and shown as `<img>` in ONE RML document | Closest to the iPad Stage Manager north star; one ui surface animates as a unit via RCSS; reuses the slice-3 dmabuf/EGLImage bridge in reverse (wlr pixels → RMLUi texture) instead of two-layer scene/RML lockstep | Cross-context texture import proves infeasible on crocus → fall back to Fork A (previews as `wlr_scene` snapshot nodes) |
| **Mechanism in kernel/core, policy in ext-stage-dock.** Kernel ui substrate gains: preview-snapshot, list/container bindings, a gesture-CLAIM input path. ext-xdg-shell gains: `Toplevel::hide()/show()` (≠ unmap), `geometry()`, `scene_tree()`. ext-stage-dock owns: the "minimized" set, dock layout, gesture recognition, easing | Keeps "kernel names no feature" — snapshot/claim/list-bindings are generic primitives; minimize-to-dock is the only policy and lives in one standard extension | — |
+| **RML compositing (DIRECTION, gated by a spike — reopens the compositing half of row 51).** RMLUi becomes the CONTENT compositor: toplevels, layer-shell clients (incl. wallpaper), and UI chrome are all RMLUi elements backed by LIVE, SHARED GL textures (zero per-frame copy via the existing shared EGLDisplay). Layout/animation/3D effects are RCSS. wlroots stays the foundation + plane manager (backend/output/renderer/seat, hardware cursor plane, fullscreen-video scanout bypass). Lost wlr_scene damage/scanout is mitigated by OUR dirty-gated rendering (idle ≈ no work) + a deferred fullscreen scanout bypass. Full design + spike acceptance criteria: `notes/rml-compositing.md` | The appeal is Hyprland-grade window effects via DECLARATIVE RCSS (layout, transitions, 3D transforms, blur/shaders) with hot-reload — and RmlUi already does transform-aware hit-testing + RTT/filters, while slice 3 + the stage dock already proved dmabuf/EGLImage texture handoff on this exact crocus HW. Doing windows in RMLUi means tiling/effects/stage-manager are all RCSS policy on top | The spike fails on the CF-AX3 (no zero-copy shared-texture path, input-routing dead-end, surface-tree composition impractical, or idle/perf out of budget) → fall back to wlr_scene compositing + transient snapshot-through-RMLUi effects |
## 3. Architecture
diff --git a/notes/rml-compositing.md b/notes/rml-compositing.md
new file mode 100644
index 0000000..ada1b8b
--- /dev/null
+++ b/notes/rml-compositing.md
@@ -0,0 +1,129 @@
+# RML compositing — architecture direction (gated by a spike)
+
+> **Status: DIRECTION CHOSEN, gated by a GO/NO-GO spike.** This reopens the
+> compositing half of plan.md §2 row 51 (wlr_scene for compositing). Nothing is
+> committed until the spike (Phase 0) passes on the real CF-AX3. Naming below is
+> PROPOSED — needs user sign-off before it lands in GLOSSARY.md.
+
+## Thesis
+The kernel's UI substrate (RMLUi) becomes the **content compositor**: application
+toplevels, layer-shell clients (wallpaper, panels), and the existing UI chrome
+are all RMLUi elements backed by **live, shared GL textures** — so window
+**layout, animation, and 3D effects are expressed in RCSS**, with no per-frame
+texture copies. Tiling, stage-manager, effects, etc. become RCSS + extension
+policy on top of this.
+
+## Why this is viable (the de-risking already done)
+- **Shared EGLDisplay already exists** (plan.md §2 row 51): the wlr renderer and
+ RMLUi's GLES 3.2 context share an EGLDisplay. Slice 3 + the stage dock already
+ move client/scene pixels into RMLUi as textures via dmabuf/EGLImage on this
+ exact Haswell/crocus HW — the texture handoff is proven; the spike's job is to
+ make it **live + zero-copy + shared-handle** rather than snapshot.
+- **RmlUi does transform-aware hit-testing for us.** `Context::ProcessMouse*` /
+ `ProcessTouch*` pick the element under a point THROUGH `transform`/3D
+ `perspective` and dispatch DOM-style events + `:hover` (auto-updated in
+ `Context::Update`). So the hard geometry of routing input through a 3D-tilted
+ window is handled upstream — we only translate "element X at local (lx,ly)" to
+ `wl_seat` (surface-local coords + implicit grab), which the kernel already
+ models.
+- **RmlUi GL3 renderer supports** transforms, clip masks, **filters (blur,
+ drop-shadow)**, **shaders**, and **render-to-texture** — and caches compiled
+ geometry + effect passes (an unchanged blur isn't recomputed). Our custom
+ RenderInterface must implement these hooks (the "custom is fine" work).
+
+## Division of labour (the end state)
+- **RMLUi = content compositor.** ALL on-screen content: toplevels, layer-shell
+ clients (incl. wallpaper — user decision), UI chrome. Layout/animation/effects
+ in RCSS.
+- **wlroots = foundation + plane manager** (NOT optional; RMLUi can't talk to
+ DRM):
+ - backend/DRM, output management, modeset, vblank/frame scheduling, GLES
+ renderer, buffer import (dmabuf→texture), allocation, `wl_seat`/input;
+ - the **hardware cursor plane** (drawing the cursor in RMLUi would force a full
+ recomposite on every move — keep it a wlr plane);
+ - the **fullscreen-video scanout bypass** (deferred optimization — see below).
+- This is NOT "hybrid compositing": in steady state RMLUi composites everything
+ and `wlr_scene` is reduced to **present the RMLUi buffer + cursor plane +
+ scanout bypass**. `wlr_scene` may stay as that thin presenter (it can even
+ scan out the RMLUi buffer itself on the primary plane).
+
+## Performance posture (replacing wlr_scene's damage/scanout)
+What we give up by moving content off `wlr_scene`: per-surface damage, occlusion
+culling, and direct scanout. Mitigations:
+- **Dirty-gated rendering (OUR mechanism — NOT a RMLUi built-in).** RMLUi tracks
+ internal dirty flags (so `Update()` is cheap at idle) and knows when animations
+ are active, but it does NOT provide screen damage or a "skip this frame"
+ signal — and the most important dirty source here (a client buffer updating) is
+ OUR shared texture changing OUTSIDE RMLUi, which RMLUi can't see anyway. So WE
+ gate: only schedule + `Render()` a frame when a signal we already own fires — a
+ client buffer commit (wlroots), an active RCSS animation (RMLUi tells us), or an
+ input-driven state change (hover/focus/drag). This keeps a static desktop at
+ ~zero GPU — the big battery/thermal win on a 15 W fanless ultrabook — WITHOUT
+ per-surface damage. (`request_frames` already stops scheduling at rest; we gate
+ on dirtiness on top.) The spike must CONFIRM idle ≈ no work in practice.
+- **Fullscreen-video scanout bypass (deferred).** When exactly one window is
+ fullscreen with nothing composited on top, pull that surface OUT of the RMLUi
+ composite and hand it to `wlr_scene`/scanout directly (RMLUi draws nothing that
+ frame). Trigger = the fullscreen STATE (e.g. VLC's fullscreen button), not the
+ click. Impact of NOT having it: more battery/heat during long fullscreen video,
+ not breakage — so it's a later optimization, sized by a spike measurement.
+- What we lose and accept for now: partial-region redraw when one small thing
+ changes (minor; the HD4400 can repaint 1080p of simple quads within budget).
+
+## Phase 0 — THE SPIKE (kernel/substrate; throwaway; one GO/NO-GO)
+Acceptance criteria, measured on the real CF-AX3:
+1. A **live** toplevel buffer sampled by RmlUi via a shared GL context — **zero
+ per-frame copy** — drawn as an element.
+2. An **RCSS 3D transform + transition** applied to it (visual proof of payoff).
+3. **Pointer + touch + keyboard routed back** to that client through RmlUi
+ picking → `wl_seat`, correct under a transform.
+4. A toplevel **with a popup + subsurface** composited correctly → decides the
+ surface-tree question: **per-subsurface elements** vs **per-window
+ render-to-texture**.
+5. A **layer-shell client (wallpaper)** also rendered as an element (proves the
+ "wallpaper through RMLUi" decision; same mechanism as #1).
+6. **Perf**: ~4 windows @1080p incl. one continuously updating (terminal/video);
+ measure frame time AND confirm **idle = ~no work** with dirty-gating on; also
+ measure the cost of pushing fullscreen video through RMLUi (to size the
+ scanout bypass).
+7. **Present path**: reuse the existing RMLUi-FBO → `wlr_scene_buffer` bridge
+ (fastest to truth); cursor stays a wlr plane.
+
+Output: report + GO/NO-GO + chosen answers to (4) and the present-path, + the
+perf numbers.
+
+## Phase 1 — Architecture (if GO): a design doc settling
+- The **surface-element model** + surface-tree handling (from spike #4).
+- The **shared-texture handoff** API in the substrate.
+- **Unified input**: fold the kernel's pointer/touch routing into RmlUi picking
+ (dovetails with the existing "substrate gets input first" implicit-grab path).
+- The **extension-facing contract**: how a future tiling/effects/stage-dock
+ extension places & animates windows — RCSS docs + data bindings (existing
+ substrate contract) vs. a new typed window-layout service.
+- Update plan.md §2 row 51 to the new compositing model.
+
+## Phase 2 — Implementation (phased, behind a flag; session stays usable)
+substrate: surface-element + shared-texture handoff → kernel: route toplevels +
+layer-shell into the substrate instead of `wlr_scene`; switch input → port
+focus/move/resize/fullscreen → re-express stage-dock minimize as RCSS → revisit
+tiling (now trivial: RCSS layout over surface elements) → effects.
+
+## Proposed naming (NEEDS SIGN-OFF before GLOSSARY.md)
+- **RML compositing** — the approach: RMLUi composites all on-screen content.
+- **surface element** — an RML element backed by a live client surface's shared
+ texture (a toplevel OR a layer surface presented inside RMLUi). Uses the
+ canonical "surface"; avoids the "window" alias.
+
+## Open questions the spike resolves
+- per-subsurface elements vs per-window render-to-texture (the #1 unknown);
+- present path: reuse FBO→scene_buffer vs eventually render direct to output;
+- real perf headroom on the HD4400 (frame time + idle + video).
+
+## Relationship to other work
+- **Tiling is deferred** and becomes much smaller on top of this (RCSS layout
+ over surface elements; the pure layout core in `notes/tiling-spec.md` still
+ applies — it's renderer-agnostic).
+- The **stage dock** already prototypes the texture-import half (frozen
+ previews); its minimize/restore becomes RCSS on the new path.
+- Status bar / home screen (slices 11–12) are RMLUi chrome already — they fit
+ natively.
diff --git a/notes/status-bar-home-screen.md b/notes/status-bar-home-screen.md
new file mode 100644
index 0000000..52d1a9a
--- /dev/null
+++ b/notes/status-bar-home-screen.md
@@ -0,0 +1,79 @@
+# Status bar + Home screen — early ideas (DRAFT, NOT yet designed)
+
+> **Status: IDEAS ONLY — needs fleshing out.** Nothing here is a committed
+> design. Every section below has open questions that must be resolved (and
+> user-signed-off) before implementation. These two features **replace the cut
+> window-list taskbar** (old slice 6) and are **sequenced AFTER slice 7
+> (tiling)** — see `tasks.md` slices 11–12.
+>
+> Unit names (`ext-statusbar`, `ext-home`) are **tentative**; a canonical name
+> needs a `GLOSSARY.md` entry with user sign-off (no synonym coinage rule).
+>
+> Rationale for replacing the taskbar: this is a touchscreen CF-AX3 with an
+> iPad-Stage-Manager north star. A conventional window-list taskbar overlaps the
+> stage dock and costs scarce screen + GPU. What's genuinely missing is (1)
+> system status and (2) an iOS-style app launch surface — these two items.
+
+## 1. Status bar (tentative: `ext-statusbar`)
+
+An iPad/iPhone-style bar pinned to the **top edge** of the screen.
+
+- **Left:** clock.
+- **Right:** a system **tray** — wifi, volume, battery.
+- **Left / middle / right** sections are **configurable** (which items appear and
+ in what order), presumably via `unbox.toml`.
+
+### Open questions / to flesh out
+- Item model: a fixed set vs a plugin/registry of "status items" extensions can
+ contribute. How does config reference them (typed, not string-keyed)?
+- Data sources: battery (sysfs/UPower?), wifi (NetworkManager/iwd via D-Bus?),
+ volume (PipeWire/WirePlumber — already in the session per `start-unbox`),
+ clock (local). Each is an effect at the edge; keep pure formatting cores.
+- Surface: a `wlr-layer-shell` top-anchored surface with an **exclusive zone**
+ (reserves top space; tiling/usable-area must account for it — ties into the
+ ext-layer-shell usable-area model already built).
+- Tap behavior of tray items (popovers for volume slider, wifi list, etc.).
+- Touch target sizing (touch-mode is state-only, no auto-scale — extension
+ adapts affordances explicitly via `on_touch_mode_changed`).
+- Theming/RCSS; per-output (one bar per output? primary only?).
+
+## 2. Home screen (iPad "Springboard"-style; tentative: `ext-home`)
+
+A full-screen surface that shows **app icons**; the iOS home screen analogue.
+
+- **Tap/click an app:**
+ - if the app is **not open** → launch it.
+ - if it **is open** (single instance) → raise/focus that window.
+ - if it is open with **multiple instances** → show all of them as options and
+ let the user pick which to bring forward.
+- **Add / remove** apps from the home screen (some management flow/UI).
+- **Swipe up from the bottom edge** → enter the home screen.
+
+### Open questions / to flesh out
+- App catalog source: XDG `.desktop` entries (Exec/Name/Icon) for the
+ add-app picker? How is the on-screen set stored/persisted (append-only state,
+ reconcile-on-boot — mirrors slice 9's durability model)?
+- Icons: needs an XDG **icon-theme** dependency — overlaps `favicon-spec.md`
+ (the dock favicon work). Resolve once, share.
+- **Instance tracking (the hard part):** "is this app already open, and which
+ windows are its instances?" requires a window↔app association (app-id /
+ `.desktop` ↔ `xdg_toplevel` app_id). Likely a kernel/core capability the
+ home screen + stage dock + a future taskbar-tray all reuse. Define the
+ contract once.
+- The multi-instance picker UI — could reuse the stage-dock preview-snapshot
+ pipeline (live thumbnails of the candidate windows).
+- Gesture: swipe-up-from-bottom mirrors the stage dock's left-edge reveal
+ recognizer; reuse the gesture-CLAIM input path / recognizer cores.
+- Layout: grid, pages/folders, reorder (drag). How it coexists with tiling and
+ the stage dock (is the home screen a workspace? a layer? what has focus?).
+- Launch reuses the existing `spawn` mechanism + `WAYLAND_DISPLAY` export.
+
+## Shared primitives these two (probably) need
+- **Window↔app association / instance enumeration** (home screen; reusable).
+- **XDG icon-theme lookup** (home screen icons; shared with favicon-spec).
+- **Top-edge exclusive-zone layer surface + usable-area accounting** (status bar).
+- **Bottom-edge gesture reveal** (home screen; same family as stage dock's
+ left-edge reveal).
+- Keep "kernel names no feature": snapshots, gesture-claim, list bindings,
+ instance enumeration are generic; status-item set and home-screen contents are
+ the policy and live in the standard extensions.
diff --git a/notes/tiling-layouts-reference.md b/notes/tiling-layouts-reference.md
new file mode 100644
index 0000000..ff7a915
--- /dev/null
+++ b/notes/tiling-layouts-reference.md
@@ -0,0 +1,78 @@
+# Tiling layouts — dwm reference (for slice 7, ext-window-tiling)
+
+> **Status: REFERENCE / research only.** A catalogue of dwm's tiling layouts to
+> draw from when designing `ext-window-tiling` (slice 7). Revisit once we have a
+> baseline tiling core to work off. These are X11/dwm patches — NOT something we
+> apply; they're a design reference. Each maps cleanly onto the slice-7 goal of a
+> **pure layout core**: `(client count, master count, mfact, area) -> list of
+> rects`, with zero wlroots types. Source: dwm.suckless.org/patches + the
+> bakkeby/dwm-flexipatch bundle (which ships these as git-applicable diffs).
+
+## Built-in dwm layouts (no patch)
+
+| Layout | Description |
+|---|---|
+| **tile** | Default master/stack: `nmaster` windows in a left master column, the rest stacked vertically on the right. `mfact` sets the master/stack split. |
+| **monocle** | Every window maximized to the full area; one shown at a time. |
+| **floating** | Windows moved/resized freely (no tiling). |
+
+## Layout patches
+
+### Master / stack variants
+| Layout | Description | Patch |
+|---|---|---|
+| **bstack** (bottomstack) | Master on **top** (full width); stack tiled left-to-right in a row beneath. | https://dwm.suckless.org/patches/bottomstack/ |
+| **bstackhoriz** | Bottom-stack variant: stack clients stacked top-to-bottom beneath the master. | (same patch as bstack) |
+| **columns** (col) | Like `tile`, but master-area clients are arranged in columns (left-to-right) instead of a single column. | https://dwm.suckless.org/patches/columns/ |
+| **deck** | Master area normal; stack clients "decked" — piled on top of each other, only the focused one visible (monocle for the stack). | https://dwm.suckless.org/patches/deck/ |
+
+### Centered master
+| Layout | Description | Patch |
+|---|---|---|
+| **centeredmaster** | Master column centered horizontally; stack split to the **left and right** of it. | https://dwm.suckless.org/patches/centeredmaster/ |
+| **centeredfloatingmaster** | Master floats centered **on top** of the full-width tiled stack. | (same patch) |
+
+### Grids
+| Layout | Description | Patch |
+|---|---|---|
+| **gridmode** (grid) | All windows in an even grid. | https://dwm.suckless.org/patches/gridmode/ |
+| **gaplessgrid** | Grid with balanced columns; the last column absorbs the remainder so there are no empty cells. | https://dwm.suckless.org/patches/gaplessgrid/ |
+| **horizgrid** | Splits into a top and bottom row (horizontal grid). | https://dwm.suckless.org/patches/horizgrid/ |
+| **nrowgrid** | Grid whose number of rows is controlled by `nmaster`. | https://dwm.suckless.org/patches/nrowgrid/ |
+
+### Spiral
+| Layout | Description | Patch |
+|---|---|---|
+| **fibonacci → spiral** | Fibonacci tiling: each window takes half the remaining space, spiraling inward. | https://dwm.suckless.org/patches/fibonacci/ |
+| **fibonacci → dwindle** | Same split, but windows dwindle toward the bottom-right instead of spiraling. | (same patch) |
+
+### Meta / configurable
+| Layout | Description | Patch |
+|---|---|---|
+| **flextile-deluxe** (supersedes **flextile**) | Configurable meta-layout: pick a split mode (horizontal / vertical / centered / floating / fixed) and a per-split tile arrangement (stack h/v, grids, fibonacci). Can reproduce tile, deck, monocle, centeredmaster, bstack/bstackhoriz, gapplessgrid, and more. | https://github.com/bakkeby/patches/wiki/flextile-deluxe/ (orig: https://dwm.suckless.org/patches/flextile/) |
+
+### Also on the suckless site (not in the flexipatch bundle)
+| Layout | Description | Patch |
+|---|---|---|
+| **tatami** | Symmetric "tatami mat" arrangement. | https://dwm.suckless.org/patches/tatami/ |
+| **three-column / tcl** | Master in a center column with stacks on both sides; sized for wide screens. | https://dwm.suckless.org/patches/tcl/ |
+
+## Adjacent (not layouts, but pair with them)
+| Patch | Description | Patch |
+|---|---|---|
+| **vanitygaps** | Configurable inner/outer gaps between tiled windows and screen edges. | https://dwm.suckless.org/patches/vanitygaps/ |
+| **cfacts** | Per-client size weights within the stack (give some stack windows more space). | https://dwm.suckless.org/patches/cfacts/ |
+
+## Notes for the unbox design (to flesh out later)
+- The whole set reduces to one pure function shape:
+ `arrange(area: Box, clients: int, nmaster: int, mfact: float, params...) -> [Box]`.
+ That's the slice-7 core (100% doctest-coverable, zero wlroots types).
+- Touchscreen / iPad direction (CF-AX3): not all of these make sense for touch.
+ Likely-relevant shortlist to decide on: tile, monocle, deck, bstack,
+ centeredmaster, spiral/dwindle. Grids and flextile-deluxe are powerful but
+ keyboard-heavy. Decide the starter set when we have a baseline.
+- `flextile-deluxe` is the "one layout to rule them all" approach — worth
+ studying as a model for a single configurable engine vs. many discrete
+ layouts.
+- Gaps (vanitygaps) and per-client weights (cfacts) are orthogonal modifiers;
+ design the core so they're parameters, not separate layouts.
diff --git a/notes/tiling-spec.md b/notes/tiling-spec.md
new file mode 100644
index 0000000..bd736cf
--- /dev/null
+++ b/notes/tiling-spec.md
@@ -0,0 +1,164 @@
+# ext-window-tiling — baseline design (slice 7)
+
+> **Status: BASELINE DECIDED — build off this.** The agreed starting point for
+> the tiling extension. Layout catalogue to grow into later lives in
+> `notes/tiling-layouts-reference.md`. Open questions at the bottom are to be
+> resolved as we build.
+
+## Naming (replaces dwm's "master")
+
+dwm's "master/stack" → we use:
+
+- **primary** — the window that gets the most space / most attention.
+- **stack** — every other tiled window.
+
+Rationale: "master" is unclear (and carries master/slave baggage). "primary" is
+plain and position-agnostic (it works whether the big window is left or right).
+Considered alternatives: `hero`, `main` (fine), `focus` (REJECTED — collides
+with keyboard focus), `stage` (REJECTED — taken by the stage dock).
+
+**Pending: final user sign-off + `GLOSSARY.md` entry** for `primary` / `stack`
+(no-synonym-coinage rule).
+
+## The baseline layout
+
+A single default tiling layout. Mirror of dwm's `tile`; the primary column sits
+on the **right by default but the side is configurable** (`primary_side`), so it
+can be flipped to a left primary + right stack.
+
+- **New windows auto-tile** — opening a window drops it into the layout
+ automatically; no manual placement step. Where it lands in the stack (top or
+ bottom) is **configurable** (`new_window`).
+- Window-count behaviour:
+ - **1 window** → fills the whole area (fullscreen).
+ - **2 windows** → split 50/50: primary right half, the one stack window left
+ half.
+ - **3+ windows** → primary takes the right portion (`split_ratio` of the
+ width); the remaining windows share the LEFT portion, stacked vertically and
+ sized evenly.
+
+```
+ 1 window 2 windows 3+ windows
+ ┌──────────┐ ┌─────┬─────┐ ┌────┬────────┐
+ │ │ │ │ │ │ S1 │ │
+ │ P │ │ S │ P │ ├────┤ P │
+ │ │ │ │ │ │ S2 │ │
+ └──────────┘ └─────┴─────┘ ├────┤ │
+ │ S3 │ │
+ └────┴────────┘
+ P = primary (right) S = stack (left, vertical)
+```
+
+## The ordering model (decided)
+
+One **ordered list** of tiled windows. The **head of the list is the primary**;
+the remaining elements are the **stack**, rendered top-to-bottom in list order.
+
+- **New windows enter at the BOTTOM of the stack** (append to the list end) —
+ they do NOT steal the primary slot.
+- **The top of the stack is promoted to primary** when the primary slot is
+ vacated (the primary window closes or is moved out). Because the primary is
+ just the head of the list, removing the head makes the next element (the old
+ top-of-stack) the new primary automatically.
+- Net effect of the two defaults: opening windows never disturbs your current
+ primary; closing the primary hands the big slot to the longest-waiting stack
+ window (the one at the top).
+
+## Config (`unbox.toml`, `[tiling]` table)
+
+```toml
+[tiling]
+primary_side = "right" # "right" (default) or "left"
+new_window = "bottom" # "bottom" (default) or "top" of the stack
+split_ratio = 0.55 # fraction of width given to the primary column
+primary_count = 1 # windows in the primary area (baseline 1)
+```
+
+- **`primary_side`** — which side the primary column sits on. Default **right**.
+- **`new_window`** — where a newly-opened window enters the stack. Default
+ **bottom**. (`top` makes the newest window the first in the stack and thus the
+ next in line for promotion to primary.)
+- **`split_ratio`** (dwm `mfact`) — primary column width fraction. **Static
+ config for now** (no runtime keybind/drag yet — add when friction demands).
+- **`primary_count`** (dwm `nmaster`) — baseline **1**; model allows >1 later
+ without redesign.
+
+### Hot-reloadable (all four)
+
+All four values **live-reload** — edit `unbox.toml`, save, and the change applies
+with no restart. This reuses the existing kernel primitive
+`Host::watch_file(path, cb) -> FileWatch` that already backs ext-keybindings'
+config reload (RAII, coalesced, editor-save/create-safe, error-isolated; one
+session inotify is shared). ext-window-tiling watches the same `unbox.toml` and
+re-parses its own `[tiling]` table.
+
+On a save:
+- Re-read + re-parse `[tiling]` (pure). **Keep-old-on-bad:** a malformed /
+ mid-edit file keeps the current values and logs one warning — never drops a
+ working layout (same contract as the keybindings reload).
+- **`primary_side`, `split_ratio`, `primary_count`** take effect immediately:
+ swap the live values and **re-arrange every output's tiled set** that frame.
+- **`new_window`** updates the stored value but only affects windows opened
+ *after* the change (it governs insertion order, not existing windows) — no
+ re-arrange needed.
+
+The watcher holds no tiling state; it just hands new parsed values to the same
+pure core, so reload is a value swap + a re-arrange call (no re-subscribe, no
+window churn).
+
+## The pure core (slice-7 contract shape)
+
+Two pure pieces, both wlroots-free and 100% doctest-coverable (the slice-7 "pure
+decision core" rule):
+
+```
+# geometry: ordered window list -> one Box each, in list order
+arrange(area: Box, n: int, primary_count: int, split_ratio: float,
+ primary_side: Side) -> [Box]
+
+# list management: where a new window is inserted (top|bottom) and primary
+# promotion on removal — pure operations over the ordered list of opaque tokens
+```
+
+- `area` = the usable region (already minus the status bar / any reserved zones).
+- `arrange` returns one `Box` per window in list order (head = primary).
+- `new_window` (top/bottom) and promotion-on-remove govern the LIST order, not
+ the geometry — keep them separate from `arrange`.
+- Effects (assigning rects to real toplevels via the scene) live in thin glue.
+
+## Decided
+- **Primary side:** configurable (`primary_side`), default **right**.
+- **New-window insertion:** configurable (`new_window`), default **bottom** of
+ the stack.
+- **Primary promotion:** when the primary leaves, the **top of the stack** is
+ promoted to primary (automatic — it's the new list head).
+
+## Scope discipline
+
+Keep the baseline minimal and **fully automatic**: **NO new keybindings or
+gestures for tiling at this stage.** Windows auto-tile on open; the primary is
+the list head; that's it. Everything below is **deferred until real-use friction
+makes it the next thing to fix** — the WM is being dogfooded on the CF-AX3 and
+features get added incrementally, largest friction first.
+
+## Deferred until needed (NOT in baseline)
+
+- **Manual primary swap / move-in-stack** — promoting an arbitrary stack window
+ or reordering. (Needs a keybind/gesture → out of scope for now.)
+- **Runtime `split_ratio` / `primary_count` changes** — config-only for now.
+- **Focus + cycling across primary/stack** — existing ext-keybindings focus ring
+ (Alt+Tab) already moves focus; no tiling-specific focus controls added now.
+- **Floating exceptions** — dialogs / fixed-size / transient windows staying
+ floating. Confirm + implement when a real app needs it.
+- **Gaps** (dwm `vanitygaps`) — inner/outer gaps as a parameter (not a layout).
+- **Touch ergonomics (CF-AX3):** many stack windows → thin left column; cap stack
+ count or collapse to deck/monocle past N (a future second layout, not baseline).
+- **Interaction with the rest of unbox:** minimize-to-stage-dock removing a
+ window from the tiled set and restoring re-inserting it; home-screen /
+ fullscreen-app vs the tiled set.
+- **Per-output / per-workspace scope** — what tiling state is keyed to (workspace
+ model is itself a deferred decision — see plan.md §7).
+
+## Not in baseline (build off this later)
+See `notes/tiling-layouts-reference.md` — deck, monocle, bstack, centeredmaster,
+spiral/dwindle, grids, flextile-deluxe-style configurability, cfacts weights.
diff --git a/tasks.md b/tasks.md
index f2a4d2f..7b353fa 100644
--- a/tasks.md
+++ b/tasks.md
@@ -5,6 +5,19 @@
## Now
+**ACTIVE (core, user-driven) — Slice 13: RML COMPOSITING SPIKE.** 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.
+NEXT ACTION: write the spike brief (kernel/substrate) and summon it.
+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
@@ -76,8 +89,12 @@ real-seat feel pending):**
minimize-keybind migration (ext-keybindings action + a stage-dock Service), and
favicon (needs an XDG icon-theme dep) follow.
-Slice 6 (ext-taskbar + ext-launcher) is paused; the stage dock matures the same
-ui-substrate gaps it would have (list bindings, first real interactive consumer).
+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.
@@ -92,11 +109,14 @@ deprecated no-op `Options::ui_spike`, retiring host-bin's demo ui.
| 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 | First standard extensions: ext-taskbar + ext-launcher | pending | proves the ui-substrate contract is complete (friction = bad contract) |
-| 7 | ext-window-tiling: pure layout core + thin scene glue | pending | layout math 100% doctest-covered, zero wlroots types in core |
+| 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 | **THE SPIKE: 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. | **ACTIVE (core) — spike** | GO/NO-GO on the CF-AX3: (1) live toplevel texture in RmlUi via shared context, ZERO per-frame copy; (2) RCSS 3D transform on it; (3) pointer+touch+keyboard routed back through RmlUi picking → wl_seat; (4) window w/ popup+subsurface composited (decides per-subsurface-elements vs per-window RTT); (5) wallpaper as an element; (6) perf ~4 windows@1080p + idle≈no-work (our dirty-gating) + video cost; (7) present via existing FBO→scene_buffer bridge. Full spec + decision row: `notes/rml-compositing.md`, plan.md §2. |
## Deferred decisions (decide when reached — see notes/plan.md §7)