summaryrefslogtreecommitdiffhomepage
AgeCommit message (Collapse)Author
2026-06-15rules: add test-determinism (condition-based client pumping, no thread+sleep)feat/rml-compositingAdam Malczewski
Crystallizes the scar behind the Wave-1b de-flake: headless integration tests must drive a real client by condition-based event-loop pumping, never a free- running thread + time budget (which starves under load -> sentinel reads / silent skips / drifting assertion count).
2026-06-15kernel/tests: de-flake the Wave-1b input-back integration testAdam Malczewski
The surface-element tree + input-back case drove a real xdg client from a free- running thread; under load that thread was starved, so its async receive assertions (ptr/touch/keyboard enter + coords) either read sentinel values and FAILED or the whole body silently SKIPPED -- the suite's assertion count oscillated (375 skip / 407 race) and the kernel gate was unreliable. Drive the client deterministically with condition-based cooperative event-loop pumping (no time-based waits, no skip), so the input-back / tree / transformed-pick / keyboard / press-hook coverage runs and passes every time. No production change. Verified: 8 consecutive normal runs + asan all OK with an identical doctest assertion count (391, 0 failed) every run.
2026-06-15docs: record screenshot + wallpaper decisions (plan §2, glossary, tasks)Adam Malczewski
- notes/plan.md §2: three settled-decision rows (screencopy/xdg-output in kernel; stb_image arbitrary-image decode + absolute-path fix; native ext-wallpaper + input_transparent flag + bundled default). - GLOSSARY: canonical 'wallpaper' term (user-confirmed). - tasks.md: 'just landed' milestone for screenshots + wallpaper.
2026-06-15ext-wallpaper: config-driven desktop wallpaper (new standard-tier unit)Adam Malczewski
A new standard-tier extension that composites a desktop background in the 'background' scene layer, below every window. Reads [wallpaper] from unbox.toml (path / fit = cover|contain|stretch|center / color), hot-reloaded via watch_file (drop + recreate an inline RCSS document with the values baked in -- avoids fragile decorator data-binding). The surface is input_transparent so it never steals clicks. Pure doctested config core; factory-only contract. When no path is configured it falls back to a bundled default image (assets/ext-wallpaper/default.jpg, auto-installed to the data dir), resolved to an absolute path via $UNBOX_ASSET_DIR or the UNBOX_ASSET_DIR_DEFAULT compile define. Degrades gracefully (color-only) if there is no GL path or the file is missing. Sized to the primary output (multi-output is a documented gap). Wiring (composition root): root meson subdir, host-bin installs it unconditionally with the config path, unbox.toml gains a documented [wallpaper] sample. (swaybg via ext-layer-shell's background band keeps working too.)
2026-06-15kernel: screenshot protocols + arbitrary-image decoding + input-transparent ↵Adam Malczewski
surfaces Three additive, kernel-internal capabilities (no extension-facing signature changes beyond the documented UiSurfaceSpec field + SurfaceElement-unrelated probes): - Screenshots (grim): create wlr_screencopy_manager_v1 + wlr_xdg_output_manager_v1 in init() alongside the existing compositor/data-device globals (policy-free plumbing; wlroots wires them to the kernel-owned outputs/renderer). The captured image is the standard wlr_scene_output_commit composite, so RML-composited documents (scene-buffer nodes) are captured correctly. wlr.hpp gains the two headers (static-blanking re-audited inert). - Arbitrary raster image decode: vendor stb_image (single-header, public domain; warnings isolated to its own warning_level=0 TU) and extend the RMLUi RenderInterface LoadTexture (was uncompressed-TGA-only) to decode PNG/JPEG/BMP/ GIF/TGA via stbi_load_from_memory (RGBA, no BGR swizzle), falling back to the legacy TGA path. A SubstrateSystemInterface JoinPath override stops RmlUi stripping the leading '/' of an absolute source during URL resolution, so both <img src='/abs'> and decorator: image('/abs') load the same file. Deterministic ui_pixel readback tests (<img> + decorator paths, red/blue PNG fixtures). - input-transparent ui surfaces: UiSurfaceSpec gains 'bool input_transparent' (default false). When true the surface still composites but is skipped by the press-ownership hit test, so it never steals pointer/touch from windows above it -- required for a full-screen background (wallpaper). Deterministic seam test proves a transparent surface does not consume a press while an opaque one does.
2026-06-15ext-window-field(rml-compositing): floating windows + resize-to-tileAdam Malczewski
Floating windows: move (titlebar), resize (two bottom corner grips), and a per- window close button. Per-window geometry (x,y,w,h,z) is bound STATE applied via data-style left/top/width/height + z-index; a pure, doctested geometry core (move / resize_bl / resize_br with anchored-opposite-edge + min-size, field clamp) computes drag results. Positioning uses left/top (NOT a transform: RmlUi's data-style does not apply a transform here the way it does left/top/width/height, and a transform would also offset the drag hit-test). z-order raise on focus; cascade placement from the client's own size. Resize-to-tile: a frame-pumped feedback loop configures each client to its on- screen box (Toplevel::set_size) so the live texture maps 1:1. Policy is config- driven -- [window-field] resize_mode = off|settle|continuous|debounced in unbox.toml, hot-reloaded; pure doctested config core. Windows are shown via <img data-attr-src> (NOT an RCSS image() decorator) so the kernel's surface-element input-back, click-to-focus, popup placement and box readback -- all keyed off the <img> src -- work. host-bin passes the config path + --rml-compositing (the latter selecting server- side decorations) through to the extensions. tasks.md records the wave.
2026-06-15ext-xdg-shell(rml-compositing): Toplevel::set_size + server-side decorationsAdam Malczewski
- Toplevel::set_size(w,h): expose wlr_xdg_toplevel_set_size (async configure; 0 = client picks). Lets the window field size a client to its tile. - Advertise xdg-decoration and force SERVER_SIDE when the RML field draws chrome (create(prefer_server_side_decorations), wired from host-bin's --rml-compositing) so clients drop their own CSD titlebars; classic path keeps CSD. The mode is applied only once the xdg surface is initialized (on the surface commit / request_mode), avoiding a wlr_xdg_surface_schedule_configure assertion when the client creates the decoration before its initial commit.
2026-06-15kernel(rml-compositing): surface-element box readback, per-row drag, ↵Adam Malczewski
xdg-decoration - SurfaceElement::rendered_width()/height(): read back the RCSS-resolved root <img> content box (the substrate already computes it for popup placement) so a window manager can size a client to its on-screen tile. Reading geometry, not computing it -- RCSS still owns layout. - UiSurface::bind_list_drag(list, name, cb(row, phase, x, y)): the per-row drag binding (list analogue of bind_drag + bind_list_event), for per-window move and resize in a data-for list. - Expose wlr_xdg_decoration_v1 through the wlr wrapper (for ext-xdg-shell's server-side-decoration negotiation).
2026-06-15rml-compositing: make the window field actually render (2 real bugs)Adam Malczewski
Diagnosed live (nested unbox + grim screenshots). Two bugs stopped any window from showing; both fixed and visually confirmed (multi-window, live-updating, focus highlight, RCSS tiling). 1. kernel (ui_substrate.cpp, adopt_node) — STABLE TEXTURE ID across re-imports. The live import did glDeleteTextures+glGenTextures every commit, minting a NEW GL id each frame. But RmlUi's image()/<img> decorator caches the texture HANDLE it gets from LoadTexture(uri) ONCE, so after the first re-import it drew a DELETED texture -> blank. Now node.tex is allocated ONCE and just re-pointed at the new EGLImage (dmabuf) / re-uploaded (shm) on each re-import, destroying the previous EGLImage after the rebind. The id stays constant => RmlUi's cached handle stays valid => the window updates live. (Reimport counting unchanged.) 2. ext-window-field assets — the window texture is painted via an RCSS image() DECORATOR bound with data-style-decorator (the stage-dock pattern), NOT an <img src="{{ w.live_uri }}">: RmlUi does not substitute a data binding inside an img src, so the literal "{{ w.live_uri }}" was reaching the texture loader ("Could not load texture"). Also `contain` (was `cover`) so the whole window shows instead of cropping its center. Verified: foot composites as a surface element in the window field and updates live (a ticking clock); a 2nd window tiles with the focused one highlighted. kernel + ext-window-field suites green; build-asan kernel green (no leak/UB). Follow-up: a kernel regression test asserting the texture id is stable across re-imports (no test caught this — RmlUi caching is the subtlety).
2026-06-15docs(tasks): RML compositing Waves 1b-3 + click-to-focus done; next = ↵Adam Malczewski
real-seat verify + W4/W5 decisions
2026-06-15rml-compositing W3: click/tap-to-focus (kernel on_pressed + window-field)Adam Malczewski
Completes the window-manager input story. kernel (additive): SurfaceElement::on_pressed(std::function<void()>) — the substrate invokes it (error-isolated to the owner) when a pointer button PRESS or touch DOWN is routed to that surface element (root or any subsurface/popup child fires the root element's handler), in addition to the existing client input-back forwarding. Not fired on motion/release/miss. It is the click/tap-to-focus SIGNAL; focus policy stays the wm's. ext-window-field: on map, sets the element's on_pressed -> that toplevel's Toplevel::focus() (keyboard focus + on_toplevel_focused -> RCSS raise/highlight), guarded on the window still being tracked. So a click/tap on a background window now focuses it (the gap noted in Wave 3). kernel + ext-window-field + ext-xdg-shell suites green; build-asan green, no new unbox::-framed leak/UB (the stored handler dies with the element).
2026-06-15ext-window-field(rml-compositing W3): toplevels as RCSS surface elementsAdam Malczewski
NEW core unit (id "window-field", depends_on "xdg-shell"), installed by host-bin ONLY under --rml-compositing / UNBOX_RML_COMPOSITING (default off; the classic wlr_scene path stays the default until RML compositing is signed off on hardware). It composites application toplevels as RCSS surface elements inside ONE ui surface (the window field) instead of as wlr_scene nodes: - activate(): fetch ext-xdg-shell Service (typed), create the window-field UiSurface at SceneLayer::normal sized to the primary output, subscribe to on_output_added + on_toplevel_mapped/unmapped/focused. - on map: Toplevel::wl_surface() -> UiSubstrate::create_surface_element(); the toplevel is Toplevel::hide()'d out of wlr_scene (the surface element is now the sole compositor of its pixels; the substrate drives its frame callbacks); appended to bind_list("wins") (live_uri = source_uri(), focused, title, app_id). - on unmap: drop the SurfaceElement + row. on focus: flip the focused flag. - Layout/animation are RCSS (assets/ext-window-field/field.{rml,rcss}): a wrapping flex field where the focused window dominates and the rest tile below, animated on a transition. No geometry from C++ (the user's contract decision). - Keyboard focus stays ext-xdg-shell's job; pointer/touch input-back is automatic in the kernel; this unit wires no seat calls. Wiring (orchestrator): root meson subdir, host-bin install behind the flag + ext_window_field_dep. Headless glue test (40 assertions): map tracks the window + drives hide(), 2nd map, focus flip via the Alt+Tab path, unmap removes it; no-GL degrade asserted. build + build-asan green; host-bin links; no regressions. Known gaps (change-requests): click-to-focus a BACKGROUND window needs a kernel "surface-element pressed -> notify owner" hook (next); wallpaper needs ext-layer-shell surface exposure (Wave 3b).
2026-06-15kernel(rml-compositing): fix Wave-1 test-seam listener lifetime (root cause)Adam Malczewski
The Wave-1 headless surface-capture test seam attached a commit Listener to every client wl_surface and only detached at Server teardown, so a client destroying a surface mid-session tripped wlroots' wl_list_empty(&surface->events.commit.listener_list) assertion (SIGABRT). This aborted ext-stage-dock-glue at teardown and forced an in-unit workaround in ext-xdg-shell-client. Fix: the per-surface commit Listener is now RAII-scoped to the surface's own lifetime (dropped on a wlr_surface.events.destroy listener), so it unsubscribes before the surface resource is destroyed. Test-probe behavior unchanged; extension-facing contract unchanged. kernel suite green; ext-stage-dock now GREEN (no teardown SIGABRT); ext-xdg-shell still green; build-asan kernel green, no new unbox::-framed leak/UB.
2026-06-15ext-xdg-shell(rml-compositing W2): add Toplevel::wl_surface() (additive)Adam Malczewski
Wave 2, additive — existing wlr_scene compositing of toplevels held intact (the switch to surface elements is a later wave behind a flag). - New contract: Toplevel::wl_surface() -> wlr_surface* (the root surface, == xdg_toplevel->base->surface, the same surface scene_tree() hosts). A borrow with the same lifetime as the Toplevel (valid until on_toplevel_unmapped). This is the surface ext-window-field (Wave 3) passes to the kernel's UiSubstrate::create_surface_element(); the kernel then manages the subsurface/popup tree itself, so no popup/subsurface handles are exposed. ABI-additive: ext-keybindings/ext-stage-dock/host-bin rebuild + link unchanged. - Fixed the ext-xdg-shell-client teardown SIGABRT. Diagnosed (not assumed): the abort is NOT pre-existing on baseline (seed 12e5016 passes) — it is the kernel Wave-1 surface-capture TEST SEAM attaching a commit Listener to every client wl_surface and only detaching at Server teardown, so a client destroying a surface mid-session trips wlroots' wl_list_empty(commit.listener_list) assertion. Fixed in-unit via teardown order (unmap via null-buffer commit; let the Server destructor reap the surface after detaching its seam). Filed a kernel change-request for the real fix (RAII per-surface destroy listener), which will also cure ext-stage-dock-glue. ext-xdg-shell suite 2/2 green in build + build-asan, no SIGABRT, no unbox:: leak/UB. wl_surface() covered (non-null, == root surface, round-trips through scene_tree_for()).
2026-06-15kernel(rml-compositing W1b): surface trees + input-back + keyboard-focusAdam Malczewski
Extends the live SurfaceElement to the whole surface TREE and routes input back to clients. Surface trees: - create_surface_element(root) now manages subsurfaces + xdg popups as per-subsurface child elements (unbox-surface://N.K), each its own live seq-gated texture at its tree offset; the substrate re-walks the live tree each dirty tick (wlr_surface_for_each_surface + popup walk), reconciling by wl_surface identity and dropping a node the instant it leaves the tree. - Frame-callback duty now walks the WHOLE tree per composited frame. - Parent-relative child placement (place_child_box, pure core): child <img> positioned relative to the root img's resolved box, so a moving/resized parent drags its children; popups unclipped. Caller tracks only the root. Input-back (automatic; the wm wires no seat calls): - pure core src/input_core.hpp (port of the spike's spike_input_core): project_to_screen/unproject_to_local + place_child_box, doctested (criterion-3 round-trip < 0.01px; affine exact). - a pointer/touch pick landing on a surface-element node maps the point through the node img's real RCSS transform via Element::Project, then box->surface- local, and forwards to that client via wl_seat at surface-local px. Normal RML picks still fire bind_event/bind_drag unchanged. Cursor stays a wlr plane. - SurfaceElement::focus_keyboard() (new public method): seat keyboard-focus MECHANISM only (focus POLICY is the window-field wave); cleared on destroy. Tests: pure-core doctests + a headless test driving a REAL in-process client (toplevel root + subsurface + xdg popup): tree composes as >=3 child <img>, whole-tree frame-done, pointer enter/motion/button + touch at expected surface- local coords (incl. a rotateY(35) transformed-element case proving Project), keyboard enter+key. kernel suite 72c/375a green; build-asan 0 records (the seat/per-node-import/listener lifetimes clean). Spike untouched.
2026-06-15kernel(rml-compositing W1): live SurfaceElement (zero-copy, self-updating)Adam Malczewski
Phase 2 Wave 1: the live sibling of Preview. A SurfaceElement is backed by a client wl_surface's current committed buffer, imported zero-copy into the RMLUi sibling GLES context and served under an unbox-surface://N URI, shown via <img src> in any UiSurface. Public contract (ui.hpp): - class SurfaceElement { source_uri(); width(); height(); } (no refresh()). - UiSubstrate::create_surface_element(wlr_surface* client) -> unique_ptr; nullptr on no-GL/import-fail, never throws; `client` is a borrow the caller must outlive and drop on unmap/destroy (lifetime documented per listener-lifetime). Behavior (ported from the in-tree spike, untouched): - seq-gated re-import (wlr_surface->current.seq), pool-reuse-proof; double- buffered wlr_buffer_lock/unlock (<=1 pinned, balanced incl. prev==buf). - frame-callback duty per composited frame so the client keeps drawing (the stuck-frame fix); a frame stays scheduled while >=1 element exists. - commit dirties the hosting ui surface (dirty-gate); static client = no work. - shm-upload + R<->B swizzle fallback when there is no dmabuf path. Pure-core predicate (surface_element_needs_reimport) doctested; headless integration test drives a REAL in-process Wayland client (re-import on seq advance, the pooled same-pointer case, zero idle re-imports, climbing frame-done). kernel suite + build-asan green on Haswell+crocus. Test-only wayland-client dep (kernel-tests scope; user-accepted). Scope held: single surface, no input-back/damage/scene changes (later waves).
2026-06-15chore: seed feat/rml-compositing from main + RML-compositing design & spike ↵Adam Malczewski
reference Phase 2 of RML compositing starts here, off main. Brings over from the throwaway spike branch: - design/planning: notes/rml-compositing-phase1.md (the Phase 1 design), notes/plan.md row 71 (ADOPTED/GO + Option-2 contract), GLOSSARY.md terms (RML compositing, surface element), tasks.md, .gitignore. - spike sources as IN-TREE REFERENCE only (packages/kernel/src/spike + build_by_default:false target + the input-core doctest): the proven mechanics Phase-2 ports (live seq-gated import, Element::Project input-back, FBO->dmabuf present, dirty-gate). Deleted once Wave 1 lands. Real Phase-2 implementation lands in subsequent wave commits.
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