Load the window around the whole view, not around its first cell
The bottom row of the grid was often blank, at a scroll position the user could sit at indefinitely. Two numbers decided when the loaded window follows the view, they lived in different languages, and they disagreed. The grid loaded three screenfuls and Rust held the window still until the first visible cell was three quarters of the way through them. Three quarters of three screenfuls is 2.25, and the view itself is one screenful tall — so the bottom of the screen had already travelled a quarter of a screenful past the last loaded cell before anything moved. Those rows are not in the model, so nothing is drawn for them. The same margin was wrong upward, and exactly so. The window is placed a quarter of itself behind the view, and the margin then declared the view too close to the top at precisely that distance: every single row scrolled upward re-read the catalog, rebuilt all 360 cells and re-queried their badges and ratings, and so did the row after it. So the grid now reports what it shows — a screenful, counting the row the scroll position has cut in half, which `visible-rows` alone undercounts and which is exactly the row reported missing — and Rust owns the rest: four screenfuls loaded, placed a quarter back, and moved once the view comes within half a screenful of an edge of them. One decision in one place, and the test now walks the view the length of the library and asserts the window covers it at every step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 180 |
|
||||
| TRACES tags found | 561 |
|
||||
| TRACES tags found | 562 |
|
||||
| Requirements defined | 177 |
|
||||
| Requirements covered | 89 |
|
||||
| **Coverage** | **50.3%** (89/177) |
|
||||
@@ -38,14 +38,14 @@ _None._
|
||||
| FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:949`](../ui/dr-ui/src/lib.rs#L949), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2105`](../ui/dr-ui/src/library.rs#L2105), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
||||
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:109`](../core/dr-pipeline/src/sidecar.rs#L109) |
|
||||
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
|
||||
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:365`](../core/dr-catalog/src/schema.rs#L365), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1218`](../ui/dr-ui/src/collections_ui.rs#L1218), [`ui/dr-ui/src/collections_ui.rs:1988`](../ui/dr-ui/src/collections_ui.rs#L1988), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:198`](../ui/dr-ui/src/library.rs#L198), [`ui/dr-ui/src/library.rs:2986`](../ui/dr-ui/src/library.rs#L2986), [`ui/dr-ui/src/library.rs:3018`](../ui/dr-ui/src/library.rs#L3018), [`ui/dr-ui/src/library_ui.rs:151`](../ui/dr-ui/src/library_ui.rs#L151), [`ui/dr-ui/src/library_ui.rs:722`](../ui/dr-ui/src/library_ui.rs#L722), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
|
||||
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:365`](../core/dr-catalog/src/schema.rs#L365), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1995`](../ui/dr-ui/src/collections_ui.rs#L1995), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:198`](../ui/dr-ui/src/library.rs#L198), [`ui/dr-ui/src/library.rs:2986`](../ui/dr-ui/src/library.rs#L2986), [`ui/dr-ui/src/library.rs:3018`](../ui/dr-ui/src/library.rs#L3018), [`ui/dr-ui/src/library_ui.rs:151`](../ui/dr-ui/src/library_ui.rs#L151), [`ui/dr-ui/src/library_ui.rs:722`](../ui/dr-ui/src/library_ui.rs#L722), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
|
||||
| FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) |
|
||||
| FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
||||
| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2581`](../ui/dr-ui/src/library.rs#L2581), [`ui/dr-ui/src/library.rs:2607`](../ui/dr-ui/src/library.rs#L2607), [`ui/dr-ui/src/library_ui.rs:138`](../ui/dr-ui/src/library_ui.rs#L138), [`ui/dr-ui/src/library_ui.rs:3539`](../ui/dr-ui/src/library_ui.rs#L3539), [`ui/dr-ui/src/library_ui.rs:4470`](../ui/dr-ui/src/library_ui.rs#L4470), [`ui/dr-ui/ui/app.slint:513`](../ui/dr-ui/ui/app.slint#L513), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
||||
| FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) |
|
||||
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:126`](../core/dr-pipeline/src/sidecar.rs#L126), [`ui/dr-ui/src/collections_ui.rs:1571`](../ui/dr-ui/src/collections_ui.rs#L1571), [`ui/dr-ui/src/collections_ui.rs:1590`](../ui/dr-ui/src/collections_ui.rs#L1590), [`ui/dr-ui/src/collections_ui.rs:236`](../ui/dr-ui/src/collections_ui.rs#L236), [`ui/dr-ui/src/collections_ui.rs:333`](../ui/dr-ui/src/collections_ui.rs#L333), [`ui/dr-ui/src/library.rs:3032`](../ui/dr-ui/src/library.rs#L3032), [`ui/dr-ui/src/library_ui.rs:593`](../ui/dr-ui/src/library_ui.rs#L593), [`ui/dr-ui/src/library_ui.rs:6151`](../ui/dr-ui/src/library_ui.rs#L6151), [`ui/dr-ui/src/library_ui.rs:6162`](../ui/dr-ui/src/library_ui.rs#L6162), [`ui/dr-ui/src/library_ui.rs:6175`](../ui/dr-ui/src/library_ui.rs#L6175), [`ui/dr-ui/src/library_ui.rs:6190`](../ui/dr-ui/src/library_ui.rs#L6190), [`ui/dr-ui/src/library_ui.rs:6199`](../ui/dr-ui/src/library_ui.rs#L6199), [`ui/dr-ui/ui/app.slint:458`](../ui/dr-ui/ui/app.slint#L458), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1212`](../ui/dr-ui/ui/library.slint#L1212), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) |
|
||||
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-catalog/src/schema.rs:783`](../core/dr-catalog/src/schema.rs#L783), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:126`](../core/dr-pipeline/src/sidecar.rs#L126), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3032`](../ui/dr-ui/src/library.rs#L3032), [`ui/dr-ui/src/library_ui.rs:593`](../ui/dr-ui/src/library_ui.rs#L593), [`ui/dr-ui/src/library_ui.rs:6151`](../ui/dr-ui/src/library_ui.rs#L6151), [`ui/dr-ui/src/library_ui.rs:6162`](../ui/dr-ui/src/library_ui.rs#L6162), [`ui/dr-ui/src/library_ui.rs:6175`](../ui/dr-ui/src/library_ui.rs#L6175), [`ui/dr-ui/src/library_ui.rs:6190`](../ui/dr-ui/src/library_ui.rs#L6190), [`ui/dr-ui/src/library_ui.rs:6199`](../ui/dr-ui/src/library_ui.rs#L6199), [`ui/dr-ui/ui/app.slint:458`](../ui/dr-ui/ui/app.slint#L458), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1212`](../ui/dr-ui/ui/library.slint#L1212), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) |
|
||||
| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:280`](../core/dr-catalog/src/schema.rs#L280), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3209`](../ui/dr-ui/src/library.rs#L3209), [`ui/dr-ui/src/library_ui.rs:282`](../ui/dr-ui/src/library_ui.rs#L282), [`ui/dr-ui/src/library_ui.rs:352`](../ui/dr-ui/src/library_ui.rs#L352), [`ui/dr-ui/src/library_ui.rs:5020`](../ui/dr-ui/src/library_ui.rs#L5020), [`ui/dr-ui/src/library_ui.rs:5073`](../ui/dr-ui/src/library_ui.rs#L5073), [`ui/dr-ui/src/library_ui.rs:6291`](../ui/dr-ui/src/library_ui.rs#L6291), [`ui/dr-ui/ui/app.slint:461`](../ui/dr-ui/ui/app.slint#L461), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/app.slint:816`](../ui/dr-ui/ui/app.slint#L816), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1278`](../ui/dr-ui/ui/library.slint#L1278), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) |
|
||||
| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1686`](../ui/dr-ui/src/collections_ui.rs#L1686), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3397`](../ui/dr-ui/src/library_ui.rs#L3397), [`ui/dr-ui/src/library_ui.rs:4060`](../ui/dr-ui/src/library_ui.rs#L4060), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2516`](../ui/dr-ui/ui/library.slint#L2516), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) |
|
||||
| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3397`](../ui/dr-ui/src/library_ui.rs#L3397), [`ui/dr-ui/src/library_ui.rs:4060`](../ui/dr-ui/src/library_ui.rs#L4060), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2516`](../ui/dr-ui/ui/library.slint#L2516), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) |
|
||||
| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1273`](../core/dr-pipeline/src/sidecar.rs#L1273), [`core/dr-pipeline/src/sidecar.rs:90`](../core/dr-pipeline/src/sidecar.rs#L90), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2600`](../ui/dr-ui/src/develop.rs#L2600), [`ui/dr-ui/src/develop.rs:2628`](../ui/dr-ui/src/develop.rs#L2628), [`ui/dr-ui/src/export.rs:749`](../ui/dr-ui/src/export.rs#L749), [`ui/dr-ui/src/lib.rs:1186`](../ui/dr-ui/src/lib.rs#L1186), [`ui/dr-ui/src/lib.rs:1526`](../ui/dr-ui/src/lib.rs#L1526), [`ui/dr-ui/src/lib.rs:1631`](../ui/dr-ui/src/lib.rs#L1631), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:796`](../ui/dr-ui/src/lib.rs#L796), [`ui/dr-ui/src/library.rs:1528`](../ui/dr-ui/src/library.rs#L1528), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:439`](../ui/dr-ui/src/library.rs#L439), [`ui/dr-ui/src/library.rs:679`](../ui/dr-ui/src/library.rs#L679), [`ui/dr-ui/src/library_ui.rs:4762`](../ui/dr-ui/src/library_ui.rs#L4762), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2325`](../ui/dr-ui/src/develop.rs#L2325), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1488`](../ui/dr-ui/src/library.rs#L1488), [`ui/dr-ui/src/library.rs:1565`](../ui/dr-ui/src/library.rs#L1565), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3316`](../ui/dr-ui/src/library.rs#L3316), [`ui/dr-ui/src/library.rs:439`](../ui/dr-ui/src/library.rs#L439), [`ui/dr-ui/src/library.rs:663`](../ui/dr-ui/src/library.rs#L663), [`ui/dr-ui/src/library.rs:679`](../ui/dr-ui/src/library.rs#L679), [`ui/dr-ui/src/library.rs:733`](../ui/dr-ui/src/library.rs#L733), [`ui/dr-ui/src/library_ui.rs:1529`](../ui/dr-ui/src/library_ui.rs#L1529), [`ui/dr-ui/src/library_ui.rs:1555`](../ui/dr-ui/src/library_ui.rs#L1555), [`ui/dr-ui/src/library_ui.rs:1571`](../ui/dr-ui/src/library_ui.rs#L1571), [`ui/dr-ui/src/library_ui.rs:1665`](../ui/dr-ui/src/library_ui.rs#L1665), [`ui/dr-ui/src/library_ui.rs:193`](../ui/dr-ui/src/library_ui.rs#L193), [`ui/dr-ui/src/library_ui.rs:2252`](../ui/dr-ui/src/library_ui.rs#L2252), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2685`](../ui/dr-ui/src/library_ui.rs#L2685), [`ui/dr-ui/src/library_ui.rs:2907`](../ui/dr-ui/src/library_ui.rs#L2907), [`ui/dr-ui/src/library_ui.rs:3188`](../ui/dr-ui/src/library_ui.rs#L3188), [`ui/dr-ui/src/library_ui.rs:3260`](../ui/dr-ui/src/library_ui.rs#L3260), [`ui/dr-ui/src/library_ui.rs:3448`](../ui/dr-ui/src/library_ui.rs#L3448), [`ui/dr-ui/src/library_ui.rs:3566`](../ui/dr-ui/src/library_ui.rs#L3566), [`ui/dr-ui/src/library_ui.rs:405`](../ui/dr-ui/src/library_ui.rs#L405), [`ui/dr-ui/src/library_ui.rs:463`](../ui/dr-ui/src/library_ui.rs#L463), [`ui/dr-ui/src/library_ui.rs:5118`](../ui/dr-ui/src/library_ui.rs#L5118), [`ui/dr-ui/src/library_ui.rs:5233`](../ui/dr-ui/src/library_ui.rs#L5233), [`ui/dr-ui/src/presets.rs:309`](../ui/dr-ui/src/presets.rs#L309), [`ui/dr-ui/src/presets.rs:321`](../ui/dr-ui/src/presets.rs#L321), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) |
|
||||
@@ -83,9 +83,9 @@ _None._
|
||||
| FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) |
|
||||
| FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489) |
|
||||
| FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) |
|
||||
| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:738`](../core/dr-catalog/src/schema.rs#L738), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3188`](../ui/dr-ui/src/collections_ui.rs#L3188), [`ui/dr-ui/src/collections_ui.rs:614`](../ui/dr-ui/src/collections_ui.rs#L614), [`ui/dr-ui/src/collections_ui.rs:676`](../ui/dr-ui/src/collections_ui.rs#L676), [`ui/dr-ui/src/lib.rs:1557`](../ui/dr-ui/src/lib.rs#L1557), [`ui/dr-ui/src/lib.rs:2297`](../ui/dr-ui/src/lib.rs#L2297), [`ui/dr-ui/src/library.rs:1307`](../ui/dr-ui/src/library.rs#L1307), [`ui/dr-ui/src/library.rs:1330`](../ui/dr-ui/src/library.rs#L1330), [`ui/dr-ui/src/library.rs:1488`](../ui/dr-ui/src/library.rs#L1488), [`ui/dr-ui/src/library_ui.rs:1030`](../ui/dr-ui/src/library_ui.rs#L1030), [`ui/dr-ui/src/library_ui.rs:1103`](../ui/dr-ui/src/library_ui.rs#L1103), [`ui/dr-ui/src/library_ui.rs:1204`](../ui/dr-ui/src/library_ui.rs#L1204), [`ui/dr-ui/src/library_ui.rs:1259`](../ui/dr-ui/src/library_ui.rs#L1259), [`ui/dr-ui/src/library_ui.rs:1377`](../ui/dr-ui/src/library_ui.rs#L1377), [`ui/dr-ui/src/library_ui.rs:1496`](../ui/dr-ui/src/library_ui.rs#L1496), [`ui/dr-ui/src/library_ui.rs:2007`](../ui/dr-ui/src/library_ui.rs#L2007), [`ui/dr-ui/src/library_ui.rs:234`](../ui/dr-ui/src/library_ui.rs#L234), [`ui/dr-ui/src/library_ui.rs:245`](../ui/dr-ui/src/library_ui.rs#L245), [`ui/dr-ui/src/library_ui.rs:253`](../ui/dr-ui/src/library_ui.rs#L253), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:274`](../ui/dr-ui/src/library_ui.rs#L274), [`ui/dr-ui/src/library_ui.rs:364`](../ui/dr-ui/src/library_ui.rs#L364), [`ui/dr-ui/src/library_ui.rs:374`](../ui/dr-ui/src/library_ui.rs#L374), [`ui/dr-ui/src/library_ui.rs:416`](../ui/dr-ui/src/library_ui.rs#L416), [`ui/dr-ui/src/library_ui.rs:479`](../ui/dr-ui/src/library_ui.rs#L479), [`ui/dr-ui/src/library_ui.rs:510`](../ui/dr-ui/src/library_ui.rs#L510), [`ui/dr-ui/src/library_ui.rs:5135`](../ui/dr-ui/src/library_ui.rs#L5135), [`ui/dr-ui/src/library_ui.rs:5153`](../ui/dr-ui/src/library_ui.rs#L5153), [`ui/dr-ui/src/library_ui.rs:5165`](../ui/dr-ui/src/library_ui.rs#L5165), [`ui/dr-ui/src/library_ui.rs:522`](../ui/dr-ui/src/library_ui.rs#L522), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2403`](../ui/dr-ui/ui/app.slint#L2403), [`ui/dr-ui/ui/app.slint:576`](../ui/dr-ui/ui/app.slint#L576), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:973`](../ui/dr-ui/ui/library.slint#L973) |
|
||||
| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:738`](../core/dr-catalog/src/schema.rs#L738), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1557`](../ui/dr-ui/src/lib.rs#L1557), [`ui/dr-ui/src/lib.rs:2297`](../ui/dr-ui/src/lib.rs#L2297), [`ui/dr-ui/src/library.rs:1307`](../ui/dr-ui/src/library.rs#L1307), [`ui/dr-ui/src/library.rs:1330`](../ui/dr-ui/src/library.rs#L1330), [`ui/dr-ui/src/library.rs:1488`](../ui/dr-ui/src/library.rs#L1488), [`ui/dr-ui/src/library_ui.rs:1030`](../ui/dr-ui/src/library_ui.rs#L1030), [`ui/dr-ui/src/library_ui.rs:1103`](../ui/dr-ui/src/library_ui.rs#L1103), [`ui/dr-ui/src/library_ui.rs:1204`](../ui/dr-ui/src/library_ui.rs#L1204), [`ui/dr-ui/src/library_ui.rs:1259`](../ui/dr-ui/src/library_ui.rs#L1259), [`ui/dr-ui/src/library_ui.rs:1377`](../ui/dr-ui/src/library_ui.rs#L1377), [`ui/dr-ui/src/library_ui.rs:1496`](../ui/dr-ui/src/library_ui.rs#L1496), [`ui/dr-ui/src/library_ui.rs:2007`](../ui/dr-ui/src/library_ui.rs#L2007), [`ui/dr-ui/src/library_ui.rs:234`](../ui/dr-ui/src/library_ui.rs#L234), [`ui/dr-ui/src/library_ui.rs:245`](../ui/dr-ui/src/library_ui.rs#L245), [`ui/dr-ui/src/library_ui.rs:253`](../ui/dr-ui/src/library_ui.rs#L253), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:274`](../ui/dr-ui/src/library_ui.rs#L274), [`ui/dr-ui/src/library_ui.rs:364`](../ui/dr-ui/src/library_ui.rs#L364), [`ui/dr-ui/src/library_ui.rs:374`](../ui/dr-ui/src/library_ui.rs#L374), [`ui/dr-ui/src/library_ui.rs:416`](../ui/dr-ui/src/library_ui.rs#L416), [`ui/dr-ui/src/library_ui.rs:479`](../ui/dr-ui/src/library_ui.rs#L479), [`ui/dr-ui/src/library_ui.rs:510`](../ui/dr-ui/src/library_ui.rs#L510), [`ui/dr-ui/src/library_ui.rs:5135`](../ui/dr-ui/src/library_ui.rs#L5135), [`ui/dr-ui/src/library_ui.rs:5153`](../ui/dr-ui/src/library_ui.rs#L5153), [`ui/dr-ui/src/library_ui.rs:5165`](../ui/dr-ui/src/library_ui.rs#L5165), [`ui/dr-ui/src/library_ui.rs:522`](../ui/dr-ui/src/library_ui.rs#L522), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2403`](../ui/dr-ui/ui/app.slint#L2403), [`ui/dr-ui/ui/app.slint:576`](../ui/dr-ui/ui/app.slint#L576), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:973`](../ui/dr-ui/ui/library.slint#L973) |
|
||||
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1259`](../ui/dr-ui/src/library_ui.rs#L1259) |
|
||||
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3188`](../ui/dr-ui/src/collections_ui.rs#L3188), [`ui/dr-ui/src/collections_ui.rs:614`](../ui/dr-ui/src/collections_ui.rs#L614), [`ui/dr-ui/src/collections_ui.rs:676`](../ui/dr-ui/src/collections_ui.rs#L676), [`ui/dr-ui/src/library_ui.rs:1030`](../ui/dr-ui/src/library_ui.rs#L1030), [`ui/dr-ui/src/library_ui.rs:1103`](../ui/dr-ui/src/library_ui.rs#L1103), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
|
||||
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1030`](../ui/dr-ui/src/library_ui.rs#L1030), [`ui/dr-ui/src/library_ui.rs:1103`](../ui/dr-ui/src/library_ui.rs#L1103), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
|
||||
| FR-NC-7 | [`core/dr-sync-nextcloud/src/lib.rs:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:2607`](../ui/dr-ui/src/library.rs#L2607), [`ui/dr-ui/src/library_ui.rs:3539`](../ui/dr-ui/src/library_ui.rs#L3539), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340) |
|
||||
| FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:949`](../ui/dr-ui/src/lib.rs#L949), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
||||
| FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:949`](../ui/dr-ui/src/lib.rs#L949) |
|
||||
@@ -99,9 +99,9 @@ _None._
|
||||
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175) |
|
||||
| FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) |
|
||||
| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2379`](../ui/dr-ui/src/lib.rs#L2379), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1031`](../ui/dr-ui/ui/library.slint#L1031) |
|
||||
| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:1501`](../ui/dr-ui/src/collections_ui.rs#L1501), [`ui/dr-ui/src/collections_ui.rs:1515`](../ui/dr-ui/src/collections_ui.rs#L1515), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:1561`](../ui/dr-ui/src/collections_ui.rs#L1561), [`ui/dr-ui/src/collections_ui.rs:1659`](../ui/dr-ui/src/collections_ui.rs#L1659), [`ui/dr-ui/src/collections_ui.rs:478`](../ui/dr-ui/src/collections_ui.rs#L478), [`ui/dr-ui/src/collections_ui.rs:507`](../ui/dr-ui/src/collections_ui.rs#L507), [`ui/dr-ui/src/collections_ui.rs:988`](../ui/dr-ui/src/collections_ui.rs#L988), [`ui/dr-ui/src/collections_ui.rs:998`](../ui/dr-ui/src/collections_ui.rs#L998), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/library_ui.rs:253`](../ui/dr-ui/src/library_ui.rs#L253), [`ui/dr-ui/src/library_ui.rs:5020`](../ui/dr-ui/src/library_ui.rs#L5020), [`ui/dr-ui/src/library_ui.rs:5165`](../ui/dr-ui/src/library_ui.rs#L5165), [`ui/dr-ui/src/library_ui.rs:6241`](../ui/dr-ui/src/library_ui.rs#L6241), [`ui/dr-ui/ui/app.slint:2132`](../ui/dr-ui/ui/app.slint#L2132), [`ui/dr-ui/ui/app.slint:262`](../ui/dr-ui/ui/app.slint#L262), [`ui/dr-ui/ui/app.slint:606`](../ui/dr-ui/ui/app.slint#L606), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) |
|
||||
| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/library_ui.rs:253`](../ui/dr-ui/src/library_ui.rs#L253), [`ui/dr-ui/src/library_ui.rs:5020`](../ui/dr-ui/src/library_ui.rs#L5020), [`ui/dr-ui/src/library_ui.rs:5165`](../ui/dr-ui/src/library_ui.rs#L5165), [`ui/dr-ui/src/library_ui.rs:6241`](../ui/dr-ui/src/library_ui.rs#L6241), [`ui/dr-ui/ui/app.slint:2132`](../ui/dr-ui/ui/app.slint#L2132), [`ui/dr-ui/ui/app.slint:262`](../ui/dr-ui/ui/app.slint#L262), [`ui/dr-ui/ui/app.slint:606`](../ui/dr-ui/ui/app.slint#L606), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) |
|
||||
| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`ui/dr-ui/src/library_ui.rs:4265`](../ui/dr-ui/src/library_ui.rs#L4265), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/ui/app.slint:1905`](../ui/dr-ui/ui/app.slint#L1905), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) |
|
||||
| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:111`](../ui/dr-ui/src/collections_ui.rs#L111), [`ui/dr-ui/src/collections_ui.rs:124`](../ui/dr-ui/src/collections_ui.rs#L124), [`ui/dr-ui/src/collections_ui.rs:1501`](../ui/dr-ui/src/collections_ui.rs#L1501), [`ui/dr-ui/src/collections_ui.rs:1515`](../ui/dr-ui/src/collections_ui.rs#L1515), [`ui/dr-ui/src/collections_ui.rs:152`](../ui/dr-ui/src/collections_ui.rs#L152), [`ui/dr-ui/src/collections_ui.rs:1561`](../ui/dr-ui/src/collections_ui.rs#L1561), [`ui/dr-ui/src/collections_ui.rs:1659`](../ui/dr-ui/src/collections_ui.rs#L1659), [`ui/dr-ui/src/collections_ui.rs:1686`](../ui/dr-ui/src/collections_ui.rs#L1686), [`ui/dr-ui/src/collections_ui.rs:478`](../ui/dr-ui/src/collections_ui.rs#L478), [`ui/dr-ui/src/collections_ui.rs:507`](../ui/dr-ui/src/collections_ui.rs#L507), [`ui/dr-ui/src/collections_ui.rs:575`](../ui/dr-ui/src/collections_ui.rs#L575), [`ui/dr-ui/src/collections_ui.rs:988`](../ui/dr-ui/src/collections_ui.rs#L988), [`ui/dr-ui/src/collections_ui.rs:998`](../ui/dr-ui/src/collections_ui.rs#L998), [`ui/dr-ui/src/library_ui.rs:4265`](../ui/dr-ui/src/library_ui.rs#L4265), [`ui/dr-ui/src/library_ui.rs:4310`](../ui/dr-ui/src/library_ui.rs#L4310), [`ui/dr-ui/src/library_ui.rs:4413`](../ui/dr-ui/src/library_ui.rs#L4413), [`ui/dr-ui/src/library_ui.rs:4441`](../ui/dr-ui/src/library_ui.rs#L4441), [`ui/dr-ui/src/library_ui.rs:5153`](../ui/dr-ui/src/library_ui.rs#L5153), [`ui/dr-ui/src/library_ui.rs:5165`](../ui/dr-ui/src/library_ui.rs#L5165), [`ui/dr-ui/ui/app.slint:1601`](../ui/dr-ui/ui/app.slint#L1601), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) |
|
||||
| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4265`](../ui/dr-ui/src/library_ui.rs#L4265), [`ui/dr-ui/src/library_ui.rs:4310`](../ui/dr-ui/src/library_ui.rs#L4310), [`ui/dr-ui/src/library_ui.rs:4413`](../ui/dr-ui/src/library_ui.rs#L4413), [`ui/dr-ui/src/library_ui.rs:4441`](../ui/dr-ui/src/library_ui.rs#L4441), [`ui/dr-ui/src/library_ui.rs:5153`](../ui/dr-ui/src/library_ui.rs#L5153), [`ui/dr-ui/src/library_ui.rs:5165`](../ui/dr-ui/src/library_ui.rs#L5165), [`ui/dr-ui/ui/app.slint:1601`](../ui/dr-ui/ui/app.slint#L1601), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) |
|
||||
| FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2024`](../ui/dr-ui/src/lib.rs#L2024), [`ui/dr-ui/src/lib.rs:2413`](../ui/dr-ui/src/lib.rs#L2413), [`ui/dr-ui/src/lib.rs:2598`](../ui/dr-ui/src/lib.rs#L2598), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:297`](../ui/dr-ui/ui/app.slint#L297), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) |
|
||||
| FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:100`](../core/dr-pipeline/src/descriptor.rs#L100), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259) |
|
||||
| NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
||||
|
||||
+183
-53
@@ -24,11 +24,27 @@ use slint::{ComponentHandle, Model as _};
|
||||
use crate::library::{self, ScanMessage, ThumbnailMessage};
|
||||
use crate::{AppWindow, KeywordRow, LibraryCell, TimelineBar};
|
||||
|
||||
/// Window size before the grid has reported its geometry.
|
||||
/// A screenful before the grid has reported its geometry.
|
||||
///
|
||||
/// Only used for the very first load; the grid replaces it with its real
|
||||
/// capacity as soon as it has laid out.
|
||||
const INITIAL_WINDOW: usize = 60;
|
||||
/// Only used for the very first load; the grid replaces it with what it
|
||||
/// actually shows as soon as it has laid out.
|
||||
const INITIAL_VIEWPORT_CELLS: usize = 20;
|
||||
|
||||
/// How many screenfuls the loaded window spans.
|
||||
///
|
||||
/// One of them is on screen, so this buys `SCREENFULS - 1` screenfuls of
|
||||
/// loaded-but-undrawn cells to scroll into, split unevenly by
|
||||
/// [`window_start`]: a quarter of the window above the view, the rest below,
|
||||
/// because a grid is read downward.
|
||||
///
|
||||
/// Owned here rather than in the grid, alongside [`window_move`]'s rule for
|
||||
/// how close the view may come to an edge before the window follows. Split
|
||||
/// across the two — the grid loading three screenfuls, Rust holding on until
|
||||
/// the view was three quarters of the way through them — the two numbers
|
||||
/// disagreed by a quarter of a screenful, and that quarter was rows on screen
|
||||
/// that no loaded cell covered. Which is the bottom row of the grid going
|
||||
/// blank.
|
||||
const SCREENFULS: usize = 4;
|
||||
|
||||
/// Never load fewer than this, whatever the viewport reports.
|
||||
///
|
||||
@@ -91,8 +107,15 @@ pub struct LibraryController {
|
||||
///
|
||||
/// A fixed count is wrong in both directions: too small on a maximised 4K
|
||||
/// window, wasteful on a narrow one. The grid measures itself and reports
|
||||
/// its capacity, including a screenful of margin either side.
|
||||
/// its screenful; this is [`SCREENFULS`] of them.
|
||||
window: RefCell<usize>,
|
||||
/// How many cells the viewport shows at once, as the grid last reported.
|
||||
///
|
||||
/// Kept beside `window` rather than divided back out of it, because
|
||||
/// `MIN_WINDOW` can hold the window above what the screenful implies — and
|
||||
/// a screenful guessed too small is exactly the mistake that leaves the
|
||||
/// bottom row of the grid outside the loaded window.
|
||||
viewport_cells: std::cell::Cell<usize>,
|
||||
/// Which rows already have a thumbnail fetch issued, so scrolling back
|
||||
/// does not refetch what is already on screen.
|
||||
/// Which images have a fetch in flight or already served, keyed on
|
||||
@@ -288,7 +311,8 @@ impl LibraryController {
|
||||
thumb_class: RefCell::new(Vec::new()),
|
||||
offset: RefCell::new(0),
|
||||
resume_at: std::cell::Cell::new(0),
|
||||
window: RefCell::new(INITIAL_WINDOW),
|
||||
window: RefCell::new((INITIAL_VIEWPORT_CELLS * SCREENFULS).max(MIN_WINDOW)),
|
||||
viewport_cells: std::cell::Cell::new(INITIAL_VIEWPORT_CELLS),
|
||||
requested: RefCell::new(Default::default()),
|
||||
scan_timer: RefCell::new(None),
|
||||
thumb_timer: RefCell::new(None),
|
||||
@@ -1703,7 +1727,8 @@ fn schedule_reload(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
// change to `columns` moves every one of them and a viewport left
|
||||
// where it was would be pointing at rows the window no longer covers.
|
||||
let window_size = *ctl_cb.window.borrow();
|
||||
*ctl_cb.offset.borrow_mut() = anchor.saturating_sub(window_size / 4);
|
||||
let total = w.get_library_total().max(0) as usize;
|
||||
*ctl_cb.offset.borrow_mut() = window_start(anchor, window_size, total);
|
||||
load_window(&w, &ctl_cb);
|
||||
w.set_library_scroll_to(anchor as i32);
|
||||
w.set_library_scroll_token(w.get_library_scroll_token() + 1);
|
||||
@@ -1838,6 +1863,19 @@ fn already_served(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where the loaded window starts for a view whose first visible cell is
|
||||
/// `anchor`.
|
||||
///
|
||||
/// A quarter of the window sits above the view, so scrolling back has loaded
|
||||
/// rows to move into, and the remaining three quarters below, because a grid
|
||||
/// is read downward. Clamped to the last position where the window is still
|
||||
/// full — otherwise a scrub to the very end loads a handful of cells and the
|
||||
/// rest of the window addresses images that do not exist.
|
||||
fn window_start(anchor: usize, window_size: usize, total: usize) -> usize {
|
||||
let max_offset = total.saturating_sub(window_size.min(total));
|
||||
anchor.saturating_sub(window_size / 4).min(max_offset)
|
||||
}
|
||||
|
||||
/// Where the loaded window should move to for a view at `first_visible`, or
|
||||
/// `None` to leave it where it is.
|
||||
///
|
||||
@@ -1850,43 +1888,62 @@ fn already_served(
|
||||
///
|
||||
/// # The rule
|
||||
///
|
||||
/// The window is centred on the view — a quarter of it behind, so scrolling
|
||||
/// back has loaded rows to move into — and clamped to the last position where
|
||||
/// it is still full. It moves only once the view comes within a quarter-window
|
||||
/// of an edge of what is loaded, so a drag reloads a few times per screenful
|
||||
/// rather than on every row.
|
||||
/// The window is placed by [`window_start`], and it moves once the view comes
|
||||
/// within half a screenful of an edge of what is loaded — below the *bottom*
|
||||
/// of the view going down, above its top going up.
|
||||
///
|
||||
/// And it never moves to where it already is. That is the case the margin test
|
||||
/// alone gets wrong: at either end of a scope the window is *pinned* — the
|
||||
/// first screenful cannot be centred further back than zero, and the last
|
||||
/// cannot start past `max_offset` — so the margin is unsatisfiable there and
|
||||
/// every row crossed in the first or last quarter of a window re-read the
|
||||
/// catalog, rebuilt the model and issued a thumbnail batch to arrive at the
|
||||
/// offset it already had. On a library of twenty-odd thousand that is a
|
||||
/// stutter at the top and the bottom of every collection, which is exactly
|
||||
/// where a cull begins and ends.
|
||||
/// # Measured against the view, not against a fraction of the window
|
||||
///
|
||||
/// Both halves of that used to be a quarter of `window_size`, and both were
|
||||
/// wrong, in opposite directions and for the same reason: a quarter of a
|
||||
/// three-screenful window is three quarters of a screenful, which is not a
|
||||
/// quantity either edge of the view cares about.
|
||||
///
|
||||
/// - Downward it was too lax. The test asked where the *first* visible cell
|
||||
/// was, so it kept the window still until `first_visible` was three quarters
|
||||
/// of the way through it — by which point the bottom of the view had
|
||||
/// travelled a quarter of a screenful past the last loaded cell. Those rows
|
||||
/// are not in the model, so nothing is drawn for them: the last row of the
|
||||
/// grid, blank, at a scroll position the user can sit at indefinitely.
|
||||
/// - Upward it was too eager, and exactly so. The window is placed a quarter
|
||||
/// of itself behind the view, and the old margin then declared the view too
|
||||
/// close to the top at precisely that distance — so the first row scrolled
|
||||
/// upward after any move re-read the catalog, rebuilt every cell, and
|
||||
/// re-queried the badges and ratings, and so did the row after it.
|
||||
///
|
||||
/// # And it never moves to where it already is
|
||||
///
|
||||
/// That is the case the margin test alone gets wrong: at either end of a scope
|
||||
/// the window is *pinned* — the first screenful cannot be placed further back
|
||||
/// than zero, and the last cannot start past `max_offset` — so the margin is
|
||||
/// unsatisfiable there and every row crossed in the first or last quarter of a
|
||||
/// window re-read the catalog, rebuilt the model and issued a thumbnail batch
|
||||
/// to arrive at the offset it already had. On a library of twenty-odd thousand
|
||||
/// that is a stutter at the top and the bottom of every collection, which is
|
||||
/// exactly where a cull begins and ends.
|
||||
fn window_move(
|
||||
first_visible: usize,
|
||||
current: usize,
|
||||
window_size: usize,
|
||||
on_screen: usize,
|
||||
total: usize,
|
||||
) -> Option<usize> {
|
||||
// The same clamp `load_window` applies, repeated here so this compares
|
||||
// The same placement `load_window` applies, repeated here so this compares
|
||||
// against where the window would come to rest rather than where it was
|
||||
// asked to go.
|
||||
let max_offset = total.saturating_sub(window_size.min(total));
|
||||
let desired = first_visible
|
||||
.saturating_sub(window_size / 4)
|
||||
.min(max_offset);
|
||||
|
||||
let desired = window_start(first_visible, window_size, total);
|
||||
if desired == current {
|
||||
return None;
|
||||
}
|
||||
|
||||
let margin = window_size / 4;
|
||||
let inside =
|
||||
first_visible >= current + margin && first_visible + margin < current + window_size;
|
||||
if inside {
|
||||
// Half a screenful of loaded cells beyond each edge of the view. Enough
|
||||
// that a flick has somewhere to land before the reload it triggers has
|
||||
// finished, small enough that the window still moves only about once per
|
||||
// screenful of travel.
|
||||
let slack = (on_screen / 2).max(1);
|
||||
let above = first_visible >= current + slack;
|
||||
let below = first_visible + on_screen + slack <= current + window_size;
|
||||
if above && below {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -4076,7 +4133,8 @@ fn place_cursor(
|
||||
let offset = *ctl.offset.borrow();
|
||||
if at < offset || at >= offset + loaded {
|
||||
let size = *ctl.window.borrow();
|
||||
*ctl.offset.borrow_mut() = at.saturating_sub(size / 4);
|
||||
let total = window.get_library_total().max(0) as usize;
|
||||
*ctl.offset.borrow_mut() = window_start(at, size, total);
|
||||
load_window(window, ctl);
|
||||
}
|
||||
|
||||
@@ -4385,16 +4443,18 @@ pub fn wire<F>(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_capacity(move |capacity| {
|
||||
window.on_library_viewport_cells(move |on_screen| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if !w.get_show_library() {
|
||||
return;
|
||||
}
|
||||
let capacity = (capacity.max(0) as usize).max(MIN_WINDOW);
|
||||
if capacity == *ctl.window.borrow() {
|
||||
let on_screen = (on_screen.max(0) as usize).max(1);
|
||||
let window_size = (on_screen * SCREENFULS).max(MIN_WINDOW);
|
||||
if window_size == *ctl.window.borrow() && on_screen == ctl.viewport_cells.get() {
|
||||
return;
|
||||
}
|
||||
*ctl.window.borrow_mut() = capacity;
|
||||
ctl.viewport_cells.set(on_screen);
|
||||
*ctl.window.borrow_mut() = window_size;
|
||||
// Coalesced with the column change that almost always accompanies
|
||||
// it: resizing the cells alters both, and reloading once per report
|
||||
// meant two full rebuilds per zoom step.
|
||||
@@ -4484,6 +4544,7 @@ pub fn wire<F>(
|
||||
first_visible,
|
||||
*ctl.offset.borrow(),
|
||||
*ctl.window.borrow(),
|
||||
ctl.viewport_cells.get(),
|
||||
w.get_library_total().max(0) as usize,
|
||||
) else {
|
||||
return;
|
||||
@@ -4643,7 +4704,8 @@ pub fn wire<F>(
|
||||
// Centred like `on_library_scrolled` does it, so scrolling up
|
||||
// from the restored position has loaded rows above it.
|
||||
let window_size = *ctl.window.borrow();
|
||||
*ctl.offset.borrow_mut() = resume.saturating_sub(window_size / 4);
|
||||
let total = w.get_library_total().max(0) as usize;
|
||||
*ctl.offset.borrow_mut() = window_start(resume, window_size, total);
|
||||
load_window(&w, &ctl);
|
||||
|
||||
// Before the grid is shown, not after: the markup gates it on
|
||||
@@ -5625,36 +5687,103 @@ mod tests {
|
||||
|
||||
// --- moving the loaded window ------------------------------------------
|
||||
//
|
||||
// 360 images loaded out of 24,000, centred on the view a quarter back.
|
||||
// A screenful of 120 cells, four of them loaded, out of 24,000.
|
||||
|
||||
const W: usize = 360;
|
||||
const ON_SCREEN: usize = 120;
|
||||
const W: usize = ON_SCREEN * SCREENFULS;
|
||||
const TOTAL: usize = 24_000;
|
||||
|
||||
/// Where the window lands for a view at `first_visible`, as the scroll
|
||||
/// handler would leave it.
|
||||
fn settle(first_visible: usize) -> usize {
|
||||
window_start(first_visible, W, TOTAL)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_view_well_inside_the_loaded_window_does_not_move_it() {
|
||||
// The whole point of loading a screenful either side: scrolling within
|
||||
// it must not touch the catalog.
|
||||
assert_eq!(window_move(5_000, 4_910, W, TOTAL), None);
|
||||
assert_eq!(window_move(5_100, 4_910, W, TOTAL), None);
|
||||
// The whole point of loading screenfuls either side: scrolling within
|
||||
// them must not touch the catalog.
|
||||
let at = settle(5_000);
|
||||
assert_eq!(window_move(5_000, at, W, ON_SCREEN, TOTAL), None);
|
||||
assert_eq!(window_move(5_100, at, W, ON_SCREEN, TOTAL), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_whole_view_stays_inside_the_loaded_window() {
|
||||
// The bug this rule exists for: the test used to ask only where the
|
||||
// *first* visible cell was, so the window sat still while the bottom
|
||||
// of the view hung a quarter of a screenful past the last loaded cell
|
||||
// — rows the model does not hold and the grid therefore draws blank.
|
||||
let mut at = settle(0);
|
||||
for first_visible in (0..TOTAL - ON_SCREEN).step_by(10) {
|
||||
if let Some(moved) = window_move(first_visible, at, W, ON_SCREEN, TOTAL) {
|
||||
at = moved;
|
||||
}
|
||||
assert!(
|
||||
first_visible >= at && first_visible + ON_SCREEN <= at + W,
|
||||
"view {first_visible}..{} is not covered by the window {at}..{}",
|
||||
first_visible + ON_SCREEN,
|
||||
at + W
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_back_a_row_does_not_reload() {
|
||||
// The mirror fault, and the more expensive one: the window is placed a
|
||||
// quarter of itself behind the view, and the old margin declared the
|
||||
// view too close to the top at exactly that distance. So every single
|
||||
// row scrolled upward re-read the catalog and rebuilt every cell.
|
||||
let at = settle(5_200);
|
||||
for row in 1..=4 {
|
||||
let first_visible = 5_200 - row * 10;
|
||||
assert_eq!(
|
||||
window_move(first_visible, at, W, ON_SCREEN, TOTAL),
|
||||
None,
|
||||
"row {first_visible} reloaded the window it was already inside"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_view_reaching_the_edge_of_the_loaded_window_moves_it() {
|
||||
// Close enough to the bottom of what is loaded that scrolling on would
|
||||
// run into rows nobody has read.
|
||||
let moved = window_move(5_200, 4_910, W, TOTAL).expect("the window follows the view");
|
||||
assert_eq!(moved, 5_200 - W / 4, "centred a quarter behind the view");
|
||||
// Far enough down that scrolling on would run into rows nobody has
|
||||
// read.
|
||||
let at = settle(5_000);
|
||||
let far = at + W - ON_SCREEN;
|
||||
let moved = window_move(far, at, W, ON_SCREEN, TOTAL).expect("the window follows the view");
|
||||
assert_eq!(moved, far - W / 4, "placed a quarter behind the view");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_window_moves_about_once_a_screenful() {
|
||||
// Too eager is a stutter under the finger, so the rule is checked from
|
||||
// both sides: the view must cross most of a screenful between reloads.
|
||||
let mut at = settle(0);
|
||||
let mut last = 0;
|
||||
let mut gaps = Vec::new();
|
||||
for first_visible in (0..12_000).step_by(10) {
|
||||
if let Some(moved) = window_move(first_visible, at, W, ON_SCREEN, TOTAL) {
|
||||
at = moved;
|
||||
gaps.push(first_visible - last);
|
||||
last = first_visible;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
gaps.iter().skip(1).all(|g| *g >= ON_SCREEN),
|
||||
"reloaded after less than a screenful of travel: {gaps:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_top_of_the_library_is_not_reloaded_on_every_row() {
|
||||
// `first_visible` cannot be centred further back than zero, so the
|
||||
// `first_visible` cannot be placed further back than zero, so the
|
||||
// margin test can never be satisfied here. Before this rule every one
|
||||
// of these re-read the catalog to arrive at the offset it already had,
|
||||
// which is the stutter at the top of every scope.
|
||||
for first_visible in [0, 6, 30, 89] {
|
||||
assert_eq!(
|
||||
window_move(first_visible, 0, W, TOTAL),
|
||||
window_move(first_visible, 0, W, ON_SCREEN, TOTAL),
|
||||
None,
|
||||
"row {first_visible} asked for a move to offset 0, which is where it is"
|
||||
);
|
||||
@@ -5668,7 +5797,7 @@ mod tests {
|
||||
let pinned = TOTAL - W;
|
||||
for first_visible in [TOTAL - W / 2, TOTAL - 30, TOTAL - 1] {
|
||||
assert_eq!(
|
||||
window_move(first_visible, pinned, W, TOTAL),
|
||||
window_move(first_visible, pinned, W, ON_SCREEN, TOTAL),
|
||||
None,
|
||||
"row {first_visible} asked for a move to the offset it already had"
|
||||
);
|
||||
@@ -5679,7 +5808,8 @@ mod tests {
|
||||
fn the_window_never_starts_past_the_last_full_screenful() {
|
||||
// Otherwise a scrub to the very end loads a handful of cells and the
|
||||
// rest of the window addresses images that do not exist.
|
||||
let moved = window_move(TOTAL - 1, 0, W, TOTAL).expect("a scrub to the end moves");
|
||||
let moved =
|
||||
window_move(TOTAL - 1, 0, W, ON_SCREEN, TOTAL).expect("a scrub to the end moves");
|
||||
assert_eq!(moved, TOTAL - W);
|
||||
}
|
||||
|
||||
@@ -5687,8 +5817,8 @@ mod tests {
|
||||
fn a_library_smaller_than_the_window_stays_at_the_beginning() {
|
||||
// `max_offset` is zero, so there is one valid position and the view
|
||||
// must never ask for another.
|
||||
assert_eq!(window_move(0, 0, W, 40), None);
|
||||
assert_eq!(window_move(39, 0, W, 40), None);
|
||||
assert_eq!(window_move(0, 0, W, ON_SCREEN, 40), None);
|
||||
assert_eq!(window_move(39, 0, W, ON_SCREEN, 40), None);
|
||||
}
|
||||
|
||||
// --- carrying thumbnails across a reload -------------------------------
|
||||
|
||||
@@ -541,7 +541,9 @@ export component AppWindow inherits Window {
|
||||
callback library-sync-now();
|
||||
callback library-columns-changed();
|
||||
callback library-scrolled(int);
|
||||
callback library-capacity(int);
|
||||
/// How many cells the grid's viewport shows at once. Rust sizes the
|
||||
/// loaded window from it — see `library_ui::window_move`.
|
||||
callback library-viewport-cells(int);
|
||||
callback library-cell-clicked(int);
|
||||
callback library-rescan();
|
||||
/// Grid → launch screen, to pick a different folder or account.
|
||||
@@ -1402,7 +1404,7 @@ in property <bool> panel-visible: true;
|
||||
root.library-columns-changed();
|
||||
}
|
||||
scrolled(i) => { root.library-scrolled(i); }
|
||||
capacity-changed(n) => { root.library-capacity(n); }
|
||||
viewport-cells-changed(n) => { root.library-viewport-cells(n); }
|
||||
cell-clicked(i) => { root.library-cell-clicked(i); }
|
||||
rescan() => { root.library-rescan(); }
|
||||
change-library() => { root.library-change(); }
|
||||
|
||||
+24
-12
@@ -1089,9 +1089,15 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// The grid scrolled: the first visible image's ordinal in the library.
|
||||
/// Rust answers by loading the window around that position.
|
||||
callback scrolled(int);
|
||||
/// The viewport can now hold a different number of cells — a resize, or a
|
||||
/// column-count change. Rust resizes the loaded window to match.
|
||||
callback capacity-changed(int);
|
||||
/// How many cells the viewport can show at once changed — a resize, or a
|
||||
/// column-count change.
|
||||
///
|
||||
/// The *screenful*, not the window to load. How many screenfuls are held
|
||||
/// around it, and how close the view may come to an edge of them before
|
||||
/// the window moves, are one decision and it is made in Rust — split
|
||||
/// across the two languages they drifted apart, and the gap between them
|
||||
/// was rows on screen that no loaded cell covered.
|
||||
callback viewport-cells-changed(int);
|
||||
|
||||
// Thumbnail progress is no longer reported here: the batch belongs to a
|
||||
// window of the grid rather than to anything the user asked for, and the
|
||||
@@ -1506,16 +1512,22 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// How many cells the viewport holds, plus a screenful either side so
|
||||
// scrolling has loaded rows to move into rather than blank ones.
|
||||
//
|
||||
// Derived rather than a constant: a fixed window is simultaneously too
|
||||
// small on a maximised 4K window — where three screenfuls fit inside it —
|
||||
// and wasteful on a narrow one.
|
||||
// How tall a screenful is, in rows. Measured rather than fixed: the same
|
||||
// constant is simultaneously too small on a maximised 4K window and
|
||||
// wasteful on a narrow one, and everything the loaded window does is
|
||||
// expressed as a multiple of this.
|
||||
property <int> visible-rows:
|
||||
max(1, ceil(grid-area.height / (cell-size + Theme.gap)));
|
||||
property <int> capacity: root.columns * (root.visible-rows * 3);
|
||||
changed capacity => { root.capacity-changed(root.capacity); }
|
||||
/// Cells the viewport shows at once, counting the row the scroll position
|
||||
/// has cut in half.
|
||||
///
|
||||
/// `visible-rows` is a ceiling on a grid whose first row starts at the top
|
||||
/// of the viewport, and the grid is only ever aligned like that at rest at
|
||||
/// the very top. Scrolled anywhere else, a part-row hangs off each end and
|
||||
/// the viewport touches one row more than that — the row this used to
|
||||
/// undercount is the bottom one, which is the row reported missing.
|
||||
property <int> viewport-cells: root.columns * (root.visible-rows + 1);
|
||||
changed viewport-cells => { root.viewport-cells-changed(root.viewport-cells); }
|
||||
/// Rows the *whole library* occupies, which is what the scrollbar spans.
|
||||
property <int> total-rows: ceil(root.total / max(1, columns));
|
||||
|
||||
@@ -2183,7 +2195,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
//
|
||||
// Creating it on `timeline.length > 0` made the sidebar's 96px
|
||||
// appear the moment the first dates were recorded, which narrowed
|
||||
// the grid — changing `columns` and `capacity`, both of which call
|
||||
// the grid — changing `columns` and `viewport-cells`, both of which call
|
||||
// back into Rust to reload the window. The first sweep flush
|
||||
// therefore landed a reload storm on top of the initial thumbnail
|
||||
// batch. Reserving the column costs 96px on an undated library and
|
||||
|
||||
Reference in New Issue
Block a user