The open stock list showed ten rows, None to Kodak Kodachrome 64, and
the other eighteen - all seven black-and-white stocks among them - could
not be reached. The data was whole; the list could not be scrolled.
Reproduced on the manual rig (Xvfb, xdotool, the automation hook):
- a drag on the list scrolled the develop column, never the list;
- a wheel run over the list scrolled the column past it, whenever the
column had scrolled under that pointer in the last 800 ms - which is
how the list is reached, by wheeling the column down to it. After a
pause and a pointer move the wheel did reach the list;
- no key did anything.
The cause is Slint's routing, not the list. Since 2d878c2 the list was a
Flickable inside the develop column's Flickable, and Slint offers every
pointer event to the outermost Flickable first
(input_event_filter_before_children, i-slint-core 1.17.1 flickable.rs).
The column holds a press back (DelayForwarding) and intercepts the first
move past 8 px on the axis it can scroll, so the list never saw a drag.
For the wheel it intercepts while its own last wheel event is under
800 ms old and within 2 px, and always for a touchpad gesture that opens
with TouchPhase::Started - so on a touchpad the list could get no wheel
at all.
The list is now a PopupWindow under the Film row. A popup is its own
item tree: while it is open, events go to it and to nothing beneath it,
so the list scrolls by wheel, drag and flick however the panel is nested
and whatever the column did last. The alternative, standing the column
down while the pointer is over the list (the sliders' hover trick), fixes
the drag but not the wheel - `interactive: false` does not gate wheel
interception - so it would have left the bug for touchpad users.
The reason 2d878c2 bounded the list still holds: it is at most 320 px
and never lengthens the column, and now it covers the sliders instead of
pushing them down. The column cannot be scrolled while it is open, which
suits a one-click question; it closes on choosing, on Escape or Back, or
on a press outside it.
Keys, with the list open: Up and Down move along it from the chosen
stock and scroll it into view, Enter chooses, Escape or Back closes it
unchanged. A popup is its own focus tree, so the keys are taken when it
opens and Slint returns focus to the develop view's scope when it closes.
The Film row gains a button role, so a screen reader and the automation
hook can name it.
FR-CAT-11a records what #67 built: groups of the same file listed on
demand, proved the same before anything moves, merged onto one survivor
and the rest trashed a group at a time. The manual's new section under
The library says how to reach the page, how the copy that stays is
chosen, what the check reads and what merges.
Every scene was recorded again on a build of this branch rebased onto the
keyboard work and TD-1, since nearly every scene depends on files those
changed: the develop top bar now carries a star strip and Pick/Reject, the
roll shows flags and stars, and the grid's selection bar gains Label and
Flag. `--changed` could not be trusted to find them, because the rebase
made each picture's commit newer than the sources it was recorded from.
The develop-zoom caption said the wheel goes on "until the pixels are
blocks". On Xvfb the deepest frames come out smooth even though the app
draws past 1:1 nearest-neighbour on a real display (confirmed by eye on
the desktop), and a GIF shrunk to 960 wide could not show 3-pixel blocks
anyway. The caption now says what the clip shows; the prose above it,
which describes what the app does, stays.
The rebase combined this branch's README additions with master's, and
index.html is generated from the README; manual-check passes on the
regenerated page.
Four scenes, recorded and looked at frame by frame:
- library-labels: 6, 7, 8 and 9 over four New York frames, 7 again to
take one off, then the Green chip narrowing the grid and back.
- compose-perspective: two towers shot from below, stood upright with
Vertical at about +58, then held against Before.
- crop-orphan: a stroke in the top-left corner, a crop that leaves it
outside, the notice with Undo crop and Keep crop, and Undo crop.
- local-intersect: a linear gradient over the lower half, then
Intersect and two strokes that survive only where it is.
develop-zoom already ends on hard-edged pixels (previous commit). The
text added is a sentence or two under the existing headings, so the
other branch's structure and anchors are left alone.
scenes.py aimed every press at window pixels, and the develop column had
already moved under it: Compose now sits above Adjust, so the old
exposure coordinate lands on a straighten slider. Every scene now names
what it presses by its accessible label through the automation hook,
places points on the photograph relative to the canvas, and opens its
photographs by file name. Each starts from a known place and undoes what
it did, so one can be recorded alone; the few that continue another's
state name it, and running one runs that first into a scratch folder.
Each scene also declares the pictures it makes and the sources they
depend on. `record.sh --check` fails when the manual shows a picture no
scene makes, or a scene makes one it does not show; it reads two files.
`record.sh --changed` re-records the scenes whose sources, or own code,
changed since the commit that last touched their pictures. record.sh
builds with the automation feature, restores the library from
DR_LIBRARY_SNAPSHOT, starts from a fresh profile and pins inference to
the CPU; the launch screen is recorded from an empty profile of its own.
Re-recorded with the ported scenes, and looked at frame by frame. What
differs from the pictures they replace:
- develop, presets, settings, local, compose, film, wb, light: the
current develop column (Compose with Vertical and Horizontal above
Adjust, the Label button), otherwise the same moments.
- library pictures: the filter bar's colour-label chips; no collection
left over from an earlier run in the sidebar; library-selection is the
twelve alpine frames rather than eight of them and four New York ones.
- library-rating rates two frames nobody had rated, so the stars are set
and not cleared.
- develop-zoom goes on past 1:1 with the wheel and ends on the file's
pixels as hard-edged blocks.
- repair covers a real mark on the road, with a size that fits it; film
is shown on the Chinatown frame instead of the road.
- panorama tries Perspective, Spherical and Cylindrical before filling.
- launch, launch-folder and panorama-aligned came out byte-identical.
The sheet could now offer "See it", but no gesture said where to look.
Every GESTURE tag whose move the manual describes names that section:
the white-balance picker, zoom and pan, masks, undo and snapshots,
export, colour labels and ratings, selection, collections, the People
page, thumbnail size. Fifty of the fifty-one; the one left, putting a
single control back to its default, has no section and is too small to
earn one.
Three important gestures had nowhere to land, so the manual gains three
short sections, without pictures for now: Bursts (opening a folded
burst, choosing the frame it shows, and the eyes-open filter), Moving
between photographs (the roll, the arrows and A/D in develop) and
Copying settings (Copy, Paste, Paste to N, and choosing what a copy
carries). The page, the gesture book and docs/gestures.md are
regenerated from them.
The packages now carry the manual, but nothing in the application opened
it: the help sheet listed gestures and stopped there.
The help sheet gains a Manual button beside Done, and Settings a Manual
row under About beside the version. Both go through dr_ui::manual, which
finds the installed page through dr_plat::system_data_dirs (the package's
share directory on Linux, the executable's directory on Windows), and a
development build also in the checkout it was compiled from. A copy with
no manual says so on the status line rather than doing nothing.
On the desktop the page goes to the system browser. A section is a URL
fragment, and xdg-open's generic mode and Windows' FileProtocolHandler
both turn a file: URL into a path and drop the fragment, so a section is
opened through a one-line redirect page written to the data directory:
the opener gets a plain path, which every opener keeps, and the browser
follows the redirect to index.html#section itself. The launcher behind
the sign-in's open_in_browser is split out so both share it; the https
check stays with the sign-in.
Android has no path to give a browser: an asset is not a file, a copy in
private storage is unreadable to other apps, a file: URI across apps is
refused, and a content: URI leaves the browser resolving every picture
against the provider. So ManualActivity, a WebView reading
file:///android_asset/manual/index.html straight out of the APK, shows
it, started by class name with the section as an extra. JavaScript is
off, links off the page go to the browser, and the theme is day-night so
the page's own light and dark follow the system. A test checks that the
manifest, the Java class and dr_ui agree on the name and the extra.
The manual existed only as docs/manual/README.md, which the forge renders
and nothing else does. An installed copy of the application, on a laptop
with no network or on a tablet, had no manual it could open.
`traces manual` renders the README to docs/manual/index.html with
pulldown-cmark (already in the tree as Slint's Markdown parser, so this
adds a dependency edge and no crate). The page is one file with an inline
stylesheet that follows the system's light or dark preference, a
contents list of every section and subsection, and the pictures by their
relative media/ paths. Each heading carries the id the forge gives it, so
README.md#rating-and-flagging and index.html#rating-and-flagging are the
same link. A picture alone in its paragraph becomes a figure whose alt
text is shown as the caption, and every picture reserves its 16:11 box
before it loads, so a jump into the middle of the page lands where it
aimed rather than a screenful above. Links to design documents, which the
installed page has no copy of, point at the forge.
The page is committed rather than rendered at build time, as the gesture
book is: it is user-facing text reviewed in the diff, and the three
packagers then only copy it. `traces manual-check` fails in CI when the
committed page is not the render of the README, and the pre-commit hook
regenerates it when the README is staged.
The manual's library section described rating and flagging only, and
the outstanding register still said colour labels were "set and shown
nowhere" and that three NFR-A11Y-3 clauses had no test. Both are now
untrue: the manual gives the keys, the Label button and the chips, and
the register names the test file and what it can and cannot vouch for.
docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.
Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
A scene that makes a parent, nests two collections in it by drag and by
the menu, files frames into a child and opens the parent to see it count
both; stills of the tree and of the menu. The collections recording is
re-made now that the bitmap under the cursor is the photograph.
drive.py grows a multi-leg drag: a diagonal with much vertical in it is
taken by the grid's Flickable as a scroll before the DragArea can claim
it, so a drag to the sidebar goes sideways first.
The recording sampled a red brick wall and moved the sliders by three
units, which at GIF size is a click that does nothing. The scene now
drags the frame cold first and picks a white air conditioner, so the
correction is visible and the picker's being absolute - set from the
photograph, not from where the sliders were - is what the picture
shows. The text says so, and says a blown highlight is refused.
The scene clicked 40px to the right of "pick", on "reset", so the
recording showed a neutral group being reset and a click on the wall
that panned. Re-recorded with the picker fixed: the word lights, the
sample moves temperature and tint, and Before shows what it corrected.
docs/manual/README.md is a tour for a photographer opening DarkRoom for
the first time — one picture per thing, moving where movement is the
point. tools/manual/ is how the pictures are made: drive.py puppeteers the
desktop build on a private Xvfb (launch, click, drag, type, screenshot,
record), scenes.py is each picture as a script, and record.sh runs them
all over a folder and writes the results into docs/manual/media/.
The media is in LFS, with the CI pulls excluding it as they exclude the
fixtures; a screenshot changes wholesale when the interface does.
Nothing in the pictures shows a person, by design: the demo library is
seventy urban and alpine frames, chosen from the catalog's rows that face
detection found nobody in.
The traceability matrix is regenerated here after the rebase that
brought this branch up to master.