diff --git a/docs/dev/traceability.md b/docs/dev/traceability.md index 834c9d0..f75d1ba 100644 --- a/docs/dev/traceability.md +++ b/docs/dev/traceability.md @@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 452 | -| TRACES tags found | 1821 | +| Source files scanned | 453 | +| TRACES tags found | 1822 | | Requirements defined | 191 | | Requirements deferred (post-v1) | 24 | | Requirements covered | 160 | @@ -80,7 +80,7 @@ _None._ | FR-DEV-3c | [`core/dr-pipeline/ops/exposure.yaml:1`](../../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:393`](../../core/dr-pipeline/src/graph.rs#L393), [`core/dr-pipeline/src/graph.rs:47`](../../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/mask.rs:1744`](../../core/dr-pipeline/src/mask.rs#L1744), [`core/dr-pipeline/src/preset.rs:752`](../../core/dr-pipeline/src/preset.rs#L752), [`ui/dr-ui/src/develop/rows.rs:928`](../../ui/dr-ui/src/develop/rows.rs#L928) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1039`](../../core/dr-gpu/src/adjust.rs#L1039), [`core/dr-gpu/src/adjust.rs:114`](../../core/dr-gpu/src/adjust.rs#L114), [`core/dr-gpu/src/adjust.rs:1279`](../../core/dr-gpu/src/adjust.rs#L1279), [`core/dr-gpu/src/adjust.rs:1373`](../../core/dr-gpu/src/adjust.rs#L1373), [`core/dr-gpu/src/adjust.rs:84`](../../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:558`](../../core/dr-gpu/tests/local_contrast.rs#L558), [`core/dr-gpu/tests/noise_reduction.rs:556`](../../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:468`](../../core/dr-pipeline/src/framing.rs#L468), [`core/dr-pipeline/src/graph.rs:991`](../../core/dr-pipeline/src/graph.rs#L991), [`core/dr-pipeline/src/operation.rs:32`](../../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:403`](../../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:53`](../../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../../core/dr-pipeline/src/operation.rs#L71) | | FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:127`](../../core/dr-decode/src/lib.rs#L127), [`core/dr-decode/src/lib.rs:777`](../../core/dr-decode/src/lib.rs#L777), [`core/dr-decode/src/lib.rs:817`](../../core/dr-decode/src/lib.rs#L817), [`core/dr-decode/src/profile.rs:102`](../../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:473`](../../core/dr-decode/src/profile.rs#L473), [`core/dr-decode/src/profile.rs:507`](../../core/dr-decode/src/profile.rs#L507), [`core/dr-decode/src/profile.rs:678`](../../core/dr-decode/src/profile.rs#L678), [`core/dr-gpu/src/adjust.rs:1260`](../../core/dr-gpu/src/adjust.rs#L1260), [`core/dr-gpu/src/adjust.rs:37`](../../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/demosaic.rs:142`](../../core/dr-gpu/src/demosaic.rs#L142), [`core/dr-gpu/src/demosaic.rs:86`](../../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:2057`](../../core/dr-pipeline/src/operation.rs#L2057), [`core/dr-pipeline/src/operation.rs:2138`](../../core/dr-pipeline/src/operation.rs#L2138), [`core/dr-pipeline/src/operation.rs:2163`](../../core/dr-pipeline/src/operation.rs#L2163), [`core/dr-pipeline/src/operation.rs:2178`](../../core/dr-pipeline/src/operation.rs#L2178), [`core/dr-pipeline/src/operation.rs:2202`](../../core/dr-pipeline/src/operation.rs#L2202), [`core/dr-pipeline/src/operation.rs:310`](../../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:502`](../../core/dr-pipeline/src/operation.rs#L502), [`core/dr-pipeline/src/operation.rs:512`](../../core/dr-pipeline/src/operation.rs#L512), [`core/dr-pipeline/src/operation.rs:805`](../../core/dr-pipeline/src/operation.rs#L805) | -| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:152`](../../core/dr-gpu/src/adjust.rs#L152), [`core/dr-gpu/src/adjust.rs:408`](../../core/dr-gpu/src/adjust.rs#L408), [`core/dr-gpu/src/adjust.rs:583`](../../core/dr-gpu/src/adjust.rs#L583), [`core/dr-gpu/src/adjust.rs:713`](../../core/dr-gpu/src/adjust.rs#L713), [`core/dr-gpu/src/adjust.rs:77`](../../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:195`](../../core/dr-gpu/tests/film_sim.rs#L195), [`core/dr-gpu/tests/film_sim.rs:1`](../../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:103`](../../core/dr-pipeline/src/graph.rs#L103), [`core/dr-pipeline/src/graph.rs:168`](../../core/dr-pipeline/src/graph.rs#L168), [`core/dr-pipeline/src/graph.rs:566`](../../core/dr-pipeline/src/graph.rs#L566), [`core/dr-pipeline/src/operation.rs:1373`](../../core/dr-pipeline/src/operation.rs#L1373), [`core/dr-pipeline/src/operation.rs:1401`](../../core/dr-pipeline/src/operation.rs#L1401), [`core/dr-pipeline/src/operation.rs:1447`](../../core/dr-pipeline/src/operation.rs#L1447), [`core/dr-pipeline/src/operation.rs:2057`](../../core/dr-pipeline/src/operation.rs#L2057), [`core/dr-pipeline/src/operation.rs:296`](../../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:139`](../../core/dr-pipeline/src/ops/film_sim.rs#L139), [`core/dr-pipeline/src/ops/film_sim.rs:163`](../../core/dr-pipeline/src/ops/film_sim.rs#L163), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:341`](../../core/dr-pipeline/src/ops/film_sim.rs#L341), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:97`](../../core/dr-pipeline/src/ops/film_sim.rs#L97), [`core/dr-pipeline/src/sidecar.rs:1077`](../../core/dr-pipeline/src/sidecar.rs#L1077), [`core/dr-pipeline/src/sidecar.rs:133`](../../core/dr-pipeline/src/sidecar.rs#L133), [`core/dr-pipeline/src/sidecar.rs:217`](../../core/dr-pipeline/src/sidecar.rs#L217), [`core/dr-pipeline/src/sidecar.rs:2722`](../../core/dr-pipeline/src/sidecar.rs#L2722), [`core/dr-pipeline/src/sidecar.rs:2798`](../../core/dr-pipeline/src/sidecar.rs#L2798), [`core/dr-pipeline/src/sidecar.rs:595`](../../core/dr-pipeline/src/sidecar.rs#L595), [`core/dr-pipeline/src/sidecar.rs:898`](../../core/dr-pipeline/src/sidecar.rs#L898), [`core/dr-pipeline/src/state.rs:100`](../../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop/render.rs:1210`](../../ui/dr-ui/src/develop/render.rs#L1210), [`ui/dr-ui/src/develop/render.rs:682`](../../ui/dr-ui/src/develop/render.rs#L682), [`ui/dr-ui/src/develop/render.rs:699`](../../ui/dr-ui/src/develop/render.rs#L699), [`ui/dr-ui/src/develop/render.rs:711`](../../ui/dr-ui/src/develop/render.rs#L711), [`ui/dr-ui/src/develop/render.rs:749`](../../ui/dr-ui/src/develop/render.rs#L749), [`ui/dr-ui/src/develop/render.rs:758`](../../ui/dr-ui/src/develop/render.rs#L758), [`ui/dr-ui/src/develop/render.rs:861`](../../ui/dr-ui/src/develop/render.rs#L861), [`ui/dr-ui/src/develop/render.rs:887`](../../ui/dr-ui/src/develop/render.rs#L887), [`ui/dr-ui/src/develop/session.rs:603`](../../ui/dr-ui/src/develop/session.rs#L603), [`ui/dr-ui/src/develop/tabs.rs:80`](../../ui/dr-ui/src/develop/tabs.rs#L80), [`ui/dr-ui/src/develop_ui.rs:386`](../../ui/dr-ui/src/develop_ui.rs#L386), [`ui/dr-ui/src/lib.rs:839`](../../ui/dr-ui/src/lib.rs#L839), [`ui/dr-ui/src/lib.rs:904`](../../ui/dr-ui/src/lib.rs#L904), [`ui/dr-ui/src/library/sidecar.rs:322`](../../ui/dr-ui/src/library/sidecar.rs#L322), [`ui/dr-ui/src/library/sidecar.rs:49`](../../ui/dr-ui/src/library/sidecar.rs#L49), [`ui/dr-ui/src/presets.rs:422`](../../ui/dr-ui/src/presets.rs#L422), [`ui/dr-ui/ui/adjust.slint:1071`](../../ui/dr-ui/ui/adjust.slint#L1071), [`ui/dr-ui/ui/adjust.slint:1081`](../../ui/dr-ui/ui/adjust.slint#L1081), [`ui/dr-ui/ui/adjust.slint:1298`](../../ui/dr-ui/ui/adjust.slint#L1298) | +| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:152`](../../core/dr-gpu/src/adjust.rs#L152), [`core/dr-gpu/src/adjust.rs:408`](../../core/dr-gpu/src/adjust.rs#L408), [`core/dr-gpu/src/adjust.rs:583`](../../core/dr-gpu/src/adjust.rs#L583), [`core/dr-gpu/src/adjust.rs:713`](../../core/dr-gpu/src/adjust.rs#L713), [`core/dr-gpu/src/adjust.rs:77`](../../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:195`](../../core/dr-gpu/tests/film_sim.rs#L195), [`core/dr-gpu/tests/film_sim.rs:1`](../../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:103`](../../core/dr-pipeline/src/graph.rs#L103), [`core/dr-pipeline/src/graph.rs:168`](../../core/dr-pipeline/src/graph.rs#L168), [`core/dr-pipeline/src/graph.rs:566`](../../core/dr-pipeline/src/graph.rs#L566), [`core/dr-pipeline/src/operation.rs:1373`](../../core/dr-pipeline/src/operation.rs#L1373), [`core/dr-pipeline/src/operation.rs:1401`](../../core/dr-pipeline/src/operation.rs#L1401), [`core/dr-pipeline/src/operation.rs:1447`](../../core/dr-pipeline/src/operation.rs#L1447), [`core/dr-pipeline/src/operation.rs:2057`](../../core/dr-pipeline/src/operation.rs#L2057), [`core/dr-pipeline/src/operation.rs:296`](../../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:139`](../../core/dr-pipeline/src/ops/film_sim.rs#L139), [`core/dr-pipeline/src/ops/film_sim.rs:163`](../../core/dr-pipeline/src/ops/film_sim.rs#L163), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:341`](../../core/dr-pipeline/src/ops/film_sim.rs#L341), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:97`](../../core/dr-pipeline/src/ops/film_sim.rs#L97), [`core/dr-pipeline/src/sidecar.rs:1077`](../../core/dr-pipeline/src/sidecar.rs#L1077), [`core/dr-pipeline/src/sidecar.rs:133`](../../core/dr-pipeline/src/sidecar.rs#L133), [`core/dr-pipeline/src/sidecar.rs:217`](../../core/dr-pipeline/src/sidecar.rs#L217), [`core/dr-pipeline/src/sidecar.rs:2722`](../../core/dr-pipeline/src/sidecar.rs#L2722), [`core/dr-pipeline/src/sidecar.rs:2798`](../../core/dr-pipeline/src/sidecar.rs#L2798), [`core/dr-pipeline/src/sidecar.rs:595`](../../core/dr-pipeline/src/sidecar.rs#L595), [`core/dr-pipeline/src/sidecar.rs:898`](../../core/dr-pipeline/src/sidecar.rs#L898), [`core/dr-pipeline/src/state.rs:100`](../../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop/render.rs:1210`](../../ui/dr-ui/src/develop/render.rs#L1210), [`ui/dr-ui/src/develop/render.rs:682`](../../ui/dr-ui/src/develop/render.rs#L682), [`ui/dr-ui/src/develop/render.rs:699`](../../ui/dr-ui/src/develop/render.rs#L699), [`ui/dr-ui/src/develop/render.rs:711`](../../ui/dr-ui/src/develop/render.rs#L711), [`ui/dr-ui/src/develop/render.rs:749`](../../ui/dr-ui/src/develop/render.rs#L749), [`ui/dr-ui/src/develop/render.rs:758`](../../ui/dr-ui/src/develop/render.rs#L758), [`ui/dr-ui/src/develop/render.rs:861`](../../ui/dr-ui/src/develop/render.rs#L861), [`ui/dr-ui/src/develop/render.rs:887`](../../ui/dr-ui/src/develop/render.rs#L887), [`ui/dr-ui/src/develop/session.rs:603`](../../ui/dr-ui/src/develop/session.rs#L603), [`ui/dr-ui/src/develop/tabs.rs:80`](../../ui/dr-ui/src/develop/tabs.rs#L80), [`ui/dr-ui/src/develop_ui.rs:386`](../../ui/dr-ui/src/develop_ui.rs#L386), [`ui/dr-ui/src/lib.rs:839`](../../ui/dr-ui/src/lib.rs#L839), [`ui/dr-ui/src/lib.rs:904`](../../ui/dr-ui/src/lib.rs#L904), [`ui/dr-ui/src/library/sidecar.rs:322`](../../ui/dr-ui/src/library/sidecar.rs#L322), [`ui/dr-ui/src/library/sidecar.rs:49`](../../ui/dr-ui/src/library/sidecar.rs#L49), [`ui/dr-ui/src/presets.rs:422`](../../ui/dr-ui/src/presets.rs#L422), [`ui/dr-ui/tests/film_list_reaches_every_stock.rs:1`](../../ui/dr-ui/tests/film_list_reaches_every_stock.rs#L1), [`ui/dr-ui/ui/adjust.slint:1071`](../../ui/dr-ui/ui/adjust.slint#L1071), [`ui/dr-ui/ui/adjust.slint:1081`](../../ui/dr-ui/ui/adjust.slint#L1081), [`ui/dr-ui/ui/adjust.slint:1298`](../../ui/dr-ui/ui/adjust.slint#L1298) | | FR-DEV-3h | [`core/dr-decode/src/lib.rs:450`](../../core/dr-decode/src/lib.rs#L450), [`core/dr-decode/src/preview.rs:29`](../../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:1477`](../../core/dr-pipeline/src/framing.rs#L1477), [`core/dr-pipeline/src/framing.rs:486`](../../core/dr-pipeline/src/framing.rs#L486), [`core/dr-pipeline/src/framing.rs:704`](../../core/dr-pipeline/src/framing.rs#L704), [`core/dr-types/src/lib.rs:339`](../../core/dr-types/src/lib.rs#L339), [`core/dr-types/src/lib.rs:447`](../../core/dr-types/src/lib.rs#L447), [`core/dr-types/src/lib.rs:459`](../../core/dr-types/src/lib.rs#L459), [`core/dr-types/src/lib.rs:475`](../../core/dr-types/src/lib.rs#L475), [`ui/dr-ui/src/develop/masks.rs:280`](../../ui/dr-ui/src/develop/masks.rs#L280), [`ui/dr-ui/src/develop/segmentation.rs:114`](../../ui/dr-ui/src/develop/segmentation.rs#L114), [`ui/dr-ui/src/segmentation.rs:499`](../../ui/dr-ui/src/segmentation.rs#L499) | | FR-DEV-3i | [`core/dr-pipeline/src/mask.rs:558`](../../core/dr-pipeline/src/mask.rs#L558), [`core/dr-segment/src/lib.rs:1`](../../core/dr-segment/src/lib.rs#L1), [`ui/dr-ui/src/segmentation.rs:1`](../../ui/dr-ui/src/segmentation.rs#L1) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:1039`](../../core/dr-gpu/src/adjust.rs#L1039), [`core/dr-gpu/src/lib.rs:368`](../../core/dr-gpu/src/lib.rs#L368), [`ui/dr-ui/ui/crop.slint:1`](../../ui/dr-ui/ui/crop.slint#L1) | diff --git a/tools/manual/scenes.py b/tools/manual/scenes.py index e89d518..1033334 100755 --- a/tools/manual/scenes.py +++ b/tools/manual/scenes.py @@ -979,6 +979,90 @@ def film(): undo_all() +FILM_LAST = 'Ilford HP5 Plus' # the last stock `DevelopSession::film_choices` lists +FILM_ROWS = 28 # None and the 27 camera stocks +FILM_ROW, FILM_LIST_MAX = 32, 320 + + +def film_list_open(): + """The film list, open, at its top: closed first if it was open. Not + by pressing the Film row again: a press outside an open list closes it + and goes no further, so that press would only close it.""" + if film_list_is_open(): + key('Escape') + pause(0.6) + dr.click(*in_column('Film@Button')) + dr.wait_for('Fujifilm C200@Text', 5) + pause(0.6) + + +def film_list_is_open(): + """Any stock's row on screen. The Film row names the chosen stock too, + so this is only true of an open list while the choice is None.""" + return any(e['role'] == 'Text' for maker in ('Fujifilm', 'Kodak', 'Ilford') + for e in dr.ask(f'labels {maker}')) + + +def film_list_origin(): + """Where the open list's popup is in the window. The hook reports what + is inside a popup relative to the popup, so a scene adds this back. The + popup asks to sit `gap-sm` (6px) under the Film row, as wide as it, and Slint moves it + up into the window when it would run off the bottom — the same clamp.""" + x0, _, x1, y1 = dr.rect('Film@Button') + W, H = dr.window_size() + h = min(FILM_ROWS * FILM_ROW, FILM_LIST_MAX) + return min(max(0, x0), W - (x1 - x0)), min(max(0, y1 + 6), H - h), x1 - x0, h + + +def film_reached(how): + if not dr.present(f'{FILM_LAST}@Text'): + raise AssertionError(f'the film list: {FILM_LAST} cannot be reached by {how}') + print(f' {FILM_LAST} reached by {how}', flush=True) + + +@scene(media=[], sources=DEVELOP_SRC + ['core/dr-film/src/**']) +def film_reach(): + """Not a picture — a check, run with every recording: the last stock in + the film list can be reached by each way there is to scroll it. It was + once a scroller inside the develop column's, and the column took the + drag and, under a still pointer, the wheel: eighteen stocks, every + black-and-white one, were on the list and out of reach.""" + at_develop() + + # The wheel, in one run with the pointer still: the column used to take + # a run like this whenever it had scrolled under that pointer last. + film_list_open() + px, py, pw, ph = film_list_origin() + wheel(12, px + pw // 2, py + ph // 2) + pause(0.8) + film_reached('the wheel') + + # A drag on the rows, which is what a flick is to a mouse. + film_list_open() + for _ in range(3): + dr.drag(px + pw // 3, py + ph - 20, px + pw // 3, py + 10, 25) + pause(0.8) + film_reached('a drag') + + # The scrollbar's thumb, dragged to the bottom of its track. + film_list_open() + dr.drag(px + pw - 5, py + 6, px + pw - 5, py + ph + 40, 25) + pause(0.6) + film_reached('the scrollbar') + + # The keys, chosen with Enter: the summary names what was chosen. + film_list_open() + for _ in range(FILM_ROWS + 2): + dr.x('key', 'Down') + key('Return') + pause(1.5) + if dr.present('Fujifilm C200@Text') or not dr.present(f'{FILM_LAST}@Text'): + # Closed, and the Film row now names the last stock. + raise AssertionError(f'the film list: Down and Enter did not choose {FILM_LAST}') + print(f' {FILM_LAST} chosen by the keys', flush=True) + undo_all() + + @scene(media=['presets.png'], sources=DEVELOP_SRC + ['ui/dr-ui/ui/presets.slint', 'ui/dr-ui/src/presets.rs']) def presets(): at_develop() diff --git a/ui/dr-ui/Cargo.toml b/ui/dr-ui/Cargo.toml index 29a6b35..9ab2ceb 100644 --- a/ui/dr-ui/Cargo.toml +++ b/ui/dr-ui/Cargo.toml @@ -168,3 +168,7 @@ env_logger.workspace = true # Standing in a test backend behind `dyn RemoteBackend`, which is an # `#[async_trait]` trait — implementing one needs the same attribute. async-trait.workspace = true +# Runs the interface without a display: the film list's reachability test +# drives the real window's event dispatch on Slint's testing backend. The same +# pinned release as the `automation` dependency above, so nothing new is built. +i-slint-backend-testing = { version = "=1.17.1", default-features = false } diff --git a/ui/dr-ui/build.rs b/ui/dr-ui/build.rs index e7912bb..e90c9bb 100644 --- a/ui/dr-ui/build.rs +++ b/ui/dr-ui/build.rs @@ -135,9 +135,14 @@ fn main() { } // The recording hook's element queries walk a tree the compiler only - // describes when asked to (src/automation.rs). Only that build asks: the - // extra tables are dead weight to everyone else. - if std::env::var_os("CARGO_FEATURE_AUTOMATION").is_some() { + // describes when asked to (src/automation.rs), and so do the tests that + // run the interface on Slint's testing backend + // (tests/film_list_reaches_every_stock.rs). So that build and every debug + // build ask; a release build is the one the extra tables are dead weight + // to. + if std::env::var_os("CARGO_FEATURE_AUTOMATION").is_some() + || std::env::var("PROFILE").as_deref() == Ok("debug") + { config = config.with_debug_info(true); } diff --git a/ui/dr-ui/tests/film_list_reaches_every_stock.rs b/ui/dr-ui/tests/film_list_reaches_every_stock.rs new file mode 100644 index 0000000..461ec29 --- /dev/null +++ b/ui/dr-ui/tests/film_list_reaches_every_stock.rs @@ -0,0 +1,232 @@ +// TRACES: FR-DEV-3f +//! Every film stock can be reached: by the keys, by a drag, by the wheel and +//! by the scrollbar. +//! +//! The list once showed ten of twenty-eight stocks and could not be scrolled +//! to the rest. It was a `Flickable` inside the develop column's `Flickable`, +//! and Slint offers a gesture to the outermost one first: the column took +//! every drag, and every wheel that came soon after it had scrolled. The +//! eighteen stocks past Kodachrome 64 — all seven black-and-white ones among +//! them — were in the model and out of reach, and to the photographer they +//! looked deleted. Nothing failed: the data was right and the markup +//! compiled, which is why this runs the interface rather than reading it. +//! +//! It runs on Slint's testing backend, which needs no display and draws +//! nothing, so CI can run it; the events go through the same dispatch a +//! window's do — the popup, the Flickables, their arbitration. The recorded +//! counterpart, on a real X server and a real pointer, is the `film_reach` +//! scene in `tools/manual/scenes.py`. +//! +//! The element queries need the compiler's debug tables, which `build.rs` +//! emits for debug builds and the `automation` feature, so this is compiled +//! out of a release test run. +#![cfg(debug_assertions)] + +use std::cell::Cell; +use std::rc::Rc; +use std::time::Duration; + +use dr_ui::{Adjustments, AppWindow, Develop}; +use i_slint_backend_testing::{init_no_event_loop, mock_elapsed_time, ElementHandle}; +use slint::platform::{Key, PointerEventButton, WindowEvent}; +use slint::{ComponentHandle, LogicalPosition, ModelRc, SharedString, VecModel}; + +const STOCKS: usize = 28; + +fn stock(i: usize) -> String { + format!("Stock {i:02}") +} + +fn last() -> String { + stock(STOCKS - 1) +} + +/// The develop view with an image open and a film group of `STOCKS` stocks. +fn develop() -> (AppWindow, Rc>) { + let app = AppWindow::new().expect("the window builds on the testing backend"); + // Tall enough that the column holds the film row without scrolling: + // this is about the list's own scrolling, not the column's. + app.window().set_size(slint::LogicalSize::new(1600., 2400.)); + app.global::().set_enabled(true); + let adjustments = app.global::(); + adjustments.set_film_in_group(true); + adjustments.set_film_selected(0); + let names: Vec = (0..STOCKS).map(|i| stock(i).into()).collect(); + adjustments.set_film_stocks(ModelRc::new(VecModel::from(names))); + let picked = Rc::new(Cell::new(-1)); + adjustments.on_film_picked({ + let picked = picked.clone(); + move |i| picked.set(i) + }); + app.show().unwrap(); + (app, picked) +} + +/// Whether a stock's row is drawn — `find_by_accessible_label` skips what a +/// clip hides, which is what a scrolled-away row is. +fn shown(app: &AppWindow, label: &str) -> bool { + ElementHandle::find_by_accessible_label(app, label) + .next() + .is_some() +} + +fn element(app: &AppWindow, label: &str) -> ElementHandle { + ElementHandle::find_by_accessible_label(app, label) + .next() + .unwrap_or_else(|| panic!("{label} is not on screen")) +} + +fn centre(e: &ElementHandle) -> LogicalPosition { + let (at, size) = (e.absolute_position(), e.size()); + LogicalPosition::new(at.x + size.width / 2., at.y + size.height / 2.) +} + +fn key(app: &AppWindow, k: Key) { + let text: SharedString = k.into(); + app.window() + .dispatch_event(WindowEvent::KeyPressed { text: text.clone() }); + app.window() + .dispatch_event(WindowEvent::KeyReleased { text }); +} + +fn film_row(app: &AppWindow) -> ElementHandle { + ElementHandle::find_by_accessible_label(app, "Film") + .find(|e| e.accessible_role() == Some(i_slint_backend_testing::AccessibleRole::Button)) + .expect("the film summary row is on screen") +} + +/// Where the open list's popup is. What is inside a popup reports its +/// position relative to the popup, and the pointer is in the window, so +/// this is added back: the popup sits `gap-sm` (6px) under the Film row, +/// and the window here is tall enough that Slint does not move it. +fn origin(app: &AppWindow) -> (f32, f32) { + let row = film_row(app); + let (at, size) = (row.absolute_position(), row.size()); + (at.x, at.y + size.height + 6.) +} + +/// `e`'s centre in the window, for an element inside the popup. +fn in_popup(app: &AppWindow, e: &ElementHandle) -> LogicalPosition { + let (ox, oy) = origin(app); + let c = centre(e); + LogicalPosition::new(ox + c.x, oy + c.y) +} + +fn open(app: &AppWindow) { + // Not by clicking the Film row again: a press outside an open popup + // closes it and goes no further. Stock 00 is left out because the Film + // row names the chosen stock too. + if (1..STOCKS).any(|i| shown(app, &stock(i))) { + key(app, Key::Escape); + settle(); + } + film_row(app).mock_single_click(PointerEventButton::Left); + settle(); + assert!( + shown(app, &stock(1)), + "clicking the film row opens the list" + ); + assert!(!shown(app, &last()), "the list opens at its top, bounded"); +} + +/// Let flicks and wheel animations run out. +fn settle() { + mock_elapsed_time(Duration::from_millis(2000)); +} + +/// A pointer drag, as a hand makes one: small steps a frame apart. +fn drag(app: &AppWindow, from: LogicalPosition, to: LogicalPosition) { + let w = app.window(); + w.dispatch_event(WindowEvent::PointerMoved { position: from }); + w.dispatch_event(WindowEvent::PointerPressed { + position: from, + button: PointerEventButton::Left, + }); + let steps = 40; + for i in 1..=steps { + let t = i as f32 / steps as f32; + let at = LogicalPosition::new(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t); + mock_elapsed_time(Duration::from_millis(16)); + w.dispatch_event(WindowEvent::PointerMoved { position: at }); + } + mock_elapsed_time(Duration::from_millis(300)); + w.dispatch_event(WindowEvent::PointerReleased { + position: to, + button: PointerEventButton::Left, + }); + settle(); +} + +#[test] +fn every_film_stock_can_be_reached() { + init_no_event_loop(); + let (app, picked) = develop(); + + // The keys: Down past the end, then Enter chooses the last stock. + open(&app); + for _ in 0..STOCKS + 5 { + key(&app, Key::DownArrow); + } + assert!( + shown(&app, &last()), + "Down walks the list to its last stock" + ); + key(&app, Key::Return); + settle(); + assert_eq!( + picked.get(), + STOCKS as i32 - 1, + "Enter chooses the stock the keys are on" + ); + assert!(!shown(&app, &stock(1)), "choosing closes the list"); + + // A drag on the rows, which is a flick to a finger. + app.global::().set_film_selected(0); + open(&app); + for _ in 0..4 { + if shown(&app, &last()) { + break; + } + // Bottom of the list to its top, wherever it has scrolled to. + let (ox, oy) = origin(&app); + drag( + &app, + LogicalPosition::new(ox + 100., oy + 300.), + LogicalPosition::new(ox + 100., oy + 20.), + ); + } + assert!( + shown(&app, &last()), + "dragging the list reaches its last stock" + ); + + // The wheel, in one run with the pointer still. + open(&app); + let over = in_popup(&app, &element(&app, &stock(3))); + for _ in 0..40 { + app.window().dispatch_event(WindowEvent::PointerScrolled { + position: over, + delta_x: 0., + delta_y: -60., + }); + mock_elapsed_time(Duration::from_millis(40)); + } + settle(); + assert!(shown(&app, &last()), "the wheel reaches the last stock"); + + // The scrollbar's thumb, dragged to the bottom of its track. + open(&app); + let bar = ElementHandle::find_by_element_type_name(&app, "ScrollBar") + .find(|e| e.size().height > 0.) + .expect("the open list wears a scrollbar"); + let (ox, oy) = origin(&app); + let (at, size) = (bar.absolute_position(), bar.size()); + let x = ox + at.x + size.width / 2.; + let top = oy + at.y; + drag( + &app, + LogicalPosition::new(x, top + 4.), + LogicalPosition::new(x, top + size.height + 40.), + ); + assert!(shown(&app, &last()), "the scrollbar reaches the last stock"); +}