summaryrefslogtreecommitdiffhomepage
AgeCommit message (Collapse)Author
2026-06-15docs: close Phase 0 (GO) + Phase 1 architecture design for RML compositingspike/rml-compositingAdam Malczewski
Phase 0 spike is closed GO (real-seat confirmed: input accurate through the 3D transform, ~30fps fill-bound under the 4-window load). Records the design that Phase 1 settles, per the user's contract decision. - notes/rml-compositing-phase1.md (NEW): the Phase 1 design doc. - Contract principle: RCSS is the single source of truth for ALL layout + animation; C++ drives the document via a TYPED substrate API (reconciled with the constitution's typed-symbol rule, which governs cross-extension dependency discovery, not the RCSS vocabulary). - New kernel primitive: SurfaceElement = a LIVE analogue of Preview (zero-copy seq-gated import, frame-callback duty, per-subsurface tree). - Window layout = existing bind_list + RCSS pattern (stage-dock's, generalized). - Unified input-back folded into the substrate (the Element::Project mapping). - Damage limiting = Option B (buffer-age + wlr_damage_ring + scissor + set_buffer_with_damage), built properly here; scanout bypass deferred. - Cross-unit contract changes + Phase-2 wave plan. - User boundary decision: a NEW ext-window-field core extension owns the window field + layout; ext-xdg-shell only supplies toplevel handles + root wl_surface (Toplevel::scene_tree retired, wl_surface() added). - notes/plan.md row 71: DIRECTION -> ADOPTED/GO + the Option-2 contract. - tasks.md: slice 13 Phase 0 CLOSED; Phase 2 Wave 1 (kernel substrate) next.
2026-06-15spike(kernel): Stage-0 perf instrumentation (per-phase split + GPU timer)Adam Malczewski
Closes out Phase 0 with the measurement that explains the ~30fps: a per-frame budget breakdown and a non-blocking GPU timer query. - GlBridge: load EXT_disjoint_timer_query (glGenQueriesEXT/Begin/End/ GetQueryObject*), gated by the GL extension string; report gpu_timer in the bring-up line. Absent -> graceful 'n/a'. - PresentTarget::render(ctx, RenderTimings*): time clear/update/render-submit/ present (CPU wall-clock) and bracket ctx->Render() with a 2-deep timer-query ring read back a frame late, so the REAL GPU fill time is measured without a glFinish stall. delete_queries in teardown. - composite_frame: time the import+layout loop; accumulate all phases; emit a [perf-split] line (per-rendered-frame averages) alongside [perf]. Result (headless 1080p, Haswell+crocus == CF-AX3 GPU class): CPU work ~2ms (import 0.7 / update 0.7 / render-submit 0.8), GPU fill (ctx->Render) ~10-15ms, present dominated by the blocking fence wait. -> fill-bound; the whole-output composite is the wall. Damage limiting is the lever, to be built properly in the Phase 1 compositor (not hacked into this throwaway). --verify ALL PASS, kernel suite green.
2026-06-15spike(kernel): fix transform-incorrect click routing + live click-debug overlayAdam Malczewski
The bug: route_point mapped the cursor with transform-UNAWARE math — `(screen - GetAbsoluteOffset) / box`. screen coords are in transformed screen space; GetAbsoluteOffset is in untransformed layout space. They coincide only for an axis-aligned window, so plain --run tested fine, but on a --demo corner (perspective + rotateX/rotateY) they diverge and clicks landed in the wrong spot inside the client. --verify criterion 3 stayed green because it exercised a DIFFERENT, correct path (unproject_to_local) the live code never called. The fix: project the screen point onto the element's own plane FIRST via RmlUi's transform-aware Element::Project() (ray/plane intersect through the element's accumulated transform), then do the box->surface-local math in the matching space. Project() is a no-op when untransformed, so --run is unchanged. This is the live-path analogue of the pure-core inverse criterion 3 verifies. Debug tool (press D in --demo): a per-surface magenta crosshair drawn as a child of the hovered window, so RmlUi renders it THROUGH that window's own 3D transform — it sits exactly under the wlr cursor iff the mapping is correct, so any gap is the live click error, visible live (no screenshots). A #dbg readout prints screen -> elem-local -> surface-local (also to the diagnostic log). 'D' is consumed so it never types into the focused client. Confirmed accurate on the real CF-AX3 seat. --verify ALL PASS, kernel suite green, spike builds clean.
2026-06-15spike(kernel): --demo perf scenario — 4 corner windows + FPS HUD/logAdam Malczewski
New `--demo` mode for a real-seat perf load test (the Phase-0 perf measurement): - Four windows, one per screen corner, angled INWARD (perspective + per-corner rotateX/rotateY) — a curated layout, distinct from --run's single centered window. - Clients: 3x foot + 1x vivaldi (Chromium on Wayland via --ozone-platform=wayland; probes vivaldi-stable/vivaldi/vivaldi-snapshot, loud warning if absent). Replaces firefox, which rendered as an empty/clear box on this compositor. Each client claimed into the next free corner slot at map time (browser steered to its designated corner via app_id match; vivaldi/chromium/firefox all recognized); per-slot bookkeeping freed on unmap/destroy. - Live FPS HUD (RmlUi text) in the corner; per-5s min/max FPS written to $UNBOX_SPIKE_FPS_LOG (default $HOME/rml-spike-fps.log), fflush+fsync per line. - Demo dead-man defaults to 120s (P resets; Esc/Ctrl+Alt+Backspace quit) so an HD video can play while watching FPS. All existing behavior intact (live-update loop, input routing through the transform, cursor, escape hatch, persistent log). --verify ALL PASS, kernel suite green, spike builds clean (build_by_default:false, out of the shipped binary). Headless smoke: 3 foot + vivaldi map into 4 distinct corners; 5s min/max FPS lines written.
2026-06-14spike(kernel): RML compositing live + interactive on the real DRM seatAdam Malczewski
Iterated the Phase-0 spike from "self-verified headless" to "works on the CF-AX3": a live, 3D-tilted client window you can type into, with a visible cursor and pointer/touch routed through the transform. Real-seat confirmed. Safety / escape (a black-screen lockout had forced reboots): - session escape hatch on --run (mirrors src/input.cpp): Esc + Ctrl+Alt+Backspace quit, Ctrl+Alt+F1..F12 VT-switch, SIGINT/SIGTERM clean-terminate. - 15s DEAD-MAN auto-exit, reset by pressing P (also a keyboard-liveness probe); UNBOX_SPIKE_TIMEOUT overrides. Guarantees the spike can never lock the machine. - persistent crash-survivable log at $HOME/rml-spike.log (UNBOX_SPIKE_LOG), fflush+fsync per line (was on tmpfs /tmp -> lost on reboot); loud diagnostics for backend/modeset/client-connect/map/import. Black screen / no window fixes: - present path works on real DRM+gles2 (blue bg + marker confirmed on panel); dark-blue clear + marker rect make "empty vs not-presenting" diagnosable. - xdg wired from new_toplevel/new_popup (was new_surface -> no role -> no configure -> client never mapped); foot now maps as a live surface element. - WAYLAND_DISPLAY exported so spawned clients connect. Interactivity: - keyboard focus enter + key forward to the focused client (was never sent without a pre-existing keyboard device); typing reaches foot. - visible wlr-plane cursor (xcursor theme load + set; never drawn into RmlUi). - pointer + touch hit-tested via RmlUi pick through the 3D transform, mapped to surface-local, forwarded via wl_seat (criterion-3 pointer confirmed at edges). Live update loop (was frozen on frame #1): - send wlr_surface_send_frame_done to all mapped surfaces+subsurfaces each frame (client lives as an imported texture, not a wlr_scene node). - re-import the surface's CURRENT buffer per commit, gated on the surface commit SEQUENCE not the buffer pointer (foot recycles a buffer pool -> same pointer, new contents); double-buffered wlr_buffer lock/release so the client is never starved. reimports now track commits ~1:1 (was capped 3:1). --verify ALL PASS (criteria 1-7), kernel suite green, build + build-asan clean. Spike target stays build_by_default:false, out of the shipped binary.
2026-06-14spike(kernel): RML compositing Phase-0 — GO (self-verified on Haswell+crocus)Adam Malczewski
Throwaway, self-contained spike target `packages/kernel/rml-compositing-spike` (build_by_default:false, not in kernel_dep) proving RMLUi can composite live client windows. `--verify` reads back the framebuffer headlessly and asserts all 7 criteria; `--run` brings up a real/nested-seat compositor for the user's real-seat run. All 7 = ALL PASS on this dev box (an Intel Haswell-ULT iGPU on Mesa crocus — the CF-AX3's GPU class): 1 zero-copy live dmabuf→EGLImage→GL texture sampled by RmlUi, cached (0 reimport when buffer unchanged); 2 RCSS perspective+rotateY on the live pixels (verified by readback); 3 screen→surface-local inversion through the 3D transform, round-trip 0.000000 px (pure core doctested in the kernel suite); 4 surface tree (toplevel+subsurface+popup) composited correctly → recommend PER-SUBSURFACE elements (RTT escape-hatch for tree-spanning effects in Phase 1); 5 wallpaper (layer surface) via the identical import path; 6 idle dirty-gate = 0 renders over 120 idle turns, exactly 1 render per commit; 7 present path FBO→dmabuf swapchain→wlr_scene_buffer with an EGL fence (no glFinish). Crocus gotchas documented (linear modifiers, AR24 swizzle, cross-context fence, FBO Y-flip, re-import only on new buffer). Real-seat GO/NO-GO (3D/touch feel, frame-time @4 windows+video, idle power) is the user's call — runbook in reports/rml-compositing-spike.md §5. Also: GLOSSARY rows for "RML compositing" + "surface element" (user-confirmed); tasks.md slice 13 updated to spike-complete/GO pending real-seat. kernel suite green; full build clean; spike --verify ALL PASS.
2026-06-14docs: plan RML compositing direction + roadmap re-scopeAdam Malczewski
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.
2026-06-14ext-layer-shell: fix configure feedback loop; default Super toggles fuzzelAdam Malczewski
The intermittent "Super needs several presses to open fuzzel" bug was a layer-shell configure storm, not a keybinding issue. The per-surface commit handler called arrange() on EVERY wl_surface commit, and arrange() unconditionally calls wlr_scene_layer_surface_v1_configure, which in wlroots 0.20 emits a fresh configure each call. A client acking + applying its configure re-entered the commit handler, producing an unbounded configure -> ack_configure -> commit -> configure loop that overflowed the client's 4 KiB connection buffer ("Data too big for buffer") and got it killed mid-handshake. fuzzel therefore only displayed when the burst happened to flush in time. Fix: only re-arrange on the initial commit or a commit that changes a layout-relevant field (current.committed != 0); a plain buffer/frame commit no longer triggers a configure. Also: - ext-keybindings: tap-Super now defaults to a launcher TOGGLE ("pkill -x fuzzel || fuzzel") in both default_bindings() and the sample unbox.toml — pressing Super while fuzzel is open closes it. - ext-keybindings: a genuine Super press always re-arms a clean tap, so a dropped Super release can no longer eat the next tap (robustness). - tests: update default-command assertions; add a bounded-configure invariant guard to the layer-shell client test.
2026-06-14docs: add separator below logo, re-center logoHEADmainAdam Malczewski
2026-06-14docs: drop redundant title, left-align logoAdam Malczewski
The logo already includes the unbox wordmark, so the separate heading was redundant. Left-align the logo.
2026-06-14docs: add logo, refresh status, document extensionsAdam Malczewski
Add the unbox logo to the README header. Replace the stale planning/harness status with the current state (slices 0-5b done, stage dock landed). Add an Extensions table, useful CLI flags, and the asset hot-reload dev workflow.
2026-06-14docs/packaging: genericize build-infra references (remove host/user/paths)Adam Malczewski
Replace the specific builder hostname, username, home path, and the cited private methodology-source repo with generic wording across ORCHESTRATOR.md, notes/plan.md, tasks.md, and the PKGBUILD maintainer line. No machine/network identifiers remain in the tree.
2026-06-14packaging: remote-build helpers (distcc offload + remote pkg build)Adam Malczewski
Transparent distributed compilation: setup-distcc.sh configures ccache (prefix_command=distcc) + ~/.distcc/hosts locally and a locked-down distccd on a remote builder, so a plain ninja/meson test offloads compiles automatically and falls back to local when the remote is down. build-remote.sh (renamed from the machine-specific build-remote.sh) builds + installs the pacman package on the fast box. start-unbox now takes its wallpaper from $HOME/.config/unbox/wallpaper (or $UNBOX_WALLPAPER) instead of a hardcoded filename. Machine/network specifics (hosts, private IPs) are NOT committed: the helpers read packaging/remote.local (gitignored); packaging/remote.local.example is the committed template with setup instructions for a new network.
2026-06-14ext-stage-dock: C++-driven interruptible slide animationAdam Malczewski
RmlUi only starts a transition on a class/definition change, never on the inline data-style-transform the dock uses for slide, so keyboard/minimize/restore open-close had stopped animating (snapped). Own the animation in C++ instead: a pure SlideAnimator that every path flows through -- keyboard/minimize/restore play it, a touch drag scrubs it (pause + set position from the finger), and release resumes easing from the current position to the snap target. Duration + easing are read from the #panel RCSS 'transition' via transition_timing(), so they stay hot-reloadable and any named RmlUi tween works. Drives slide per frame via request_frames; the surface now hides when the close animation completes (replaces the old transitionend path). Body=drag-handle / panel=transform split preserved.
2026-06-14kernel: add request_frames() frame callback + UiSurface::transition_timing()Adam Malczewski
Two additive primitives for C++-driven, RCSS-tunable animation: - Host::request_frames(cb) -> FrameRequest: a per-frame callback (RAII handle) run before tick_all each frame; the kernel schedules frames continuously while >=1 request is alive and stops at rest. Fills the missing animation timer. - UiSurface::transition_timing(element_id, property): reads the RCSS-authored transition duration + easing, returning RmlUi's tween wrapped as a pure std::function (no RmlUi types cross the contract) so an extension can drive its own animation with hot-reloadable, designer-tunable timing/easing.
2026-06-14ext-stage-dock: use shader-based linear-gradient for the dock backgroundAdam Malczewski
horizontal-gradient is RmlUi's legacy decorator: it bakes the ramp into interpolated vertex colours through the plain colour shader, so it bypassed the gradient dither and showed hard 8-bit alpha banding over light backgrounds. Switch to linear-gradient(to right, ...), which renders per-pixel in our GL3 gradient shader (where the dither lives), giving a clean fade to transparent.
2026-06-14kernel: dither gradient shader to kill 8-bit banding (TPDF ~1 LSB)Adam Malczewski
The shader-based gradient (linear/radial) fragment shader now adds a mean-zero triangular-PDF dither (~1 LSB of 8-bit) from a spatial pixel-position hash before writing finalColor, so smooth ramps (e.g. an alpha fade) no longer show quantization banding when written to the 8-bit surface buffer. Spatial, non-temporal — no shimmer on static panels. Applies to every shader gradient.
2026-06-14ext-stage-dock: interactive touch edge-swipe to open/close the dockAdam Malczewski
Drag from the left edge to open, or drag the open dock back to close; finger-following with a 50%-or-fling snap on release. New gesture::Controller pure core converges both input paths onto one RevealRecognizer: OPEN via the kernel touch bus (dock hidden at down), CLOSE via UiSurface::bind_drag (the visible dock captures the touch). Slide is value-driven (data-style-transform) and eased only when not dragging. Two real-seat fixes found via per-frame logging: - Flicker: RmlUi projects a drag event's coords into the DRAGGED element's transformed frame. We had drag:drag on the same <body> we translate by slide, so the reported x fed back into slide and ping-ponged every frame. Fix: body is a stationary drag handle; an inner .panel carries the transform. - Direction: drag_start now seeds the recognizer from the dock's current fraction (1 + slide/width) instead of a hardcoded value, so a drag opens or closes correctly from any state.
2026-06-14kernel: add UiSurface::bind_drag (RmlUi drag events with surface-local coords)Adam Malczewski
Forwards RmlUi Dragstart/Drag/Dragend for a named callback as DragPhase {start,move,end} with surface-local x/y, so an extension can drive an interactive drag from a captured ui-surface touch (the touch bus never sees it). Mirrors bind_event's error-isolation + hot-reload handling.
2026-06-14packaging: pacman PKGBUILD + static toml++ + remote build scriptAdam Malczewski
- host-bin: install the `unbox` binary (install: true) so `meson install` (and the package) ship /usr/bin/unbox. Assets already install via the top-level install_subdir to /usr/share/unbox. - ext-keybindings: force the toml++ subproject to default_library=static. Its wrapped meson.build hardcodes default_library=shared, which made the installed binary NEED libtomlplusplus.so.3 at runtime (only resolvable in the dev tree via LD_LIBRARY_PATH). Static-linking bakes it in. - packaging/PKGBUILD: builds the working tree (reuses synced subprojects, no network), plain meson (no devtools needed), options=(!debug), and ALWAYS configures a fresh build dir — a reused/stale dir silently drops subproject option overrides (this is what reintroduced the shared-toml regression). - packaging/start-unbox: dbus-run-session -- unbox (mirrors start-labwc.sh). - packaging/build-remote.sh: rsync the tree to the fast box (builder), makepkg there, ferry the .pkg.tar.zst back, pacman -U here. Self-verifies the packaged + installed binary has no unresolved/shared-toml dependency (extracting to a real file — readelf can't read a non-seekable pipe). Runtime NEEDED is now only system libs (wlroots-0.20, wayland, xkbcommon, freetype, EGL, GLESv2, libstdc++/m/gcc_s/c) — installs + runs on the CF-AX3.
2026-06-14dock: increase slide transition to 0.36s cubic-in-outAdam Malczewski
2026-06-14ext-keybindings + ext-stage-dock: config-driven dock-toggle-visible (Super+d)Adam Malczewski
- **ext-stage-dock**: exports a `Service` interface with `toggle_visible()`. The extension inherits from it and registers via `provide_service` in `activate()`. The method slides the dock in/out (using the existing RCSS transition) regardless of slot count; showing an empty dock is valid. - **ext-keybindings**: new `Action::dock_toggle_visible` action, mapped from `"dock-toggle-visible"` in `unbox.toml [[keybind]]`, dispatched to the stage-dock Service. Default binding: `Super+d`. - **Manifest**: ext-keybindings now `depends_on {"xdg-shell", "stage-dock"}`. - **Build**: subdir order swapped so ext-stage-dock builds before ext-keybindings; `ext_stage_dock_dep` is a link-time dep of the ext-keybindings library and transitively exposed via `ext_keybindings_dep`. - **Tests**: glue tests install ext-stage-dock alongside ext-xdg-shell; policy test expects 6 default bindings. All 10/10 green on build + build-asan. Configure in unbox.toml: [[keybind]] keys = "Super+d" action = "dock-toggle-visible"
2026-06-14ext-stage-dock: full-height left rail — 288px wide, gradient, centered cardsAdam Malczewski
Turn the card-hugging dock into a full-height left rail. - C++ (extension.cpp/dock_layout.hpp): the surface is now the full OUTPUT HEIGHT (hug reverted; content_height helpers/tests dropped) and kDockWidth widened 240 -> 288 (~20%). Cards unchanged at 224dp. set_visible still hides the rail when empty, so it only appears when there are minimized windows. - RCSS (dock.rcss): body.dock fills the surface (width:100% height:100%) as a flex COLUMN scroll container with a `horizontal-gradient(#000000ff #00000000)` decorator — fully-opaque black at the left screen edge fading to transparent across the wider rail. Cards centered both axes (align-items:center + a `div.rail` margin:auto wrapper that vertically centers when they fit and collapses gracefully — no flex-center+overflow strand — scrolling when they overflow). Scrollbars hidden (scrollbarvertical/horizontal size 0). - RML (dock.rml): a `div.rail` wrapper around the data-for slot list enables the center-or-scroll pattern. Accepted caveat (quick path): the full-height 288px surface consumes pointer/touch across the left strip while shown; the deferred input-transparent UiSurfaceSpec flag is the real fix. Real-seat verified via live RML/RCSS hot-reload. ext-stage-dock 2/2 green on build + build-asan.
2026-06-13kernel: fix asset hot-reload regression (watch the whole asset dir, not the ↵Adam Malczewski
.rml basename) The watch_file refactor (35e5d32) moved the substrate's UI-asset hot-reload onto the shared FileWatcher but armed a BASENAME watch on the document's .rml file only. The dock's styling lives in a separately-<link>ed dock.rcss, so editing it (the common case) never matched the watch — asset hot-reload silently stopped working on the real seat (no "dev hot-reload ON" line, no reload on save), while config watching kept working. The ui_reload_surface() seam test passed because it bypassed the real inotify->reload path. Fix: FileWatcher::add_dir watches the document's whole DIRECTORY (so any .rml/.rcss in it triggers the surface reload); the substrate uses it and restores the "dev hot-reload ON (inotify watching asset dir '...')" log. Added an END-TO-END test mirroring the dock (a doc that <link>s a separate .rcss, real inotify event, wl_event_loop pumped, assert the document actually reloaded) — fails on the buggy code, passes now; no more relying on the seam. Real-seat verified: editing dock.rcss now reloads the live dock (border-radius + background-color changes apply on save). kernel 59 cases/260 assertions green on build + build-asan, no new suppressions. Edits confined to packages/kernel/.
2026-06-13ext-stage-dock: dock background 50% transparent black (#00000080)Adam Malczewski
2026-06-13tasks: record config (unbox.toml) hot-reload + watch_file primitiveAdam Malczewski
2026-06-13ext-keybindings: hot-reload unbox.toml (live config, no restart)Adam Malczewski
Editing the config now re-applies keybindings live, via the kernel's watch_file service. In activate() we watch the effective config path (the create() arg, else ~/.config/unbox/unbox.toml) — even if it doesn't exist yet, so creating it later is picked up — holding the FileWatch as a member. On change, reload_config() re-reads + re-parses (the existing pure toml core) and SWAPs the live binding table the key_filter link matches against (matcher_), so new bindings apply with no re-subscribe. A malformed / unreadable / mid-edit-broken file KEEPS the current bindings and logs one warning — the session never loses working keys, never throws. Real-seat verified: editing the command (fuzzel->foot) logged "config reloaded (5 binding(s))" live; a deliberately broken file logged "reload failed; keeping current bindings" with the session staying ALIVE; restoring it recovered. Added a pure reload-semantics doctest (A->B swap; malformed keeps prior). ext-keybindings 2/2 green on build + build-asan. Edits confined to packages/ext-keybindings/.
2026-06-13kernel: generalize the inotify watcher into a Host::watch_file serviceAdam Malczewski
The hot-reload watcher was substrate-internal; expose it as a typed RAII primitive any extension can use (config hot-reload is the first consumer), per "the kernel owns the event/service bus; extensions never hold raw event-loop glue". - New public watch.hpp: `class FileWatch` (move-only RAII; ~/reset() stop the watch) + `Host::watch_file(path, on_change) -> FileWatch`. on_change fires on the event-loop thread, COALESCED (one save = one call), EDITOR-SAFE (dir-watch the basename across temp+rename), fires on CREATE of a not-yet-existing file, and is ERROR-ISOLATED to the calling extension (carries its id; a throw disables only that extension). UNGATED — works without UNBOX_DEV. - New src/file_watcher.{hpp,cpp}: ONE session-wide inotify instance on the wl_event_loop multiplexing all watched paths. The substrate's UI-asset hot-reload was refactored onto it (no second inotify); only the substrate's *decision* to watch UI assets stays UNBOX_DEV-gated. Created lazily on first watch; torn down leak-clean before the loop dies. host.hpp/kernel.md documented. kernel 58 cases/254 assertions green on build + build-asan (incl. the inotify path), no new suppressions. Edits confined to packages/kernel/.
2026-06-13tasks: record RML/RCSS hot-reload dev workflowAdam Malczewski
2026-06-13ext-stage-dock + build: externalize the dock document to assets/ ↵Adam Malczewski
(hot-reloadable) - The dock's inline kDockRml C++ string is gone; the document now lives in assets/ext-stage-dock/dock.rml (structure) + dock.rcss (styles), loaded via UiSurfaceSpec::rml_path = "ext-stage-dock/dock.rml". The bind_list*/event setup in activate() is unchanged (the substrate re-applies it across hot-reloads). - Build wiring (top-level meson.build): install_subdir the top-level assets/ tree to <datadir>/unbox/<unit>/, and -DUNBOX_ASSET_DIR_DEFAULT=<prefix>/<datadir>/unbox so an installed unbox finds its assets with no env. Dev runs set UNBOX_ASSET_DIR=<repo>/assets + UNBOX_DEV=1 to read the source tree and arm the hot-reload watcher. Real-seat verified: editing dock.rcss (border-radius 10dp<->70dp) updates the live dock with NO recompile and NO restart. ext-stage-dock 2/2 green on build + build-asan. Design iteration on the dock is now edit-a-file.
2026-06-13kernel: load ui surfaces from RML asset files + dev hot-reloadAdam Malczewski
Externalize UI documents so RML/RCSS design changes need no C++ recompile — and, in dev, no restart. - UiSurfaceSpec::rml_path now actually loads the document from a file (path wins over rml_inline, as documented). Resolution: absolute path as-is; relative path against $UNBOX_ASSET_DIR, else the compile-time UNBOX_ASSET_DIR_DEFAULT (the install data dir), else cwd. The document URL is set so its <link> RCSS / asset refs resolve relative to the doc's own dir. Missing/unreadable file -> nullptr (degrade, never throw). - Dev hot-reload (gated by $UNBOX_DEV): an inotify watcher integrated into the wl_event_loop (never blocks) watches the asset DIRS (dir-watch for IN_CLOSE_WRITE / IN_MOVED_TO, since editors save via temp+rename), coalesces events, and on a change to a surface's backing .rml/.rcss reloads the document IN PLACE: ClearStyleSheetCache + UnloadDocument + reload, preserving the surface's RmlUi context, data model and the extension's registered bind_*/bind_list* getters (the extension does NOT re-register), and its geometry/visibility; preview textures are kept. A malformed file on reload is ERROR-ISOLATED — the previous good document keeps rendering, one warning is logged, and a later good save recovers; the session never dies. - Test seam Server::ui_reload_surface() drives reload deterministically. ui.hpp documents rml_path + the dev hot-reload behavior. kernel 54 cases/232 assertions green on build + build-asan (incl. the UNBOX_DEV inotify path), no new suppressions. Edits confined to packages/kernel/.
2026-06-13ext-stage-dock: thumbnail fully covers the card (fix right-edge placeholder ↵Adam Malczewski
sliver) Real-seat pixel sampling showed a ~2px vertical sliver of the slot placeholder (#2e2e32) on the card's RIGHT edge only (full height; other edges flush). A magenta diagnostic background on the thumb proved the gap was NOT a transparent texture edge (it stayed placeholder-colored, not magenta) — the thumb BOX was ~2px short on the right (RmlUi box rounding anchored top-left). The preview texture import is correct. Fix: the full-bleed thumb now overscans the slot by -2dp on all sides; the rounded overflow:hidden slot clips the overscan, so the image covers the whole rounded card with no placeholder edge on any side and corners stay rounded. RCSS-only. Verified real-seat: image reaches all four edges, zero placeholder sliver. ext-stage-dock 2/2 green on build + build-asan. Also records the transparency/ card-redesign milestone in tasks.md.
2026-06-13ext-stage-dock: card is a rounded thumbnail of the windowAdam Malczewski
Replaces the panel-with-inset-image card with the window preview as the card itself, rounded on all four corners. - div.slot is a rounded overflow:hidden clip container (explicit 224x140dp box, #2e2e32 rounded placeholder for not-yet-rendered previews). - A full-bleed child <div class="thumb"> carries the preview via data-style-decorator="'image( ' + row.preview + ' )'" with `cover center center` (shorthand order verified against vendored RmlUi DecoratorTiled). As a child of the rounded overflow:hidden slot, its image clips to the rounded corners — RmlUi does NOT clip an element's own decorator to its own border-radius, so the decorator must live on the clipped child. - Title overlay disabled (display:none) but kept in the markup + the {{ row.title }} binding/getter stay live, for a later text redesign. - Card height 140 (kCardHeight); surface-hug content heights 156/304/600 updated. - Preserved: d1 slot-enter animation, transparent strip, surface hugs the stack. Real-seat verified: a clean rounded window thumbnail in the dock. ext-stage-dock 2/2 green on build + build-asan.
2026-06-13kernel: regression tests for RmlUi clipping (scissor + stencil clip-mask)Adam Malczewski
The stage dock's rounded image cards were the first thing to exercise the substrate's RmlUi clipping path (the slice-3 spike doc never used overflow / border-radius). Investigated on report of square corners: the renderer's clipping is CORRECT — added 4 headless+gles2 cases proving it (shm + dmabuf, asymmetric so a flipped scissor Y would fail): - overflow:hidden parent clips an oversized child (outside bands transparent); - border-radius circle clip-mask rounds the corners (corners transparent); - an image() decorator on a CHILD of a rounded overflow:hidden card clips to the rounded shape (the dock's correct structure); - a transformed rounded clip survives a set_size grow (the dock's real path). The square-corner bug was RmlUi-core behavior — an element's own image() decorator is not clipped to its own border-radius (a background-color rounds by geometry; a decorator needs the clip mask, which self-render never sets) — fixed in the dock by moving the decorator to a child. No renderer/substrate source change. kernel 49 cases/208 assertions green on build + build-asan.
2026-06-13notes: spec dock favicons (XDG icon lookup + lunasvg/stb_image decode); ↵Adam Malczewski
defer in plan §7
2026-06-13tasks: stage dock transparency + usability pass real-seat-verifiedAdam Malczewski
2026-06-13ext-stage-dock: transparent strip, surface hugs cards, fix re-minimize after ↵Adam Malczewski
empty Builds on the kernel per-pixel-alpha + set_size-resize capabilities. - Transparent strip: body.dock background #1c1c1ee6 -> transparent, so the windows beneath show through everywhere the cards don't cover; cards keep their solid #2e2e32 panel. data-attr-src preview, Noto Sans, and the d1 slot-enter animation are intact. - Surface hugs the card stack: height = surface_height(count) = max(1, 2*pad + count*card + (count-1)*gap) (0->1px hidden, 1->140, 2->272, …), never the full output height, so the transparent area doesn't needlessly capture input. The empty dock is a positive 1px hidden placeholder (the substrate rejects 0 geometry); grows/shrinks via set_size on minimize/restore. - Fix: re-minimize after the dock empties was a no-op. do_restore relied on on_toplevel_focused re-firing, but a restored window was never seat-defocused, so focus() is a seat no-op and the event never fires — leaving focused_ stale, so the next Super+M's focused_ guard rejected it (a new toplevel mapping unstuck it). Fix: set focused_ = tl directly in restore. No kernel change. Tests: new policy cases (surface_height always positive; hug heights 0/140/272/536) and a glue minimize->restore->minimize 1->0->1 cycle with has_focused() probe. ext-stage-dock 2/2 green on build + build-asan (no sanitizer reports). Real-seat verified: transparent strip, dock visible, cards float, re-minimize works. Edits confined to packages/ext-stage-dock/.
2026-06-13kernel: ui surfaces composite with per-pixel alpha + set_size resizes the targetAdam Malczewski
Two substrate capabilities the stage dock forced (both verified real-seat nested and on the gles2 headless path): 1. Per-pixel alpha. A ui surface composited opaque, so any overlay (the dock) occluded the toplevels beneath it. Root cause: a stray opaque render_iface->Clear() (glClearColor 0,0,0,1) in render_surface overrode the transparent BeginFrame clear, and EndFrame's premultiplied composite carried the opaque base to the buffer. Fix: drop the stray Clear(); clear the OUTPUT FBO to (0,0,0,0) once before BeginFrame. Blend was already correct premultiplied; the substrate never sets an opaque region (now guarded by a probe); ARGB8888 alpha survives end to end. A document whose <body> is transparent now shows the scene through its un-painted pixels. 2. set_size resizes the render target. Previously logical-only (the slice-5 documented change-request): set_size re-laid-out the RmlUi document but did NOT realloc the GL target, so a surface created small and grown rendered into its original buffer (the dock, created as a 1px placeholder and grown on minimize, was invisible). Fix: set_size now reallocs the FBO + dmabuf swapchain/shm + EGLImage + texture + scene buffer on an ACTUAL size change (no-op same-size, cheap; set_position still cheap). Grow and shrink both render fully; alpha/upright-flip/blend/fence-sync preserved. ui.hpp documents both. kernel 45 cases/182 assertions green on build + build-asan (no new suppressions). Edits confined to packages/kernel/.
2026-06-13tasks: stage dock previews real-seat-verified (data-attr-src fix)Adam Malczewski
2026-06-13Slice 10 c2/d1 fix: dock previews were blank (RML/RCSS authoring bugs)Adam Malczewski
Real-seat (nested) showed the dock compositing but slots blank. Three bugs in the inline dock document, all caught in the live RmlUi log: - <img src="{{ row.preview }}"> never bound — RmlUi interpolates {{ }} only in TEXT, not attributes, so it tried to load a texture literally named "{{ row.preview }}". Fixed with data-attr-src="row.preview". - font-family: sans-serif -> "No font face defined" (substrate only loads Noto Sans). Fixed to the loaded face. - transform-origin: top left -> RCSS parse error. Fixed to RmlUi-valid syntax. Verified real-seat: minimizing two foot windows shows two preview cards in the dock with the actual window snapshots visible; the live log is free of the three [rmlui] errors. Super+M works repeatedly with >1 window ("works once" was the single-window no-op: nothing left to focus/minimize). Suites green build + asan.
2026-06-13tasks: slice 10 a1-d1 landed (stage dock); real-seat + 1 boundary call nextAdam Malczewski
2026-06-13Slice 10 d1: RCSS animation — dock slide-in + per-slot settleAdam Malczewski
Layers animation on c2's static pipeline with zero data-model / contract change (c2 glue test passes unchanged). The dock body slides in from the left edge on empty->non-empty via an RCSS transition on transform:translateX driven by a bound `open` bool (data-class-open); slide-out defers set_visible(false) until the slide's transitionend so the close is seen. Each freshly instanced preview card plays a one-shot @keyframes (scale 0.72->1.0 + fade, transform-origin top-left = "scales down into a spot"). Dock stays a 240px left-edge strip (animation is a transform inside the surface, never a resize) so windows stay clickable. Key finding: RmlUi 6.2 dispatches animationend/transitionend as real events, and the substrate's data-event-* path binds a listener for ANY registered event — so data-event-transitionend routes into bind_event with NO kernel change. The ONLY remaining gap for the literal cross-screen window->dock flight is a single input-transparent UiSurfaceSpec flag (a kernel boundary decision, NOT built). Restore stays instant in d1 (animated grow-back is e1's drag-out). 10/10 suites green on build + build-asan (asan clean). Visual feel is real-seat.
2026-06-13Slice 10 c2: stage dock end-to-end (minimize -> preview -> restore)Adam Malczewski
ext-stage-dock activate() now wires the full static pipeline: track toplevels via the ext-xdg-shell Service; Super+M (stopgap keybinding) minimizes the focused window -> snapshot a Preview from its scene_tree(), push a slot, hide() the live node, refocus another window; the dock is one overlay UiSurface rendering the slots via the b2 list bindings (data-for over {preview src, title}); tapping a slot fires bind_list_event -> show()+focus() the window and drop its slot/Preview. Storing the Toplevel* across minimize is safe because hide() keeps it mapped; slots are dropped on on_toplevel_unmapped. Teardown is reverse-declaration-order (subscriptions first, surface before slots), asan-clean. host-bin installs ext-stage-dock (standard, depends_on xdg-shell; hidden until it holds a minimized window). Headless glue test (real in-process xdg client) proves the model + scene-node enable bit true->false->true, slots 0->1->0. RML carries dock/slot classes as d1's RCSS animation hooks. Stopgaps: Super+M (to migrate into an ext-keybindings action + a stage-dock Service post-d1); favicon deferred (needs an XDG icon-theme dep — to surface to the user). 10/10 suites green on build + build-asan. Visual/tap path is real-seat (pending).
2026-06-13Slice 10 b4: ext-stage-dock new unit — skeleton + pure coresAdam Malczewski
New standard extension (id "stage-dock", depends_on xdg-shell). Ships the unit skeleton (factory-only public header, minimal no-op activate, meson + suite) and the two PURE DECISION CORES, doctest-hard with no kernel/wlroots: - src/reveal.hpp: reversible left-edge swipe recognizer (stream -> reveal fraction [0,1]; release -> open/close commit by threshold + fling velocity). - src/dock_layout.hpp: reveal-fraction -> on-screen dock box (slides -dock_width .. 0) + slot capacity / content-height / per-slot rect math. Real wiring (RML doc, snapshot, minimize/restore) lands in c2/d1/e1. Root meson.build gains the subdir; host-bin registration deferred to c2. ext-stage-dock suite green (17 cases / 74 assertions). Edits in packages/ext-stage-dock/ + root meson.build subdir.
2026-06-13Slice 10 b1: ext-xdg-shell Toplevel minimize mechanismAdam Malczewski
Neutral compositor-side mechanism the stage dock drives to minimize a window (the "minimized" state/policy stays in ext-stage-dock). Adds to Toplevel: geometry() -> wlr_box (size the preview + restore position), scene_tree() -> wlr_scene_tree* (feed UiSubstrate::create_preview + the node hide/show toggles), and hide()/show() — disable/enable the scene node so the client stops compositing and frame callbacks WITHOUT unmapping (no on_toplevel_unmapped, no focus change), idempotent. Verified against wlroots 0.20 that a disabled node quiesces frames. A real in-process wayland client test (test_minimize.cpp) drives it on the headless backend. ext-xdg-shell suite green on build + build-asan. Edits confined to packages/ext-xdg-shell/.
2026-06-13Slice 10 b2: UiSurface list/container data bindingsAdam Malczewski
The stage dock is one RML document rendering a variable list of slots (one per minimized window). Adds the deferred slice-6 list-binding shape to UiSurface: bind_list(name, count) + typed per-row fields bind_list_string/int/double/bool (list, field, getter(row)) read as {{ row.field }} via data-for, and bind_list_event(list, event, callback(row)) routed from data-event-*(it_index). dirty(<list>) re-reads count + visible rows. Same error-isolation + bind-before- first-frame contract as the scalar bindings; nested lists unsupported. kernel suite green on build + build-asan (asan clean). Edits confined to packages/kernel/.
2026-06-13Slice 10 a1: preview pipeline spike — wlr pixels -> RMLUi texture (Fork-B GO)Adam Malczewski
The keystone for the stage dock. Proves Fork B on the real target (Mesa crocus, HD 4400): a toplevel's pixels, rendered by the wlr GLES2 renderer into a LINEAR ARGB8888 dmabuf, import as an EGLImage -> sampled GL texture in the sibling RMLUi GLES 3.2 context (the slice-3 bridge run in reverse) and composite into an <img src="unbox-preview://N"> inside a ui surface — upright, color-correct. Public surface (ui.hpp): class Preview (source_uri/source_width/source_height/ refresh) + UiSubstrate::create_preview(wlr_scene_tree*) -> unique_ptr<Preview> (nullptr if no GL path; never throws). Kernel-suite probes: ui_preview_import_is_dmabuf, ui_pixel(x,y). Clean four-resource teardown (URI reg, GL texture, EGLImage, dmabuf); refresh-after-source-destruction is UB (consumer drops Preview on unmap). kernel 42 cases/150 assertions green on build + build-asan (asan clean, no new suppressions). Edits confined to packages/kernel/.
2026-06-13Stage dock: land plan + vocabulary (Fork B, mechanism/policy split)Adam Malczewski
Slice 10 (stage dock): Stage-Manager-style left-edge dock of minimized-window previews revealed by a left-edge swipe. Records the Fork-B decision (previews are toplevel snapshots imported as textures into the RMLUi context, shown as <img> in one RML document) and the mechanism/policy split (kernel: snapshot + list bindings + gesture-claim; ext-xdg-shell: hide/show/geometry/scene_tree; ext-stage-dock: minimized set, layout, recognition, easing). Adds canonical terms: stage dock, stage (reserved), preview, favicon, gesture, swipe, minimize, restore, reveal.
2026-06-13docs: record kernel-hardwired VT switching (plan §2, tasks)Adam Malczewski
2026-06-13Kernel: Ctrl+Alt+F1..F12 VT switching (session escape hatch)Adam Malczewski
Intercept the XF86Switch_VT_1..12 keysyms before the keybinding filter and call wlr_session_change_vt, so the user can always switch consoles while unbox runs. Clean no-op without a session (headless/nested). Pure vt_for_keysym helper + doctest; wlroots reached via the wlr.hpp wrapper. Real-seat verified on the CF-AX3.