diff --git a/docs/ui-navigation.md b/docs/ui-navigation.md new file mode 100644 index 0000000..6d9b6cb --- /dev/null +++ b/docs/ui-navigation.md @@ -0,0 +1,298 @@ +# UI navigation: finding things once there are many + +TRACES: FR-UI-1 | FR-UI-3 | FR-UI-5 | FR-DEV-3a | FR-DEV-3c + +Successor to `ui-refinement.md`, which asked how the interface should *look*. +This asks how someone finds anything in it. The two are sequenced together at +the end. + +## Why now + +Local adjustments landed (D14, `segmentation.md` §14) and the develop column +went from four panels to six: image, histogram, geometry, settings, local, +adjust. Adjust alone is ten operations, and the colour mixer contributes +thirty-six parameters by itself. That is already past what one scrolling +column presents well, and the operation set is meant to keep growing — +FR-DEV-3 lists texture, clarity, sharpening and noise reduction as v1, none of +which exist yet. + +But the count is the lesser problem. **Local adjustments introduced a mode +without introducing a way to see it**, and that is the part that can lose +someone's work rather than merely slow them down. + +--- + +## 1. The three problems, which are not one problem + +### 1.1 Scope — invisible state + +Selecting a mask layer silently re-points the adjust panel at that layer's +chain. Same thirty sliders, different meaning, and the only indication is a +caption between the two panels. + +Three ways that bites: + +- An exposure change lands on the whole photograph when it was meant for a + face, or the reverse. Both are silent; both are discovered later. +- **The histogram does not follow scope.** The instrument the tonal controls + are judged against reports the whole frame while the slider edits a + subject's face. FR-DSP-7 asks the histogram to describe what the + photographer is looking at; under a mask it currently does not. +- Undo interleaves global and local edits with nothing distinguishing them. + +This is the classic modal fault, and the classic remedy applies: make the mode +visible, or make it not a mode. See §3. + +### 1.2 Extent — six panels in 280px + +The column scrolls as one (`ui-refinement.md` Workstream C explains why: a +scroller inside a scroller gives every drag a third thing to be lost to). At +six panels that scroll is long enough that the histogram — the instrument +everything tonal is judged against — is frequently off screen while the +sliders it reports on are being dragged. + +### 1.3 View — grid and develop are still separate screens + +Diagnosed as `ui-refinement.md` Workstream F and not yet built. Unchanged by +this document, which assumes F lands. + +--- + +## 2. What other programs do + +Worth summarising honestly, because all three problems are solved elsewhere +and the solutions have known costs. + +### On a desktop, two families + +**A collapsible stack.** Lightroom Classic's right panel is a vertical column +of modules — Basic, Tone Curve, HSL, Detail, Lens, Effects — each collapsible, +each reporting whether anything inside it has been touched. Everything is in +one place and in a fixed order, so muscle memory works; the cost is a long +scroll and a lot of triangles. + +**Tool tabs.** Capture One puts an icon strip at the top of the tool panel and +gives each tab a curated set of tools; RawTherapee tabs the right-hand panel +the same way. Scroll is bounded and there is a clear sense of place; the cost +is that a control you cannot name is in one of eight places, and switching tabs +loses the context you were comparing against. + +**Groups over a stack.** darktable combines both — a row of group icons +filtering a long list of collapsible modules, plus a search box. It is the most +powerful and the most often described as overwhelming, which is worth reading +as a warning about combining mechanisms rather than about either one. + +Across all of them, **masking is its own tool**, not a panel among peers. +Lightroom opens a masking tool with its own layer list and its own canvas +overlay; Capture One makes layers a persistent selector at the top of the +adjustments tab. Nobody makes a mask a panel that silently rewires a different +panel — which is what DarkRoom currently does. + +### On a phone, one family + +Lightroom Mobile, Photomator and VSCO all converge on the same shape: **a +horizontal strip of tool icons along the bottom**, and tapping one replaces a +bottom sheet with that tool's controls. Snapseed goes further — one tool fills +the screen and a vertical swipe chooses the parameter while a horizontal one +sets it. + +The convergence is not fashion. Three physical facts drive it: + +- **Thumb reach.** A one-handed grip reaches the bottom third. A right-hand + column is a mouse idiom. +- **The image needs the screen.** A 280px column is a fifth of a desktop + window and most of a phone in portrait. +- **There is no hover.** Disclosure triangles and hover-revealed affordances + are worth less; a control is either visible or gone. + +--- + +## 3. The decisions + +### D-N1 — Local adjustment becomes a mode · **DECIDED** + +Not a panel that re-points another panel. A mode, in the sense `crop-mode` +already is: it changes what the canvas does, scopes what the column shows, and +is left explicitly. + +**Why this shape rather than louder signalling.** The app already has this +pattern and the user already knows it. Crop mode arms a canvas interaction, +draws an overlay, gives the column one job, and exits by the same control that +entered it. Local masking is the same animal — a canvas interaction plus a +scoped panel — and building it as a peer panel is what created §1.1. Making it +a mode removes the ambiguity by construction instead of describing it in a +caption. + +It also inherits machinery that exists. `lib.rs` already resolves Escape and +the Android back gesture to "leave the innermost state first" (FR-UI-5); local +mode joins that stack and needs no new exit concept. + +In local mode: + +- the canvas turns the overlay on and arms click-to-select; +- the column shows the mask stack and, beneath it, the adjustments **scoped to + the selected layer**; +- the header names the scope — the layer, not "adjust"; +- leaving returns to the whole photograph, by Escape, by back, or by the mode + control. + +**The histogram follows the scope.** Under a mask it reduces over the masked +pixels only. This is FR-DSP-7 read literally — it asks the histogram to +describe what is being looked at — and without it the instrument and the +controls disagree about what they are measuring. Costs a mask term in the +histogram reduction, which already runs per frame over the displayed frame. + +### D-N2 — Diverge on **width**, not on platform · **RECOMMENDED** + +The question was whether desktop and Android should diverge. They should +diverge, but not along that line. + +**Platform is the wrong axis.** A tablet in landscape wants what a desktop +wants. A desktop window dragged to a third of the screen wants what a phone +wants. Splitting on `cfg(target_os)` would give the same physical situation two +answers depending on which binary it happened to be. + +**Width is the right axis, and it already exists.** `apply_layout_class` in +`lib.rs` already classifies the window as `expanded` or `compact` on width +alone, already remembers a user override per class, and already drives panel +visibility. The divergence proposed here is a second consumer of a decision +the app is already making. + +So: + +| | **expanded** | **compact** | +|---|---|---| +| Modes | strip at the top of the canvas | strip along the bottom | +| Tools | side column, collapsible stack | bottom sheet, one group at a time | +| Mask stack | inline in the column | its own sheet, reached from the strip | + +**What must not diverge is the controls.** Both layouts consume the same +`ParamRow` model, the same generated capability list and the same widgets. +Only the container differs. That is what keeps FR-DEV-3a intact — a new +operation appears in both layouts with no UI edit, because neither layout +knows what an operation is. + +### D-N3 — Collapsible panels, not tool tabs · **OPEN** + +For the expanded layout, extend `ui-refinement.md` Workstream C from +sections-inside-adjust to the panels themselves: each collapses to a header +carrying a modified dot, and collapse state survives a drag elsewhere. + +**Why this over tabs, on the evidence above.** Tabs need a taxonomy, and the +taxonomy is the problem. `ui-refinement.md` condemns the `starts-group` flag +for being the core telling the panel where sections go, and FR-DEV-3a requires +that adding an operation needs no UI edit. A tab strip built from a hardcoded +op-id → tab table in `dr-ui` breaks the second; one built from a `group:` field +in `ops/*.yaml` risks breaking the first. + +There *is* a legitimate route to tabs, and it should be recorded rather than +discovered later: the descriptor could declare an operation's **nature** — +tone, colour, detail, optics — the same shape as `Affects` and `ParamKind` +already take. The core would be saying *what the operation is*, which is its +business, and the frontend would remain free to render that as a tab, a +section heading, or nothing at all. That stays on the right side of §4.3a. + +**The recommendation is to defer it.** Ten operations do not need eight tabs, +collapse needs no taxonomy at all, and the nature field is easy to add later +and awkward to remove. Revisit when the operation count passes roughly fifteen +— which FR-DEV-3's outstanding list will reach. + +**Open, because it is a taste call**: whether the expanded layout should also +gain tabs, or stay a single collapsible stack indefinitely. + +--- + +## 4. Workstreams + +Numbered N to avoid colliding with `ui-refinement.md`'s A–F. + +### N1 — The mode strip + +**Deliverable.** One control naming the current mode, replacing the implicit +`crop-mode` boolean: **Photo · Crop · Local**. Top of the canvas in expanded, +bottom in compact. The selected mode is the accent's job — it means *active*, +which is exactly this. + +`crop-mode` becomes one value of a mode enum rather than its own flag, so the +two modes cannot both be on, which today they can. + +**Done when.** Entering crop from the strip does what the crop button did; +Escape and back leave the innermost mode; no two modes are ever active +together. + +### N2 — Local mode + +**Depends on** N1. + +**Deliverable.** Entering local mode turns the overlay on and arms picking +without either being a separate toggle — they are what the mode *is*. The +column shows the mask stack, then the scoped adjustments. The adjust header +names the layer. + +Leaving local mode clears the selection so the adjustments are unambiguously +global again. + +**Done when.** There is no way to have a mask selected without knowing it, and +the two toggles that currently arm the overlay and picking are gone. + +### N3 — Scope-following histogram + +**Depends on** N2. + +**Deliverable.** The histogram reduction takes an optional mask; in local mode +it reduces over the selected layer's coverage. The panel says which it is +showing, because a histogram of a face is a strange shape and the user should +know why. + +**Done when.** Selecting a layer visibly changes the histogram, and leaving +local mode restores the frame's. + +### N4 — Collapsible panels + +**Depends on** `ui-refinement.md` A. Extends C. + +**Deliverable.** Each panel in the develop column collapses to its header; +headers carry the modified dot C defines; state is keyed by panel identity and +survives a slider drag. Default: image and histogram open, the rest collapsed +until touched. + +**Done when.** A fresh develop view fits without scrolling at 900px tall, and +the histogram is reachable without scrolling past the sliders it reports on. + +### N5 — Compact layout + +**Depends on** N1, N4. + +**Deliverable.** In the compact class the column becomes a bottom sheet and the +mode strip moves to the bottom. Tool groups are reached from the strip, one at +a time. Same models, same widgets, different container. + +**Done when.** A phone-width window shows the photograph with a thumb-reachable +strip and no side column, and every control reachable in expanded is reachable +here. + +--- + +## 5. Sequencing + +``` +ui-refinement A ──┬── C ──── N4 ──┐ + └── D ──── F ├── N5 +N1 ── N2 ── N3 ───────────────────┘ +``` + +N1–N3 are independent of `ui-refinement.md` and can start now; they touch the +canvas and the develop column's contents, not its layout. N4 needs C's +`Section`. N5 needs both and should land last, as F does — it is the one that +rearranges everything. + +## 6. Invariants, for every workstream + +- **FR-DEV-3a.** Adding a pipeline operation must still surface in both + layouts with no UI edit. No file under `ui/` may name an operation. +- **ARCH §4.3a.** Composition is the frontend's decision. The core may declare + what an operation *is*; it may not declare where the panel puts it. +- **FR-UI-3.** Touch targets survive the compact layout — it is the layout + most likely to be touched. +- **FR-UI-5.** Escape and the Android back gesture leave the innermost state + first. Every mode added here joins that order.