Files
valw/docs/superpowers/specs/2026-10-02-valw-phase-5a-editor-design.md
T

153 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <file>`, 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 <file>` opens a window titled `<file name> — 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: `<stem> 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 <file>` (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.