153 lines
6.8 KiB
Markdown
153 lines
6.8 KiB
Markdown
# 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.
|