diff --git a/docs/traceability.md b/docs/traceability.md index b3881c6..146814d 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -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) | diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index 0542d0b..487d80b 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -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, + /// 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, /// 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) { // 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 { - // 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( { 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( 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( // 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 ------------------------------- diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index 5a6a41e..bc9d325 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -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 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(); } diff --git a/ui/dr-ui/ui/library.slint b/ui/dr-ui/ui/library.slint index aa7a616..5662a88 100644 --- a/ui/dr-ui/ui/library.slint +++ b/ui/dr-ui/ui/library.slint @@ -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 visible-rows: max(1, ceil(grid-area.height / (cell-size + Theme.gap))); - property 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 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 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