diff --git a/docs/superpowers/specs/2026-10-02-valw-phase-5a-editor-design.md b/docs/superpowers/specs/2026-10-02-valw-phase-5a-editor-design.md new file mode 100644 index 0000000..75e1be0 --- /dev/null +++ b/docs/superpowers/specs/2026-10-02-valw-phase-5a-editor-design.md @@ -0,0 +1,152 @@ +# valw Phase 5a: the built-in editor, basics (design) + +Date: 2026-10-02 +Status: approved in brainstorming, pending written-spec review +Builds on: Phases 0–4 (on `main`); vision section 4.5 + +## 1. Goal + +Replace Satty with valw's own editor for everyday markup: click a preview +thumbnail, draw arrows, boxes and highlights, save or copy, done. Phase 5b +adds text, blur/pixelate, numbered markers and crop on top of this. + +Success: the user can mark up a screenshot as fast as in macOS Markup, and +what is saved is pixel-for-pixel what the canvas shows (at the image's own +resolution). + +### Out of scope (5b or later) + +- Text, blur/pixelate, numbered markers, crop (5b). +- Selecting, moving or editing a drawn shape; filled shapes; zoom inside + the editor; a custom colour picker. + +## 2. Decisions + +| Question | Decision | Why | +|---|---|---| +| Split | 5a basics now, 5b afterwards, each with its own spec and plan | The editor is the largest phase; 5a is already usable on its own | +| Toolkit | `eframe`/`egui` with the `glow` backend (OpenGL ES through EGL), no Vulkan, no wgpu | Ready-made buttons, shortcuts and (for 5b) text input; a normal Wayland window | +| Process | `valw edit `, its own process | Same as Satty today; the preview host stays small | +| Look | macOS Markup: one slim toolbar on top, the image large below | User choice | +| Ctrl+S | Overwrite the file; Ctrl+Shift+S saves a copy next to it | Like macOS; the preview already points at that file | +| Rendering | Shapes become shared geometry (polylines, polygons) in image pixels; egui draws it on screen, `tiny-skia` draws it for export | One geometry, two painters: the export matches the screen | + +## 3. Behaviour + +### 3.1 Window + +`valw edit ` opens a window titled ` — valw`, app id +`valw-editor` (for niri window rules). The image is fitted into the area +below the toolbar, centred, never upscaled beyond 100 %. A file that can't +be read is an error on stderr and exit code 1, before any window appears. + +### 3.2 Toolbar (left to right) + +| Group | Items | +|---|---| +| Tools | Arrow, Rectangle, Ellipse, Line, Pen, Highlighter | +| Colour | red `#ff3b30` (default), orange `#ff9500`, yellow `#ffcc00`, green `#34c759`, blue `#007aff`, black, white | +| Width | S 2 px, M 4 px (default), L 8 px (image pixels) | +| History | Undo, Redo (disabled when there is nothing to do) | +| Output | Copy, Save | + +The active tool, colour and width are highlighted. Default tool: Arrow. + +### 3.3 Drawing + +- Press, drag, release on the image creates one shape from the press point + to the release point (Pen and Highlighter: every point on the way). + A press outside the image does nothing; a drag that leaves the image is + clamped to its edges. +- A shape whose drag is shorter than 2 image px (a click) is discarded, + except for Pen, which keeps a dot. +- Shift while dragging: Line and Arrow snap to 45° steps; Rectangle and + Ellipse become a square and a circle. +- Arrow: a line with a filled triangular head at the release point; the + head grows with the width (length `max(12, 4 × width)`, 30° half-angle). +- Rectangle and Ellipse: outlines only. +- Highlighter: the chosen colour at 40 % opacity, width × 4, round caps; + drawn over everything below it. +- Width is in image pixels, so a 4 px arrow is 4 px in the saved file. + +### 3.4 Keys + +| Key | Effect | +|---|---| +| A, R, O, L, P, H | Arrow, Rectangle, Ellipse (O), Line, Pen, Highlighter | +| Ctrl+Z | Undo | +| Ctrl+Shift+Z, Ctrl+Y | Redo | +| Ctrl+C | Copy the edited image to the clipboard | +| Ctrl+S | Save over the file | +| Ctrl+Shift+S | Save a copy: ` edited.png` next to it (`unique_path` if taken) | +| Esc | Close; with unsaved changes the first Esc shows a bar "Unsaved changes — Esc to discard, Ctrl+S to save", the second closes | + +Closing the window (compositor close) with unsaved changes behaves like +the first Esc: the bar appears and the window stays. + +### 3.5 Undo + +The document is the base image plus a list of shapes. Every finished shape +pushes onto the list and clears the redo stack; undo moves the last shape +to the redo stack, redo moves it back. Saving does not clear history. +"Unsaved" means the shape list differs from what was last saved. + +### 3.6 Save and copy + +Export composites the shapes onto the base image at its full resolution +with `tiny-skia` (anti-aliased), then encodes PNG with the existing +`output::encode_png`. + +- Save: `output::write_atomic` over the file; the window stays open. +- Save a copy: as above to the `edited` path; the window stays open. +- Copy: the existing clipboard path (`output::copy_to_clipboard`, which + forks a server); the window stays open. +- Each prints a short status in the toolbar ("Saved", "Copied") for 2 s; + a failure shows its message there instead and is logged. + +### 3.7 Opening from the preview + +The preview host reads `[editor] backend` when a thumbnail is clicked: +`"builtin"` (default) runs `valw edit ` (the running executable), +`"satty"` keeps today's Satty command. Unknown values are a config error. + +## 4. Structure + +| File | Role | +|---|---| +| `src/editor/mod.rs` | `run(path)`: load, start eframe, the `App` (toolbar, canvas, keys, status bar) | +| `src/editor/doc.rs` | `Doc` (base image, shapes, redo stack, saved marker): add / undo / redo / dirty | +| `src/editor/shape.rs` | `Tool`, `Shape`, `Style`; constraints (Shift), shape → `Geometry` (polylines and polygons in image px) | +| `src/editor/export.rs` | `Geometry` → pixels with `tiny-skia`; `edited_path` | +| `src/editor/view.rs` | Fit the image into the canvas: image ↔ screen transforms | +| `src/config.rs` | `[editor] backend = "builtin" \| "satty"` | +| `src/host.rs` | `open_editor` picks the backend | +| `src/main.rs` | `Command::Edit { file }` | +| `nix/package.ulu.nix` | `LD_LIBRARY_PATH` gains `wayland` and `libxkbcommon` (winit loads them at run time) | + +Dependencies: `eframe` (default features off; `glow`, `wayland`, +`default_fonts`), `tiny-skia`. + +## 5. Testing + +Unit tests (`cargo nextest`): +- `shape`: Shift snapping (angles, square, circle); the arrow head's size + and direction; highlighter width and alpha; a click is discarded but a + Pen dot is kept. +- `doc`: add / undo / redo order, redo cleared by a new shape, dirty after + edits and clean after save, undo past a save makes it dirty. +- `export`: a red 4 px line on a white image has red pixels on the line + and untouched pixels away from it; a highlighter over a known colour + blends at 40 %; the output keeps the image size. +- `view`: fitting (never above 100 %, centred) and the round trip + image → screen → image. +- `edited_path`: `a.png` → `a edited.png`, unique when taken. +- Config: `[editor] backend` default and the unknown-value error; the + preview host's backend choice. + +Manual checks go to the Phase 5a 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.