docs: phase 5a editor design spec
This commit is contained in:
1 parent
760c1251e9
commit
1599bd4c7e
1 file changed
+152
@@ -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 <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.
|
||||||
Reference in new issue
Block a user