5.3 KiB
valw Phase 4: zoom mode (design)
Date: 2026-10-02
Status: approved in brainstorming, pending written-spec review
Builds on: Phases 0–3 (on main); vision section 4.7
1. Goal
A woomer-like magnifier: valw zoom freezes the focused output and lets
the user zoom towards the pointer, pan, and spotlight an area, then either
leave or capture what they see.
Success: zooming and panning stay smooth (60 fps) on the user's Intel HD
4000 at 1920×1080, pixels stay crisp, and c hands a crop to the usual
save → preview → clipboard flow.
Out of scope
- Zoom on every output at once, smooth (linear) filtering, several captures from one zoom session.
- Vulkan or wgpu (the user asked for no Vulkan).
2. Decisions
| Question | Decision | Why |
|---|---|---|
| Renderer | OpenGL ES 3.0 through EGL (khronos-egl, wayland-egl, glow) |
No Vulkan; a single textured triangle doesn't need wgpu. A spike on the user's HD 4000 (Mesa, crocus) got GLES 3.0, a compiling #version 300 es shader, MAX_TEXTURE_SIZE 16384 and 61 fps at 1920×1080 on a layer surface |
| Wayland backend | wayland-backend with client_system (libwayland-client) for the whole program |
EGL and wayland-egl need the C wl_display* and wl_proxy*; the API valw uses doesn't change |
| Output | The focused output only | Simple, like woomer |
| Filtering | Nearest | Crisp pixels for inspection |
c |
Crops the visible area of the original frame (native pixels), runs the normal capture flow, exits | Zoomed region select; no upscaled pixels |
Esc / q |
Leave without saving, exit code 0 | Leaving is the normal end of zoom, not a cancelled capture |
3. Behaviour
Start, as for the other modes: load the config, take the capture lock,
wait --delay, hide the thumbnails, capture niri's focused output (with
the cursor if --cursor/show_cursor). Then a full-screen layer surface
(namespace valw-zoom, overlay layer, exclusive keyboard) on that output
shows the frozen frame at 1×.
| Input | Effect |
|---|---|
| Wheel | Zoom towards the pointer: the frame point under the pointer stays put. One notch = one zoom.scroll_step (default 1.15); touchpad scrolling: one step per 15 logical px. Range 1×–32× |
| Left drag | Pan, 1:1 with the pointer |
f |
Flashlight on/off: outside a circle around the pointer the image is dimmed to 25 % brightness |
| Ctrl + wheel | Flashlight radius × / ÷ scroll_step (default zoom.flashlight_radius = 180 logical px, range 20–2000) |
0 |
Back to 1× (animated) |
c |
Capture the visible area (see 2) and exit |
Esc, q |
Exit, nothing saved |
- The view never leaves the image: at 1× it is exactly the frame, and a zoomed view is clamped to the frame's edges.
- Zoom and reset animate: the shown scale and position approach their targets exponentially (about 90 % in 120 ms) and snap when close. Pan is immediate. Frames are drawn on frame callbacks only while something changes; an idle view draws nothing.
- The cursor is the default pointer; while panning it is
grabbing.
4. Structure
| File | Role |
|---|---|
src/zoom/view.rs |
Pure view math: View { scale, origin } (origin = the frame pixel at the top-left of the screen), zoom_at, pan, reset, clamp, the animation step, visible_rect(frame size) -> PixelRect, scroll → zoom factor, flashlight radius bounds |
src/zoom/gl.rs |
EGL display/context/window surface on the layer surface, the texture upload, one shader program (fullscreen triangle; fragment shader maps screen → frame pixels, texelFetch/nearest, swizzles BGRX, flashlight dimming) |
src/zoom/mod.rs |
run(wl, output, frame, config) -> Result<Option<PixelRect>>: surface, input routing, the draw loop; Some(rect) for c, None for Esc/q |
src/wayland.rs |
State::zoom; pointer, axis, key and modifier events go to it while it is open |
src/main.rs |
Command::Zoom, Mode::Zoom; None returns Ok(()) before saving |
src/config.rs |
[zoom] scroll_step = 1.15, flashlight_radius = 180 |
src/doctor.rs |
A line for EGL + an OpenGL ES 3 config (warn, not fail, when missing: only zoom needs it) |
nix/package.ulu.nix |
LD_LIBRARY_PATH suffix with libglvnd (libEGL.so.1); Mesa's drivers come from /run/opengl-driver on NixOS |
A missing EGL/GLES 3 makes valw zoom fail with a clear error and the
hint to run valw doctor; the other modes don't touch EGL.
5. Testing
Unit tests (cargo nextest) for view.rs:
zoom_atkeeps the frame point under the pointer fixed (several scales, pointer at a corner and the centre).- Scale is clamped to 1–32; at 1× the origin is 0,0; a zoomed origin is
clamped to the frame edges;
panrespects the same clamp. - The animation reaches the target and then reports "idle".
visible_rectat 1× is the whole frame; at 4× with a known origin it is the expected rectangle, rounded outwards and inside the frame.- Scroll → factor for wheel notches and touchpad pixels; flashlight radius bounds.
- Config: the
[zoom]defaults and overrides; doctor: the EGL line's levels.
The headless sway check keeps covering screencopy and the overlay, now on
client_system. Zoom needs a GPU, so it is checked by hand: the Phase 4
section of docs/test-checklist.md.
6. Definition of done
cargo clippy -- -D warnings, cargo fmt --check, cargo nextest run
and nix flake check pass; the checklist section is written.