7.1 KiB
valw theme round: the paint shader, overview backdrop, window picker (design)
Date: 2026-10-03
Status: approved in brainstorming (after the throwaway valw-lab spike), pending written-spec review
Builds on: every phase on main; zoom's EGL/GLES renderer (src/zoom/gl.rs)
1. Goal
Give valw a theme of its own: a Balatro-like liquid-paint background, written from scratch as a shader, coloured from the user's theme, a wallpaper or a window. Three uses:
- T1 A shared theme module (shader, palettes); the region overlay's dark dim takes the theme's colour (no shader there: the user must see what they select).
- T2
valw backdrop: the shader, calm and slow, behind niri's overview, coloured from the wallpaper (replacing Noctalia's blurred backdrop). - T3 An Alt+Tab-style window picker (
valw windowfrom now on): cards for every window over the shader, the shader coloured from the selected window's icon; Enter or a click captures that window.
Out of scope
- Live window thumbnails in the picker: niri 26.04 gives clients no way to
read a window's pixels except
screenshot-window, which also sets the clipboard and shows a notification every time. Recorded as a research item; cards show icons until then. - The shader on the toolbar, zoom or editor (possible later on the same module).
2. The paint shader
The spike's shader, cleaned up (our own code, not Balatro's):
- Two octaves of domain warping (six sine-warp iterations each, a fixed rotation between them), banded into a 3-colour palette with soft edges, dark rims, glossy streaks and a soft vignette.
- Uniforms: time, resolution,
motion(0 = flow, 1 = swirl around the centre),speed,scale(blob size), palettebase,body,highlight. - Lives in
src/theme/paint.fragand is drawn by a renderer shared with zoom's GL code (EGL + glow; no Vulkan).
3. Palettes
theme::Palette { base, body, highlight } (linear 0–1 RGB), from:
- Noctalia's theme:
noctalia msg color-scheme-getandtheme-mode-get; for acommunityscheme the palette file$XDG_STATE_HOME/noctalia/community-palettes/<id>.jsonat the current mode: base =mSurface, body =mSecondary, highlight =mPrimary(accentmTertiarykept for later). Other sources (builtin, wallpaper, custom) fall back to (3) until their files are mapped. - An image (a wallpaper, an icon): dominant colours by coarse bucketing of a downscaled copy; the three most frequent distinct colours (ignoring transparent and near-grey pixels for icons), sorted dark → light.
- A built-in default (dark grey-blue), when Noctalia isn't there or anything above fails (logged at debug level, never an error).
4. T1: themed region dim
The overlay outside the selection becomes the frozen screen blended 55 %
towards the theme's base colour (today: darkened to 60 %), so it stays
see-through. Computed once per output before the overlay appears (no cost
per frame).
5. T2: valw backdrop
- A long-running process: one background-layer surface per output
(namespace
valw-backdrop, no keyboard, empty input region), drawing the shader withmotion = flowat a slow speed, coloured from that output's wallpaper (noctalia msg wallpaper-get <connector>, palette source 2). - Draws only on frame callbacks, so it costs nothing while niri doesn't show it. When frames resume after more than 2 s, it re-reads the wallpaper (a changed wallpaper changes the colours, easing over 1 s).
- Outputs coming and going are followed; a second
valw backdropexits at once (a lock, like the preview host's). - Wiring (in copland, documented in the README):
spawn-at-startup "valw" "backdrop", nirilayer-rule { match namespace="^valw-backdrop$"; place-within-backdrop true; }, Noctalia[backdrop] enabled = false.
6. T3: the window picker
valw windownow opens the picker; the click-a-window mode stays asvalw window --pick(and Space in region mode keeps using it).- A full-screen overlay layer surface on the focused output (exclusive
keyboard): the shader full screen, and over it a centred row (wrapping
to more rows when needed) of cards for every window, most recently
focused first (niri's
focus_timestamp), the focused window's card selected. - A card: a rounded translucent panel, the app icon (96 logical px), the window title (one line, ellipsised) and the app name below. The selected card is larger (110 %), brighter, with a 2 px light border.
- Keys: Tab / → / ↓ next, Shift+Tab / ← / ↑ previous, Enter capture, Esc cancel (exit 3). Mouse: hover selects, a click captures, a click outside the cards cancels.
- The shader's motion alternates per run (swirl, flow, swirl, …; the last
one is remembered in
$XDG_STATE_HOME/valw/picker), and stays fixed while the picker is open. Its colours come from the selected window's icon (palette source 2) and ease to the next card's over 300 ms. - Icons: the window's
app_id→ its.desktopfile ($XDG_DATA_DIRSapplications,StartupWMClassor file name) →Icon=→ the icon theme from~/.config/gtk-3.0/settings.ini(gtk-icon-theme-name), itsInherits, thenhicolor; SVGs rendered withresvg. No icon: a rounded square with the app name's initial. - Capturing: the picker closes (surface destroyed, flushed and
round-tripped), then the Phase 3 path (
niri screenshot-window --id), shadow, sound, preview, clipboard as usual.
7. Structure
| File | Role |
|---|---|
src/theme/mod.rs |
Palette, sources (Noctalia, image, default) |
src/theme/paint.frag, src/theme/gl.rs |
the shader and its renderer (shared EGL setup with zoom::gl) |
src/render.rs, src/region.rs |
themed dim |
src/backdrop.rs |
valw backdrop |
src/picker/{mod,layout,icons}.rs |
the picker, card layout, icon lookup |
src/main.rs |
Command::Backdrop, Command::Window { pick } |
src/niri.rs |
windows() (all windows with focus timestamps) |
README.md |
backdrop wiring |
New dependency: resvg (SVG icons; built on tiny-skia).
8. Testing
Unit tests (cargo nextest):
- palettes: Noctalia palette file parsing (light and dark, missing keys → default), image palette (dark → light order, transparent and grey pixels ignored for icons), default.
- themed dim: a pixel is blended towards
baseby the right amount. - picker layout: card positions for 1, 5 and 20 windows on 1366×768 and 1920×1080 (rows wrap, centred, selected card scaled), hit-testing, keyboard navigation wrapping, MRU order from focus timestamps.
- picker motion alternation from the state file (missing/broken file → swirl).
- icon lookup against a fake data dir (desktop file by name and by
StartupWMClass, theme inheritance, hicolor fallback, SVG and PNG). - backdrop: the "resume after a pause" rule and colour easing.
Manual checks go to the theme section of docs/test-checklist.md
(including how it feels on the HD 4000).
9. Definition of done
cargo clippy -- -D warnings, cargo fmt --check, cargo nextest run
and nix flake check pass; README and checklist updated; copland wiring
done with the user's approval.