The integration brought #1864: a fresh crop keeps the raw develop
aspect untouched, and the first toggle only records "free" for the
tool (lib.rs interceptor), so reading develop.crop.aspect never
changes on an untouched photo. The lock the row controls is the
effective one - crop_tool_aspect(), which now starts engaged on
"original" - and that is what the test asserts, like main's own
crop_keys. The feature behavior was already right; only the test's
observation point was stale.
1139/1139 ui-egui lib; fmt clean.
crop.lockAspect is declared like any other command (label Lock Aspect Ratio, default A, both keymap profiles) and lists in Help - Keyboard Shortcuts, editable like the rest. The hidden remapping that turned view.visualizeSpots into crop.aspect while the Crop panel was open is gone: the two now share A as a contextual pair - Visualize Spots fires everywhere except under the Crop panel, where the aspect lock takes the key - so A no longer steals the Remove panel from Crop or runs both commands at once (the old remap plus a direct binding would double-toggle). tests_keymap 28/28.
egui clears `press_origin` on the release, so a slow frame that swallows
press, move and release into one batch reports `primary_pressed` with no
origin: the gesture fell back to the release position and a real drag
wrote nothing (its length check saw a zero-length stub). The gate now
reads the primary press back from the frame's own events before that
fallback — the press decides what it means, wherever the batch ended.
`contains_pointer` reads where the pointer is NOW; a slow frame can
deliver the press on a menu and the move off it in the same pass, and
the gate would then pass with the saved press origin under the menu.
`layer_id_at(press_origin) == resp.layer_id` asks which layer owns the
press itself: the menu's area (or any modal) wins there and the guide
under it stays untouched. The navigator check stays for its own widget.
`primary_pressed` plus `resp.rect.contains` reads the raw pointer, so
anything floating above the photo — the context menu, a dropdown, a
dialog — also started a guide gesture under it. The gate now asks
`resp.contains_pointer()`, which the hit test keeps honest about
covering widgets; the navigator check stays. Two tests press on a menu
over a line and over an endpoint: nothing is selected, moved or drawn.
- a secondary/middle press or one on the navigator panned the photo but
also selected or drew a guide: the gestures take the primary button and
skip the navigator's rect
- egui forgets the press origin by the release, so the endpoint drag
compared against the guide's old position: a press that landed within
the grab radius and never moved wrote a no-op guide; the gesture now
carries the press point itself
- a selection left behind by a Delete past the last guide clears with the
photo/tool check in crop_overlay
A press starts what it means right away from the pointer's press point: on an endpoint (within 7 px) it drags that endpoint, committing one geometry.guides edit on release (the guide must stay at least 0.02 long; a press without a real move only selects, so a click never leaves a no-op undo entry); on a line (within 6 px) it selects the guide (a press on the selected one deselects); anywhere else it starts a new guide from the press point itself instead of where egui's drag threshold was crossed, leaving nothing selected. The endpoint being dragged is drawn bright while the pointer moves. tests_guided_guides 8/8.
A click near a guide selects it (white, thicker, larger endpoints); drawing a new guide or clicking away clears the selection, and so does leaving the photo or the tool. Delete / Backspace with the tool open removes only the selected guide (geometry.guides minus its index, one undo step); with none selected it removes nothing — the photo itself is never deleted while Guided Upright is open. selected_guide is UI-only (serde skip). Coverage: tests_guided_guides 5/5.
The folder's mtime may not move from the save's listing to the list that follows (same clock tick on a fast machine), so the stale cache hid the bad files and problems came back empty - deterministic on a warm build. The test drops the cache as its own wild.json case already did (and as templates_changed documents).
The folder tree writes FolderNode.path with forward slashes on every platform, but tests_folder_drop built its widget ids from to_string_lossy, which gives backslashes on Windows: the 8 tests never found their rows there. row_path() normalizes the way tests_sync already does. Same class of fix in tests_export (path comparison) and an unused-import guard in catalog's staging tests (the module's only test is unix-only).