| Age | Commit message (Collapse) | Author |
|
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).
|
|
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.
|
|
- 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.
|
|
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.)
|
|
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.
|
|
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.
|
|
- 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.
|
|
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).
|
|
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).
|
|
real-seat verify + W4/W5 decisions
|
|
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).
|
|
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).
|
|
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.
|
|
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()).
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
|
|
The logo already includes the unbox wordmark, so the separate heading
was redundant. Left-align the logo.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
- 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.
|
|
|
|
- **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"
|
|
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.
|
|
.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/.
|
|
|
|
|
|
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/.
|
|
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/.
|
|
|
|
(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.
|
|
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/.
|
|
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.
|
|
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.
|
|
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.
|
|
defer in plan §7
|
|
|
|
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/.
|
|
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/.
|
|
|