Regenerate the traceability matrix
The platform layer was never scanned, so every FR-PLAT-* and NFR-PORT-* tag in dr-plat was invisible. Coverage 51.4% -> 57.6%, almost all of it pre-existing tags that were simply not being counted.
This commit is contained in:
Generated
+3
@@ -1528,6 +1528,9 @@ dependencies = [
|
|||||||
"keyring-core",
|
"keyring-core",
|
||||||
"log",
|
"log",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+19
@@ -115,6 +115,25 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
base64 = "0.23"
|
base64 = "0.23"
|
||||||
|
|
||||||
|
# Display-server clients, for FR-DSP-8's per-display profile acquisition.
|
||||||
|
#
|
||||||
|
# Neither is a new cost: winit already builds both, so the versions are the
|
||||||
|
# ones Slint's backend has resolved to and pinning anything else here would
|
||||||
|
# compile a second copy. Both are pure Rust — x11rb speaks the X11 wire
|
||||||
|
# protocol itself rather than binding libxcb, and wayland-client binds
|
||||||
|
# libwayland only under a feature that is off — which keeps the Android
|
||||||
|
# cross-compile a plain Rust dependency graph, the same criterion as the TLS
|
||||||
|
# and SQLite choices above. They are declared under a target predicate that
|
||||||
|
# excludes Android, where neither display server exists.
|
||||||
|
#
|
||||||
|
# `staging` on wayland-protocols is what carries `wp_color_manager_v1`: the
|
||||||
|
# colour-management extension is still staging upstream, which is the
|
||||||
|
# protocol-level statement of the thing FR-DSP-8 anticipates when it says
|
||||||
|
# Wayland's colour management "is not universally available".
|
||||||
|
x11rb = { version = "0.13", features = ["randr"] }
|
||||||
|
wayland-client = "0.31"
|
||||||
|
wayland-protocols = { version = "0.32", features = ["client", "staging"] }
|
||||||
|
|
||||||
# Platform secure storage: Secret Service on Linux, Keystore on Android
|
# Platform secure storage: Secret Service on Linux, Keystore on Android
|
||||||
# (FR-NC-2). Credentials never touch the catalog or a plain file.
|
# (FR-NC-2). Credentials never touch the catalog or a plain file.
|
||||||
# keyring 4 restructured its features: `v1` is the default set and brings
|
# keyring 4 restructured its features: `v1` is the default set and brings
|
||||||
|
|||||||
@@ -39,6 +39,22 @@ pub struct Chromaticities {
|
|||||||
pub white: [f64; 2],
|
pub white: [f64; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Chromaticities {
|
||||||
|
/// This gamut's linear RGB to the ICC profile connection space, row-major.
|
||||||
|
///
|
||||||
|
/// The same three columns [`ColourSpace::to_pcs_xyz`] produces, for a set
|
||||||
|
/// of primaries that is *not* one of the four the pipeline knows. That is
|
||||||
|
/// the only reason this is public: a display's profile describes primaries
|
||||||
|
/// nobody chose, and the only way to ask which of the four it is nearest
|
||||||
|
/// is to put both through the same reduction and compare the numbers
|
||||||
|
/// (see `dr_plat::display`). Comparing raw xy pairs instead would be
|
||||||
|
/// wrong, because a profile's colorants have already been adapted to D50
|
||||||
|
/// and a space's published chromaticities have not.
|
||||||
|
pub fn to_pcs_xyz(&self) -> [f32; 9] {
|
||||||
|
narrow(mul(adaptation(white_xyz(self), PCS_D50), rgb_to_xyz(self)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// How a space maps linear light onto the numbers stored in a file.
|
/// How a space maps linear light onto the numbers stored in a file.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum Transfer {
|
pub enum Transfer {
|
||||||
@@ -183,8 +199,7 @@ impl ColourSpace {
|
|||||||
/// defined at D50 and nowhere else — a profile carrying unadapted D65
|
/// defined at D50 and nowhere else — a profile carrying unadapted D65
|
||||||
/// colorants describes a space nobody asked for.
|
/// colorants describes a space nobody asked for.
|
||||||
pub fn to_pcs_xyz(self) -> [f32; 9] {
|
pub fn to_pcs_xyz(self) -> [f32; 9] {
|
||||||
let c = self.chromaticities();
|
self.chromaticities().to_pcs_xyz()
|
||||||
narrow(mul(adaptation(white_xyz(&c), PCS_D50), rgb_to_xyz(&c)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The white-point adaptation folded into [`Self::to_pcs_xyz`], row-major.
|
/// The white-point adaptation folded into [`Self::to_pcs_xyz`], row-major.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ and the reason is that some of the work is done and untagged.
|
|||||||
| FR-DSP-5 zoom and pan | **Substantially done, untagged.** `Framing::view` shrinks the sampled region while the render target keeps its size, so zooming *raises* the resolution the pipeline works at. That is FR-DSP-5's requirement, arrived at without tiles. |
|
| FR-DSP-5 zoom and pan | **Substantially done, untagged.** `Framing::view` shrinks the sampled region while the render target keeps its size, so zooming *raises* the resolution the pipeline works at. That is FR-DSP-5's requirement, arrived at without tiles. |
|
||||||
| FR-DSP-6 colour management | **Done.** Output space is a parameter of composition. |
|
| FR-DSP-6 colour management | **Done.** Output space is a parameter of composition. |
|
||||||
| FR-DSP-7 histogram and clipping | **Done**, GPU-side, no per-frame readback. |
|
| FR-DSP-7 histogram and clipping | **Done**, GPU-side, no per-frame readback. |
|
||||||
| FR-DSP-8 per-display colour | **Absent.** One transform, not per-display. |
|
| FR-DSP-8 per-display colour | **Done**, with one caveat named in §5.4. Acquisition per display server, an sRGB fallback that is visible in About, and the canvas rendered at physical pixel size. |
|
||||||
| FR-PLG-* | **Zero tagged.** But `ops/*.yaml` + `build.rs` is already the class-1 plugin format compiled at build time rather than loaded. |
|
| FR-PLG-* | **Zero tagged.** But `ops/*.yaml` + `build.rs` is already the class-1 plugin format compiled at build time rather than loaded. |
|
||||||
|
|
||||||
Two of the five uncovered display requirements are therefore *measurement and tagging*, not
|
Two of the five uncovered display requirements are therefore *measurement and tagging*, not
|
||||||
@@ -119,15 +119,44 @@ stated in the About page beside the other diagnostics so a photographer can see
|
|||||||
on rather than wonder.
|
on rather than wonder.
|
||||||
|
|
||||||
**5.2 Reacting to a move.** The transform is selected per the display currently showing the canvas
|
**5.2 Reacting to a move.** The transform is selected per the display currently showing the canvas
|
||||||
and updates when the window moves. Slint reports window moves; the composed output space is already
|
and updates when the window moves. The composed output space is already a parameter of composition
|
||||||
a parameter of composition (`compose_with_framing(..., output)`), so a display change is a
|
(`compose_with_framing(..., output)`), so a display change is a recomposition, not a pipeline
|
||||||
recomposition, not a pipeline change. This is the part the existing design makes cheap.
|
change. This is the part the existing design makes cheap.
|
||||||
|
|
||||||
|
Slint turned out *not* to report window moves — there is no `on_moved` on any backend — so the
|
||||||
|
window's position and scale factor are sampled twice a second and the platform re-surveyed only when
|
||||||
|
they differ. See §5.4 for the display server where the position itself is unavailable.
|
||||||
|
|
||||||
**5.3 Fractional scaling.** "Handled without resampling artefacts in the canvas" — the canvas is a
|
**5.3 Fractional scaling.** "Handled without resampling artefacts in the canvas" — the canvas is a
|
||||||
wgpu texture handed to the compositor, so the requirement is that we render at the *physical* pixel
|
wgpu texture handed to the compositor, so the requirement is that we render at the *physical* pixel
|
||||||
size rather than the logical one and let the compositor present 1:1. Worth an explicit test, since
|
size rather than the logical one and let the compositor present 1:1. Worth an explicit test, since
|
||||||
the failure is subtle: a slightly soft canvas that looks like a bad demosaic.
|
the failure is subtle: a slightly soft canvas that looks like a bad demosaic.
|
||||||
|
|
||||||
|
### 5.4 What landed, and the one thing that did not
|
||||||
|
|
||||||
|
`dr_plat::display` surveys the session's displays; `dr_ui::display_ui` decides which one is showing
|
||||||
|
the canvas and keeps `DevelopSession`'s output space pointed at it. Composition was already
|
||||||
|
parameterised on the space, so the pixel path changed by one argument.
|
||||||
|
|
||||||
|
Two things are worth recording because they are trades rather than omissions.
|
||||||
|
|
||||||
|
**A profile is matched to the nearest of four spaces, not applied.** A measured panel is none of
|
||||||
|
`Srgb`, `DisplayP3`, `AdobeRgb` or `ProPhoto`, and a general ICC engine is a much larger piece of
|
||||||
|
work — a CMM, rendering intents, LUT-based profiles, and a per-frame cost to argue about. The
|
||||||
|
profile is reduced to its D50-adapted colorants and matched against the four; a match that is merely
|
||||||
|
nearest is marked as such, and About says "nearest to Display P3" rather than "Display P3". A
|
||||||
|
LUT-based profile, which is what a hardware calibrator often writes, is declined by shape and falls
|
||||||
|
back to sRGB with that stated. An approximation the photographer can see beats a silent one.
|
||||||
|
|
||||||
|
**On Wayland the canvas follows the first output, not the window.** A Wayland client is never told
|
||||||
|
where its window is — `xdg_toplevel` carries no position, deliberately — so the "which display"
|
||||||
|
question cannot be answered by geometry there. The protocol's own answer is
|
||||||
|
`wp_color_management_surface_feedback_v1`, which hands a client the preferred image description for
|
||||||
|
*its surface* and re-sends it on a move; it needs the application's `wl_surface`, which Slint owns
|
||||||
|
and does not expose. So the profiles are read correctly for every output and the *selection* among
|
||||||
|
them is right on X11 and on any single-monitor Wayland session, which is most of them. Closing the
|
||||||
|
gap is a Slint surface handle, not a change to any of this.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Extensibility: the format already exists
|
## 6. Extensibility: the format already exists
|
||||||
|
|||||||
+59
-59
@@ -9,18 +9,18 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
|||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Source files scanned | 254 |
|
| Source files scanned | 265 |
|
||||||
| TRACES tags found | 732 |
|
| TRACES tags found | 765 |
|
||||||
| Requirements defined | 177 |
|
| Requirements defined | 177 |
|
||||||
| Requirements covered | 98 |
|
| Requirements covered | 102 |
|
||||||
| **Coverage** | **55.4%** (98/177) |
|
| **Coverage** | **57.6%** (102/177) |
|
||||||
|
|
||||||
### By type
|
### By type
|
||||||
|
|
||||||
| Type | Covered | Defined |
|
| Type | Covered | Defined |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| FR | 78 | 122 |
|
| FR | 80 | 122 |
|
||||||
| NFR | 18 | 49 |
|
| NFR | 20 | 49 |
|
||||||
| R | 2 | 6 |
|
| R | 2 | 6 |
|
||||||
|
|
||||||
## Orphan tags
|
## Orphan tags
|
||||||
@@ -33,108 +33,112 @@ _None._
|
|||||||
|
|
||||||
| ID | Tagged in |
|
| ID | Tagged in |
|
||||||
|---|---|
|
|---|---|
|
||||||
| FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`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-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) |
|
| FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`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-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) |
|
||||||
| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`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:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1104`](../ui/dr-ui/src/lib.rs#L1104), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1008`](../ui/dr-ui/ui/library.slint#L1008), [`ui/dr-ui/ui/library.slint:1170`](../ui/dr-ui/ui/library.slint#L1170), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) |
|
| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1105`](../ui/dr-ui/src/lib.rs#L1105), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1008`](../ui/dr-ui/ui/library.slint#L1008), [`ui/dr-ui/ui/library.slint:1170`](../ui/dr-ui/ui/library.slint#L1170), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) |
|
||||||
| 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:1104`](../ui/dr-ui/src/lib.rs#L1104), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2128`](../ui/dr-ui/src/library.rs#L2128), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
| 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:1105`](../ui/dr-ui/src/lib.rs#L1105), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2128`](../ui/dr-ui/src/library.rs#L2128), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
||||||
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118) |
|
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118) |
|
||||||
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
|
| 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:583`](../core/dr-catalog/src/schema.rs#L583), [`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:3095`](../ui/dr-ui/src/library.rs#L3095), [`ui/dr-ui/src/library.rs:3129`](../ui/dr-ui/src/library.rs#L3129), [`ui/dr-ui/src/library_ui.rs:184`](../ui/dr-ui/src/library_ui.rs#L184), [`ui/dr-ui/src/library_ui.rs:757`](../ui/dr-ui/src/library_ui.rs#L757), [`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:583`](../core/dr-catalog/src/schema.rs#L583), [`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:3095`](../ui/dr-ui/src/library.rs#L3095), [`ui/dr-ui/src/library.rs:3129`](../ui/dr-ui/src/library.rs#L3129), [`ui/dr-ui/src/library_ui.rs:184`](../ui/dr-ui/src/library_ui.rs#L184), [`ui/dr-ui/src/library_ui.rs:757`](../ui/dr-ui/src/library_ui.rs#L757), [`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-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), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:46`](../platform/dr-plat/src/storage.rs#L46) |
|
||||||
| 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-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:2604`](../ui/dr-ui/src/library.rs#L2604), [`ui/dr-ui/src/library.rs:2630`](../ui/dr-ui/src/library.rs#L2630), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3624`](../ui/dr-ui/src/library_ui.rs#L3624), [`ui/dr-ui/src/library_ui.rs:4590`](../ui/dr-ui/src/library_ui.rs#L4590), [`ui/dr-ui/ui/app.slint:540`](../ui/dr-ui/ui/app.slint#L540), [`ui/dr-ui/ui/settings.slint:353`](../ui/dr-ui/ui/settings.slint#L353), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
| 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:2604`](../ui/dr-ui/src/library.rs#L2604), [`ui/dr-ui/src/library.rs:2630`](../ui/dr-ui/src/library.rs#L2630), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3624`](../ui/dr-ui/src/library_ui.rs#L3624), [`ui/dr-ui/src/library_ui.rs:4590`](../ui/dr-ui/src/library_ui.rs#L4590), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`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), [`core/dr-catalog/src/schema.rs:304`](../core/dr-catalog/src/schema.rs#L304), [`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-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), [`core/dr-catalog/src/schema.rs:304`](../core/dr-catalog/src/schema.rs#L304), [`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:1001`](../core/dr-catalog/src/schema.rs#L1001), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`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:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`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:3143`](../ui/dr-ui/src/library.rs#L3143), [`ui/dr-ui/src/library_ui.rs:6271`](../ui/dr-ui/src/library_ui.rs#L6271), [`ui/dr-ui/src/library_ui.rs:6282`](../ui/dr-ui/src/library_ui.rs#L6282), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6295`](../ui/dr-ui/src/library_ui.rs#L6295), [`ui/dr-ui/src/library_ui.rs:6310`](../ui/dr-ui/src/library_ui.rs#L6310), [`ui/dr-ui/src/library_ui.rs:6319`](../ui/dr-ui/src/library_ui.rs#L6319), [`ui/dr-ui/ui/app.slint:485`](../ui/dr-ui/ui/app.slint#L485), [`ui/dr-ui/ui/app.slint:658`](../ui/dr-ui/ui/app.slint#L658), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`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:902`](../ui/dr-ui/ui/library.slint#L902) |
|
| 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:1001`](../core/dr-catalog/src/schema.rs#L1001), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`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:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`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:3143`](../ui/dr-ui/src/library.rs#L3143), [`ui/dr-ui/src/library_ui.rs:6271`](../ui/dr-ui/src/library_ui.rs#L6271), [`ui/dr-ui/src/library_ui.rs:6282`](../ui/dr-ui/src/library_ui.rs#L6282), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6295`](../ui/dr-ui/src/library_ui.rs#L6295), [`ui/dr-ui/src/library_ui.rs:6310`](../ui/dr-ui/src/library_ui.rs#L6310), [`ui/dr-ui/src/library_ui.rs:6319`](../ui/dr-ui/src/library_ui.rs#L6319), [`ui/dr-ui/ui/app.slint:492`](../ui/dr-ui/ui/app.slint#L492), [`ui/dr-ui/ui/app.slint:665`](../ui/dr-ui/ui/app.slint#L665), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`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:902`](../ui/dr-ui/ui/library.slint#L902) |
|
||||||
| 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:498`](../core/dr-catalog/src/schema.rs#L498), [`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:3389`](../ui/dr-ui/src/library.rs#L3389), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5140`](../ui/dr-ui/src/library_ui.rs#L5140), [`ui/dr-ui/src/library_ui.rs:5193`](../ui/dr-ui/src/library_ui.rs#L5193), [`ui/dr-ui/src/library_ui.rs:6411`](../ui/dr-ui/src/library_ui.rs#L6411), [`ui/dr-ui/ui/app.slint:488`](../ui/dr-ui/ui/app.slint#L488), [`ui/dr-ui/ui/app.slint:658`](../ui/dr-ui/ui/app.slint#L658), [`ui/dr-ui/ui/app.slint:897`](../ui/dr-ui/ui/app.slint#L897), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:128`](../ui/dr-ui/ui/settings.slint#L128) |
|
| 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:498`](../core/dr-catalog/src/schema.rs#L498), [`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:3389`](../ui/dr-ui/src/library.rs#L3389), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5140`](../ui/dr-ui/src/library_ui.rs#L5140), [`ui/dr-ui/src/library_ui.rs:5193`](../ui/dr-ui/src/library_ui.rs#L5193), [`ui/dr-ui/src/library_ui.rs:6411`](../ui/dr-ui/src/library_ui.rs#L6411), [`ui/dr-ui/ui/app.slint:495`](../ui/dr-ui/ui/app.slint#L495), [`ui/dr-ui/ui/app.slint:665`](../ui/dr-ui/ui/app.slint#L665), [`ui/dr-ui/ui/app.slint:904`](../ui/dr-ui/ui/app.slint#L904), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:147`](../ui/dr-ui/ui/settings.slint#L147) |
|
||||||
| 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:3482`](../ui/dr-ui/src/library_ui.rs#L3482), [`ui/dr-ui/src/library_ui.rs:4180`](../ui/dr-ui/src/library_ui.rs#L4180), [`ui/dr-ui/ui/app.slint:653`](../ui/dr-ui/ui/app.slint#L653), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
| 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:3482`](../ui/dr-ui/src/library_ui.rs#L3482), [`ui/dr-ui/src/library_ui.rs:4180`](../ui/dr-ui/src/library_ui.rs#L4180), [`ui/dr-ui/ui/app.slint:660`](../ui/dr-ui/ui/app.slint#L660), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
||||||
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`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:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:3316`](../ui/dr-ui/src/develop.rs#L3316), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`ui/dr-ui/src/lib.rs:1874`](../ui/dr-ui/src/lib.rs#L1874), [`ui/dr-ui/src/lib.rs:480`](../ui/dr-ui/src/lib.rs#L480), [`ui/dr-ui/src/lib.rs:905`](../ui/dr-ui/src/lib.rs#L905), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4882`](../ui/dr-ui/src/library_ui.rs#L4882), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`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:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1366`](../ui/dr-ui/src/lib.rs#L1366), [`ui/dr-ui/src/lib.rs:1767`](../ui/dr-ui/src/lib.rs#L1767), [`ui/dr-ui/src/lib.rs:1903`](../ui/dr-ui/src/lib.rs#L1903), [`ui/dr-ui/src/lib.rs:481`](../ui/dr-ui/src/lib.rs#L481), [`ui/dr-ui/src/lib.rs:906`](../ui/dr-ui/src/lib.rs#L906), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4882`](../ui/dr-ui/src/library_ui.rs#L4882), [`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:555`](../core/dr-catalog/src/schema.rs#L555), [`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:2812`](../ui/dr-ui/src/develop.rs#L2812), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1511`](../ui/dr-ui/src/library.rs#L1511), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3496`](../ui/dr-ui/src/library.rs#L3496), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3345`](../ui/dr-ui/src/library_ui.rs#L3345), [`ui/dr-ui/src/library_ui.rs:3533`](../ui/dr-ui/src/library_ui.rs#L3533), [`ui/dr-ui/src/library_ui.rs:3651`](../ui/dr-ui/src/library_ui.rs#L3651), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5238`](../ui/dr-ui/src/library_ui.rs#L5238), [`ui/dr-ui/src/library_ui.rs:5353`](../ui/dr-ui/src/library_ui.rs#L5353), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`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:555`](../core/dr-catalog/src/schema.rs#L555), [`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:2870`](../ui/dr-ui/src/develop.rs#L2870), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1511`](../ui/dr-ui/src/library.rs#L1511), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3496`](../ui/dr-ui/src/library.rs#L3496), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3345`](../ui/dr-ui/src/library_ui.rs#L3345), [`ui/dr-ui/src/library_ui.rs:3533`](../ui/dr-ui/src/library_ui.rs#L3533), [`ui/dr-ui/src/library_ui.rs:3651`](../ui/dr-ui/src/library_ui.rs#L3651), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5238`](../ui/dr-ui/src/library_ui.rs#L5238), [`ui/dr-ui/src/library_ui.rs:5353`](../ui/dr-ui/src/library_ui.rs#L5353), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`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:121`](../core/dr-decode/src/preview.rs#L121) |
|
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
||||||
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1846`](../ui/dr-ui/src/lib.rs#L1846), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1875`](../ui/dr-ui/src/lib.rs#L1875), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) |
|
| FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) |
|
||||||
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) |
|
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) |
|
||||||
| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:343`](../core/dr-catalog/src/schema.rs#L343), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/ui/settings.slint:385`](../ui/dr-ui/ui/settings.slint#L385), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:343`](../core/dr-catalog/src/schema.rs#L343), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/ui/settings.slint:404`](../ui/dr-ui/ui/settings.slint#L404), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
||||||
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
||||||
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) |
|
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) |
|
||||||
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:617`](../core/dr-pipeline/src/framing.rs#L617), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:479`](../core/dr-pipeline/src/operation.rs#L479), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:1687`](../core/dr-pipeline/src/sidecar.rs#L1687), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1280`](../ui/dr-ui/src/develop.rs#L1280), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1758`](../ui/dr-ui/src/develop.rs#L1758), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:1790`](../ui/dr-ui/src/develop.rs#L1790), [`ui/dr-ui/src/develop.rs:1812`](../ui/dr-ui/src/develop.rs#L1812), [`ui/dr-ui/src/develop.rs:1958`](../ui/dr-ui/src/develop.rs#L1958), [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3851`](../ui/dr-ui/src/develop.rs#L3851), [`ui/dr-ui/src/develop.rs:3905`](../ui/dr-ui/src/develop.rs#L3905), [`ui/dr-ui/src/develop.rs:3949`](../ui/dr-ui/src/develop.rs#L3949), [`ui/dr-ui/src/develop.rs:3999`](../ui/dr-ui/src/develop.rs#L3999), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1422`](../ui/dr-ui/src/lib.rs#L1422), [`ui/dr-ui/src/lib.rs:2124`](../ui/dr-ui/src/lib.rs#L2124), [`ui/dr-ui/src/lib.rs:301`](../ui/dr-ui/src/lib.rs#L301), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:2105`](../ui/dr-ui/ui/app.slint#L2105), [`ui/dr-ui/ui/app.slint:984`](../ui/dr-ui/ui/app.slint#L984) |
|
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:617`](../core/dr-pipeline/src/framing.rs#L617), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:479`](../core/dr-pipeline/src/operation.rs#L479), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:1687`](../core/dr-pipeline/src/sidecar.rs#L1687), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1451`](../ui/dr-ui/src/lib.rs#L1451), [`ui/dr-ui/src/lib.rs:2153`](../ui/dr-ui/src/lib.rs#L2153), [`ui/dr-ui/src/lib.rs:302`](../ui/dr-ui/src/lib.rs#L302), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:2118`](../ui/dr-ui/ui/app.slint#L2118), [`ui/dr-ui/ui/app.slint:991`](../ui/dr-ui/ui/app.slint#L991) |
|
||||||
| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1177`](../ui/dr-ui/src/develop.rs#L1177), [`ui/dr-ui/src/lib.rs:595`](../ui/dr-ui/src/lib.rs#L595) |
|
| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1194`](../ui/dr-ui/src/develop.rs#L1194), [`ui/dr-ui/src/lib.rs:596`](../ui/dr-ui/src/lib.rs#L596) |
|
||||||
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) |
|
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) |
|
||||||
| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:4578`](../ui/dr-ui/src/develop.rs#L4578) |
|
| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:4636`](../ui/dr-ui/src/develop.rs#L4636) |
|
||||||
| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`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:476`](../core/dr-gpu/tests/local_contrast.rs#L476), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:612`](../core/dr-pipeline/src/graph.rs#L612), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) |
|
| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`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:476`](../core/dr-gpu/tests/local_contrast.rs#L476), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:612`](../core/dr-pipeline/src/graph.rs#L612), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) |
|
||||||
| 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:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`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:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`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:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:1505`](../core/dr-pipeline/src/operation.rs#L1505), [`core/dr-pipeline/src/operation.rs:1530`](../core/dr-pipeline/src/operation.rs#L1530), [`core/dr-pipeline/src/operation.rs:1545`](../core/dr-pipeline/src/operation.rs#L1545), [`core/dr-pipeline/src/operation.rs:1569`](../core/dr-pipeline/src/operation.rs#L1569), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:563`](../core/dr-pipeline/src/operation.rs#L563) |
|
| 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:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`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:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`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:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:1505`](../core/dr-pipeline/src/operation.rs#L1505), [`core/dr-pipeline/src/operation.rs:1530`](../core/dr-pipeline/src/operation.rs#L1530), [`core/dr-pipeline/src/operation.rs:1545`](../core/dr-pipeline/src/operation.rs#L1545), [`core/dr-pipeline/src/operation.rs:1569`](../core/dr-pipeline/src/operation.rs#L1569), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:563`](../core/dr-pipeline/src/operation.rs#L563) |
|
||||||
| 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:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1028`](../core/dr-pipeline/src/operation.rs#L1028), [`core/dr-pipeline/src/operation.rs:1057`](../core/dr-pipeline/src/operation.rs#L1057), [`core/dr-pipeline/src/operation.rs:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:150`](../core/dr-pipeline/src/ops/film_sim.rs#L150), [`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:328`](../core/dr-pipeline/src/ops/film_sim.rs#L328), [`core/dr-pipeline/src/ops/film_sim.rs:42`](../core/dr-pipeline/src/ops/film_sim.rs#L42), [`core/dr-pipeline/src/ops/film_sim.rs:85`](../core/dr-pipeline/src/ops/film_sim.rs#L85), [`core/dr-pipeline/src/ops/film_sim.rs:90`](../core/dr-pipeline/src/ops/film_sim.rs#L90), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1948`](../core/dr-pipeline/src/sidecar.rs#L1948), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`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.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:2844`](../ui/dr-ui/src/develop.rs#L2844), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:2894`](../ui/dr-ui/src/develop.rs#L2894), [`ui/dr-ui/src/develop.rs:2903`](../ui/dr-ui/src/develop.rs#L2903), [`ui/dr-ui/src/develop.rs:3006`](../ui/dr-ui/src/develop.rs#L3006), [`ui/dr-ui/src/develop.rs:3324`](../ui/dr-ui/src/develop.rs#L3324), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/lib.rs:2098`](../ui/dr-ui/src/lib.rs#L2098), [`ui/dr-ui/src/lib.rs:528`](../ui/dr-ui/src/lib.rs#L528), [`ui/dr-ui/src/lib.rs:586`](../ui/dr-ui/src/lib.rs#L586), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:889`](../ui/dr-ui/ui/adjust.slint#L889), [`ui/dr-ui/ui/adjust.slint:959`](../ui/dr-ui/ui/adjust.slint#L959), [`ui/dr-ui/ui/app.slint:2717`](../ui/dr-ui/ui/app.slint#L2717), [`ui/dr-ui/ui/app.slint:778`](../ui/dr-ui/ui/app.slint#L778) |
|
| 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:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1028`](../core/dr-pipeline/src/operation.rs#L1028), [`core/dr-pipeline/src/operation.rs:1057`](../core/dr-pipeline/src/operation.rs#L1057), [`core/dr-pipeline/src/operation.rs:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/ops/film_sim.rs:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:150`](../core/dr-pipeline/src/ops/film_sim.rs#L150), [`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:328`](../core/dr-pipeline/src/ops/film_sim.rs#L328), [`core/dr-pipeline/src/ops/film_sim.rs:42`](../core/dr-pipeline/src/ops/film_sim.rs#L42), [`core/dr-pipeline/src/ops/film_sim.rs:85`](../core/dr-pipeline/src/ops/film_sim.rs#L85), [`core/dr-pipeline/src/ops/film_sim.rs:90`](../core/dr-pipeline/src/ops/film_sim.rs#L90), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1948`](../core/dr-pipeline/src/sidecar.rs#L1948), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`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.rs:2885`](../ui/dr-ui/src/develop.rs#L2885), [`ui/dr-ui/src/develop.rs:2902`](../ui/dr-ui/src/develop.rs#L2902), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:2952`](../ui/dr-ui/src/develop.rs#L2952), [`ui/dr-ui/src/develop.rs:2961`](../ui/dr-ui/src/develop.rs#L2961), [`ui/dr-ui/src/develop.rs:3064`](../ui/dr-ui/src/develop.rs#L3064), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/lib.rs:2127`](../ui/dr-ui/src/lib.rs#L2127), [`ui/dr-ui/src/lib.rs:529`](../ui/dr-ui/src/lib.rs#L529), [`ui/dr-ui/src/lib.rs:587`](../ui/dr-ui/src/lib.rs#L587), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:889`](../ui/dr-ui/ui/adjust.slint#L889), [`ui/dr-ui/ui/adjust.slint:959`](../ui/dr-ui/ui/adjust.slint#L959), [`ui/dr-ui/ui/app.slint:2730`](../ui/dr-ui/ui/app.slint#L2730), [`ui/dr-ui/ui/app.slint:785`](../ui/dr-ui/ui/app.slint#L785) |
|
||||||
| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:924`](../core/dr-pipeline/src/framing.rs#L924), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) |
|
| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:924`](../core/dr-pipeline/src/framing.rs#L924), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1992`](../ui/dr-ui/src/develop.rs#L1992), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) |
|
||||||
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
||||||
| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3369`](../ui/dr-ui/src/develop.rs#L3369), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3394`](../ui/dr-ui/src/develop.rs#L3394), [`ui/dr-ui/src/develop.rs:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/develop.rs:3446`](../ui/dr-ui/src/develop.rs#L3446), [`ui/dr-ui/src/develop.rs:3465`](../ui/dr-ui/src/develop.rs#L3465), [`ui/dr-ui/src/develop.rs:3481`](../ui/dr-ui/src/develop.rs#L3481), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1386`](../ui/dr-ui/src/lib.rs#L1386), [`ui/dr-ui/src/lib.rs:1400`](../ui/dr-ui/src/lib.rs#L1400), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/develop.rs:3427`](../ui/dr-ui/src/develop.rs#L3427), [`ui/dr-ui/src/develop.rs:3440`](../ui/dr-ui/src/develop.rs#L3440), [`ui/dr-ui/src/develop.rs:3452`](../ui/dr-ui/src/develop.rs#L3452), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3504`](../ui/dr-ui/src/develop.rs#L3504), [`ui/dr-ui/src/develop.rs:3523`](../ui/dr-ui/src/develop.rs#L3523), [`ui/dr-ui/src/develop.rs:3539`](../ui/dr-ui/src/develop.rs#L3539), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1399`](../ui/dr-ui/src/lib.rs#L1399), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:1437`](../ui/dr-ui/src/lib.rs#L1437), [`ui/dr-ui/src/lib.rs:2323`](../ui/dr-ui/src/lib.rs#L2323), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
||||||
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3281`](../ui/dr-ui/src/develop.rs#L3281), [`ui/dr-ui/src/develop.rs:3302`](../ui/dr-ui/src/develop.rs#L3302), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:547`](../ui/dr-ui/src/settings_ui.rs#L547), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) |
|
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3360`](../ui/dr-ui/src/develop.rs#L3360), [`ui/dr-ui/src/lib.rs:1366`](../ui/dr-ui/src/lib.rs#L1366), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:547`](../ui/dr-ui/src/settings_ui.rs#L547), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) |
|
||||||
| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:1437`](../ui/dr-ui/src/lib.rs#L1437), [`ui/dr-ui/src/lib.rs:2323`](../ui/dr-ui/src/lib.rs#L2323), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
||||||
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:681`](../core/dr-pipeline/src/graph.rs#L681), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:517`](../core/dr-pipeline/src/operation.rs#L517), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2127`](../ui/dr-ui/src/develop.rs#L2127), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/develop.rs:2230`](../ui/dr-ui/src/develop.rs#L2230), [`ui/dr-ui/src/develop.rs:2313`](../ui/dr-ui/src/develop.rs#L2313), [`ui/dr-ui/src/develop.rs:2327`](../ui/dr-ui/src/develop.rs#L2327), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2391`](../ui/dr-ui/src/lib.rs#L2391), [`ui/dr-ui/src/lib.rs:306`](../ui/dr-ui/src/lib.rs#L306), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:676`](../ui/dr-ui/ui/adjust.slint#L676), [`ui/dr-ui/ui/app.slint:1840`](../ui/dr-ui/ui/app.slint#L1840), [`ui/dr-ui/ui/app.slint:2357`](../ui/dr-ui/ui/app.slint#L2357), [`ui/dr-ui/ui/app.slint:2623`](../ui/dr-ui/ui/app.slint#L2623), [`ui/dr-ui/ui/app.slint:332`](../ui/dr-ui/ui/app.slint#L332), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) |
|
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:681`](../core/dr-pipeline/src/graph.rs#L681), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:517`](../core/dr-pipeline/src/operation.rs#L517), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2144`](../ui/dr-ui/src/develop.rs#L2144), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/develop.rs:2247`](../ui/dr-ui/src/develop.rs#L2247), [`ui/dr-ui/src/develop.rs:2330`](../ui/dr-ui/src/develop.rs#L2330), [`ui/dr-ui/src/develop.rs:2344`](../ui/dr-ui/src/develop.rs#L2344), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1458`](../ui/dr-ui/src/lib.rs#L1458), [`ui/dr-ui/src/lib.rs:2420`](../ui/dr-ui/src/lib.rs#L2420), [`ui/dr-ui/src/lib.rs:307`](../ui/dr-ui/src/lib.rs#L307), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:676`](../ui/dr-ui/ui/adjust.slint#L676), [`ui/dr-ui/ui/app.slint:1853`](../ui/dr-ui/ui/app.slint#L1853), [`ui/dr-ui/ui/app.slint:2370`](../ui/dr-ui/ui/app.slint#L2370), [`ui/dr-ui/ui/app.slint:2636`](../ui/dr-ui/ui/app.slint#L2636), [`ui/dr-ui/ui/app.slint:339`](../ui/dr-ui/ui/app.slint#L339), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) |
|
||||||
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:543`](../core/dr-pipeline/src/graph.rs#L543), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:654`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L654), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2627`](../ui/dr-ui/src/develop.rs#L2627), [`ui/dr-ui/src/develop.rs:3640`](../ui/dr-ui/src/develop.rs#L3640), [`ui/dr-ui/src/develop.rs:4123`](../ui/dr-ui/src/develop.rs#L4123), [`ui/dr-ui/src/develop.rs:4157`](../ui/dr-ui/src/develop.rs#L4157), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/lib.rs:736`](../ui/dr-ui/src/lib.rs#L736), [`ui/dr-ui/src/lib.rs:795`](../ui/dr-ui/src/lib.rs#L795) |
|
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:543`](../core/dr-pipeline/src/graph.rs#L543), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:654`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L654), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2668`](../ui/dr-ui/src/develop.rs#L2668), [`ui/dr-ui/src/develop.rs:3698`](../ui/dr-ui/src/develop.rs#L3698), [`ui/dr-ui/src/develop.rs:4181`](../ui/dr-ui/src/develop.rs#L4181), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/lib.rs:71`](../ui/dr-ui/src/lib.rs#L71), [`ui/dr-ui/src/lib.rs:737`](../ui/dr-ui/src/lib.rs#L737), [`ui/dr-ui/src/lib.rs:796`](../ui/dr-ui/src/lib.rs#L796) |
|
||||||
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) |
|
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5473`](../ui/dr-ui/src/develop.rs#L5473), [`ui/dr-ui/src/lib.rs:1414`](../ui/dr-ui/src/lib.rs#L1414), [`ui/dr-ui/src/lib.rs:2626`](../ui/dr-ui/src/lib.rs#L2626) |
|
||||||
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2693`](../ui/dr-ui/src/develop.rs#L2693), [`ui/dr-ui/src/develop.rs:5225`](../ui/dr-ui/src/develop.rs#L5225), [`ui/dr-ui/src/develop.rs:5257`](../ui/dr-ui/src/develop.rs#L5257), [`ui/dr-ui/src/develop.rs:621`](../ui/dr-ui/src/develop.rs#L621), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1485`](../ui/dr-ui/src/lib.rs#L1485), [`ui/dr-ui/src/lib.rs:296`](../ui/dr-ui/src/lib.rs#L296), [`ui/dr-ui/ui/app.slint:292`](../ui/dr-ui/ui/app.slint#L292), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
|
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2747`](../ui/dr-ui/src/develop.rs#L2747), [`ui/dr-ui/src/develop.rs:5283`](../ui/dr-ui/src/develop.rs#L5283), [`ui/dr-ui/src/develop.rs:5315`](../ui/dr-ui/src/develop.rs#L5315), [`ui/dr-ui/src/develop.rs:621`](../ui/dr-ui/src/develop.rs#L621), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1514`](../ui/dr-ui/src/lib.rs#L1514), [`ui/dr-ui/src/lib.rs:297`](../ui/dr-ui/src/lib.rs#L297), [`ui/dr-ui/ui/app.slint:299`](../ui/dr-ui/ui/app.slint#L299), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
|
||||||
|
| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:2644`](../ui/dr-ui/src/develop.rs#L2644), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5473`](../ui/dr-ui/src/develop.rs#L5473), [`ui/dr-ui/src/develop.rs:5519`](../ui/dr-ui/src/develop.rs#L5519), [`ui/dr-ui/src/develop.rs:5538`](../ui/dr-ui/src/develop.rs#L5538), [`ui/dr-ui/src/develop.rs:689`](../ui/dr-ui/src/develop.rs#L689), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1388`](../ui/dr-ui/src/lib.rs#L1388), [`ui/dr-ui/src/lib.rs:1414`](../ui/dr-ui/src/lib.rs#L1414), [`ui/dr-ui/src/lib.rs:2603`](../ui/dr-ui/src/lib.rs#L2603), [`ui/dr-ui/src/lib.rs:2626`](../ui/dr-ui/src/lib.rs#L2626), [`ui/dr-ui/ui/app.slint:1713`](../ui/dr-ui/ui/app.slint#L1713), [`ui/dr-ui/ui/app.slint:278`](../ui/dr-ui/ui/app.slint#L278), [`ui/dr-ui/ui/settings.slint:120`](../ui/dr-ui/ui/settings.slint#L120), [`ui/dr-ui/ui/settings.slint:730`](../ui/dr-ui/ui/settings.slint#L730) |
|
||||||
| FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
| FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||||
| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:533`](../core/dr-pipeline/src/graph.rs#L533), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:595`](../core/dr-types/src/settings.rs#L595), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:533`](../core/dr-pipeline/src/graph.rs#L533), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:595`](../core/dr-types/src/settings.rs#L595), [`ui/dr-ui/src/develop.rs:5538`](../ui/dr-ui/src/develop.rs#L5538), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||||
| FR-EXP-3 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
| FR-EXP-3 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||||
| FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
| FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||||
| FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
| FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
||||||
| FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:344`](../ui/dr-ui/src/lib.rs#L344), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:602`](../ui/dr-ui/src/settings_ui.rs#L602) |
|
| FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:345`](../ui/dr-ui/src/lib.rs#L345), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:602`](../ui/dr-ui/src/settings_ui.rs#L602) |
|
||||||
| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:186`](../ui/dr-ui/src/lib.rs#L186), [`ui/dr-ui/src/lib.rs:2040`](../ui/dr-ui/src/lib.rs#L2040), [`ui/dr-ui/src/lib.rs:344`](../ui/dr-ui/src/lib.rs#L344), [`ui/dr-ui/src/lib.rs:379`](../ui/dr-ui/src/lib.rs#L379), [`ui/dr-ui/src/lib.rs:406`](../ui/dr-ui/src/lib.rs#L406), [`ui/dr-ui/src/library_ui.rs:3362`](../ui/dr-ui/src/library_ui.rs#L3362), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6253`](../ui/dr-ui/src/library_ui.rs#L6253), [`ui/dr-ui/src/library_ui.rs:6330`](../ui/dr-ui/src/library_ui.rs#L6330), [`ui/dr-ui/src/library_ui.rs:6342`](../ui/dr-ui/src/library_ui.rs#L6342), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1120`](../ui/dr-ui/ui/app.slint#L1120), [`ui/dr-ui/ui/app.slint:1548`](../ui/dr-ui/ui/app.slint#L1548), [`ui/dr-ui/ui/library.slint:1323`](../ui/dr-ui/ui/library.slint#L1323), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
|
| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:187`](../ui/dr-ui/src/lib.rs#L187), [`ui/dr-ui/src/lib.rs:2069`](../ui/dr-ui/src/lib.rs#L2069), [`ui/dr-ui/src/lib.rs:345`](../ui/dr-ui/src/lib.rs#L345), [`ui/dr-ui/src/lib.rs:380`](../ui/dr-ui/src/lib.rs#L380), [`ui/dr-ui/src/lib.rs:407`](../ui/dr-ui/src/lib.rs#L407), [`ui/dr-ui/src/library_ui.rs:3362`](../ui/dr-ui/src/library_ui.rs#L3362), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6253`](../ui/dr-ui/src/library_ui.rs#L6253), [`ui/dr-ui/src/library_ui.rs:6330`](../ui/dr-ui/src/library_ui.rs#L6330), [`ui/dr-ui/src/library_ui.rs:6342`](../ui/dr-ui/src/library_ui.rs#L6342), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1127`](../ui/dr-ui/ui/app.slint#L1127), [`ui/dr-ui/ui/app.slint:1558`](../ui/dr-ui/ui/app.slint#L1558), [`ui/dr-ui/ui/library.slint:1323`](../ui/dr-ui/ui/library.slint#L1323), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
|
||||||
| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:652`](../core/dr-types/src/lib.rs#L652), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:779`](../ui/dr-ui/src/export.rs#L779), [`ui/dr-ui/src/export.rs:796`](../ui/dr-ui/src/export.rs#L796), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:652`](../core/dr-types/src/lib.rs#L652), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:779`](../ui/dr-ui/src/export.rs#L779), [`ui/dr-ui/src/export.rs:796`](../ui/dr-ui/src/export.rs#L796), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||||
| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2771`](../ui/dr-ui/src/develop.rs#L2771), [`ui/dr-ui/src/lib.rs:344`](../ui/dr-ui/src/lib.rs#L344) |
|
| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2829`](../ui/dr-ui/src/develop.rs#L2829), [`ui/dr-ui/src/lib.rs:345`](../ui/dr-ui/src/lib.rs#L345) |
|
||||||
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) |
|
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) |
|
||||||
| FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:406`](../ui/dr-ui/src/lib.rs#L406), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:953`](../ui/dr-ui/src/library.rs#L953), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:3362`](../ui/dr-ui/src/library_ui.rs#L3362), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
| FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:407`](../ui/dr-ui/src/lib.rs#L407), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:953`](../ui/dr-ui/src/library.rs#L953), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:3362`](../ui/dr-ui/src/library_ui.rs#L3362), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||||
| FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:34`](../core/dr-sync-nextcloud/src/lib.rs#L34), [`core/dr-sync-nextcloud/src/lib.rs:892`](../core/dr-sync-nextcloud/src/lib.rs#L892), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/lib.rs:40`](../core/dr-sync/src/lib.rs#L40), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1) |
|
| FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:34`](../core/dr-sync-nextcloud/src/lib.rs#L34), [`core/dr-sync-nextcloud/src/lib.rs:892`](../core/dr-sync-nextcloud/src/lib.rs#L892), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/lib.rs:40`](../core/dr-sync/src/lib.rs#L40), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1) |
|
||||||
| FR-NC-2 | [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`core/dr-sync-nextcloud/src/session.rs:34`](../core/dr-sync-nextcloud/src/session.rs#L34) |
|
| FR-NC-2 | [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`core/dr-sync-nextcloud/src/session.rs:34`](../core/dr-sync-nextcloud/src/session.rs#L34), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) |
|
||||||
| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2604`](../ui/dr-ui/src/library.rs#L2604), [`ui/dr-ui/src/library.rs:2630`](../ui/dr-ui/src/library.rs#L2630), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3624`](../ui/dr-ui/src/library_ui.rs#L3624), [`ui/dr-ui/src/library_ui.rs:4590`](../ui/dr-ui/src/library_ui.rs#L4590), [`ui/dr-ui/ui/app.slint:540`](../ui/dr-ui/ui/app.slint#L540), [`ui/dr-ui/ui/settings.slint:353`](../ui/dr-ui/ui/settings.slint#L353), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2604`](../ui/dr-ui/src/library.rs#L2604), [`ui/dr-ui/src/library.rs:2630`](../ui/dr-ui/src/library.rs#L2630), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3624`](../ui/dr-ui/src/library_ui.rs#L3624), [`ui/dr-ui/src/library_ui.rs:4590`](../ui/dr-ui/src/library_ui.rs#L4590), [`ui/dr-ui/ui/app.slint:547`](../ui/dr-ui/ui/app.slint#L547), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
||||||
| 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-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-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-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:555`](../core/dr-catalog/src/schema.rs#L555), [`core/dr-catalog/src/schema.rs:956`](../core/dr-catalog/src/schema.rs#L956), [`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:1769`](../ui/dr-ui/src/lib.rs#L1769), [`ui/dr-ui/src/lib.rs:2647`](../ui/dr-ui/src/lib.rs#L2647), [`ui/dr-ui/src/library.rs:1330`](../ui/dr-ui/src/library.rs#L1330), [`ui/dr-ui/src/library.rs:1353`](../ui/dr-ui/src/library.rs#L1353), [`ui/dr-ui/src/library.rs:1511`](../ui/dr-ui/src/library.rs#L1511), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5255`](../ui/dr-ui/src/library_ui.rs#L5255), [`ui/dr-ui/src/library_ui.rs:5273`](../ui/dr-ui/src/library_ui.rs#L5273), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`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:2796`](../ui/dr-ui/ui/app.slint#L2796), [`ui/dr-ui/ui/app.slint:647`](../ui/dr-ui/ui/app.slint#L647), [`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:974`](../ui/dr-ui/ui/library.slint#L974) |
|
| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:555`](../core/dr-catalog/src/schema.rs#L555), [`core/dr-catalog/src/schema.rs:956`](../core/dr-catalog/src/schema.rs#L956), [`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:1798`](../ui/dr-ui/src/lib.rs#L1798), [`ui/dr-ui/src/lib.rs:2689`](../ui/dr-ui/src/lib.rs#L2689), [`ui/dr-ui/src/library.rs:1330`](../ui/dr-ui/src/library.rs#L1330), [`ui/dr-ui/src/library.rs:1353`](../ui/dr-ui/src/library.rs#L1353), [`ui/dr-ui/src/library.rs:1511`](../ui/dr-ui/src/library.rs#L1511), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5255`](../ui/dr-ui/src/library_ui.rs#L5255), [`ui/dr-ui/src/library_ui.rs:5273`](../ui/dr-ui/src/library_ui.rs#L5273), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`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:2809`](../ui/dr-ui/ui/app.slint#L2809), [`ui/dr-ui/ui/app.slint:654`](../ui/dr-ui/ui/app.slint#L654), [`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:974`](../ui/dr-ui/ui/library.slint#L974) |
|
||||||
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) |
|
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) |
|
||||||
| 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:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`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:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`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-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`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:2630`](../ui/dr-ui/src/library.rs#L2630), [`ui/dr-ui/src/library_ui.rs:3624`](../ui/dr-ui/src/library_ui.rs#L3624), [`ui/dr-ui/ui/settings.slint:353`](../ui/dr-ui/ui/settings.slint#L353) |
|
| FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`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:2630`](../ui/dr-ui/src/library.rs#L2630), [`ui/dr-ui/src/library_ui.rs:3624`](../ui/dr-ui/src/library_ui.rs#L3624), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372) |
|
||||||
| 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:1104`](../ui/dr-ui/src/lib.rs#L1104), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
| 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:1105`](../ui/dr-ui/src/lib.rs#L1105), [`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:1104`](../ui/dr-ui/src/lib.rs#L1104) |
|
| 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:1105`](../ui/dr-ui/src/lib.rs#L1105) |
|
||||||
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) |
|
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1767`](../ui/dr-ui/src/lib.rs#L1767), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) |
|
||||||
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) |
|
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) |
|
||||||
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) |
|
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
||||||
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
||||||
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:845`](../ui/dr-ui/src/lib.rs#L845), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:846`](../ui/dr-ui/src/lib.rs#L846), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
||||||
|
| FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1) |
|
||||||
| FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:129`](../core/dr-types/src/lib.rs#L129), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200) |
|
| FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:129`](../core/dr-types/src/lib.rs#L129), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200) |
|
||||||
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) |
|
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) |
|
||||||
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:186`](../ui/dr-ui/src/lib.rs#L186) |
|
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:187`](../ui/dr-ui/src/lib.rs#L187) |
|
||||||
| 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-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:2729`](../ui/dr-ui/src/lib.rs#L2729), [`ui/dr-ui/src/lib.rs:78`](../ui/dr-ui/src/lib.rs#L78), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1040`](../ui/dr-ui/ui/library.slint#L1040) |
|
| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2771`](../ui/dr-ui/src/lib.rs#L2771), [`ui/dr-ui/src/lib.rs:79`](../ui/dr-ui/src/lib.rs#L79), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1040`](../ui/dr-ui/ui/library.slint#L1040) |
|
||||||
| 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:78`](../ui/dr-ui/src/lib.rs#L78), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5140`](../ui/dr-ui/src/library_ui.rs#L5140), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/src/library_ui.rs:6361`](../ui/dr-ui/src/library_ui.rs#L6361), [`ui/dr-ui/ui/app.slint:2463`](../ui/dr-ui/ui/app.slint#L2463), [`ui/dr-ui/ui/app.slint:278`](../ui/dr-ui/ui/app.slint#L278), [`ui/dr-ui/ui/app.slint:677`](../ui/dr-ui/ui/app.slint#L677), [`ui/dr-ui/ui/app.slint:684`](../ui/dr-ui/ui/app.slint#L684), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/settings.slint:106`](../ui/dr-ui/ui/settings.slint#L106) |
|
| 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:79`](../ui/dr-ui/src/lib.rs#L79), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5140`](../ui/dr-ui/src/library_ui.rs#L5140), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/src/library_ui.rs:6361`](../ui/dr-ui/src/library_ui.rs#L6361), [`ui/dr-ui/ui/app.slint:2476`](../ui/dr-ui/ui/app.slint#L2476), [`ui/dr-ui/ui/app.slint:285`](../ui/dr-ui/ui/app.slint#L285), [`ui/dr-ui/ui/app.slint:684`](../ui/dr-ui/ui/app.slint#L684), [`ui/dr-ui/ui/app.slint:691`](../ui/dr-ui/ui/app.slint#L691), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/settings.slint:106`](../ui/dr-ui/ui/settings.slint#L106) |
|
||||||
| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/library_ui.rs:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`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/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2105`](../ui/dr-ui/ui/app.slint#L2105), [`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-3 | [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/library_ui.rs:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`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/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2118`](../ui/dr-ui/ui/app.slint#L2118), [`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: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:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`ui/dr-ui/src/library_ui.rs:4430`](../ui/dr-ui/src/library_ui.rs#L4430), [`ui/dr-ui/src/library_ui.rs:4533`](../ui/dr-ui/src/library_ui.rs#L4533), [`ui/dr-ui/src/library_ui.rs:4561`](../ui/dr-ui/src/library_ui.rs#L4561), [`ui/dr-ui/src/library_ui.rs:5273`](../ui/dr-ui/src/library_ui.rs#L5273), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/ui/app.slint:1777`](../ui/dr-ui/ui/app.slint#L1777), [`ui/dr-ui/ui/app.slint:653`](../ui/dr-ui/ui/app.slint#L653), [`ui/dr-ui/ui/app.slint:684`](../ui/dr-ui/ui/app.slint#L684), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
| 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:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`ui/dr-ui/src/library_ui.rs:4430`](../ui/dr-ui/src/library_ui.rs#L4430), [`ui/dr-ui/src/library_ui.rs:4533`](../ui/dr-ui/src/library_ui.rs#L4533), [`ui/dr-ui/src/library_ui.rs:4561`](../ui/dr-ui/src/library_ui.rs#L4561), [`ui/dr-ui/src/library_ui.rs:5273`](../ui/dr-ui/src/library_ui.rs#L5273), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/ui/app.slint:1790`](../ui/dr-ui/ui/app.slint#L1790), [`ui/dr-ui/ui/app.slint:660`](../ui/dr-ui/ui/app.slint#L660), [`ui/dr-ui/ui/app.slint:691`](../ui/dr-ui/ui/app.slint#L691), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
||||||
| 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:2356`](../ui/dr-ui/src/lib.rs#L2356), [`ui/dr-ui/src/lib.rs:2763`](../ui/dr-ui/src/lib.rs#L2763), [`ui/dr-ui/src/lib.rs:2948`](../ui/dr-ui/src/lib.rs#L2948), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:313`](../ui/dr-ui/ui/app.slint#L313), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) |
|
| 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:2385`](../ui/dr-ui/src/lib.rs#L2385), [`ui/dr-ui/src/lib.rs:2805`](../ui/dr-ui/src/lib.rs#L2805), [`ui/dr-ui/src/lib.rs:2990`](../ui/dr-ui/src/lib.rs#L2990), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:320`](../ui/dr-ui/ui/app.slint#L320), [`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) |
|
| 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), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) |
|
| NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) |
|
||||||
| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:2074`](../ui/dr-ui/src/lib.rs#L2074), [`ui/dr-ui/ui/app.slint:1131`](../ui/dr-ui/ui/app.slint#L1131), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
|
| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:2103`](../ui/dr-ui/src/lib.rs#L2103), [`ui/dr-ui/ui/app.slint:1138`](../ui/dr-ui/ui/app.slint#L1138), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
|
||||||
| NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) |
|
| NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) |
|
||||||
| NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) |
|
| NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) |
|
||||||
| NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
| NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
||||||
| NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
| NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
||||||
| NFR-P5 | [`core/dr-catalog/src/schema.rs:304`](../core/dr-catalog/src/schema.rs#L304), [`ui/dr-ui/src/library.rs:3229`](../ui/dr-ui/src/library.rs#L3229), [`ui/dr-ui/src/library.rs:4002`](../ui/dr-ui/src/library.rs#L4002), [`ui/dr-ui/src/library_ui.rs:4070`](../ui/dr-ui/src/library_ui.rs#L4070), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) |
|
| NFR-P5 | [`core/dr-catalog/src/schema.rs:304`](../core/dr-catalog/src/schema.rs#L304), [`ui/dr-ui/src/library.rs:3229`](../ui/dr-ui/src/library.rs#L3229), [`ui/dr-ui/src/library.rs:4002`](../ui/dr-ui/src/library.rs#L4002), [`ui/dr-ui/src/library_ui.rs:4070`](../ui/dr-ui/src/library_ui.rs#L4070), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) |
|
||||||
| NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`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), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) |
|
| NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:944`](../ui/dr-ui/src/export.rs#L944), [`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), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) |
|
||||||
| NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302) |
|
| NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
||||||
|
| NFR-PORT-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1) |
|
||||||
| NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`ui/dr-ui/src/library.rs:953`](../ui/dr-ui/src/library.rs#L953) |
|
| NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`ui/dr-ui/src/library.rs:953`](../ui/dr-ui/src/library.rs#L953) |
|
||||||
| NFR-R2 | [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1) |
|
| NFR-R2 | [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1) |
|
||||||
| NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) |
|
| NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) |
|
||||||
| NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
| NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
||||||
| NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
| NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
||||||
| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70) |
|
| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:71`](../ui/dr-ui/src/lib.rs#L71) |
|
||||||
| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:555`](../core/dr-catalog/src/schema.rs#L555), [`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), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2604`](../ui/dr-ui/src/library.rs#L2604) |
|
| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:555`](../core/dr-catalog/src/schema.rs#L555), [`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), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2604`](../ui/dr-ui/src/library.rs#L2604) |
|
||||||
| NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) |
|
| NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) |
|
||||||
|
| NFR-SEC-2 | [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) |
|
||||||
| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) |
|
| R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) |
|
||||||
| R4 | [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
| R4 | [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
||||||
|
|
||||||
## Not yet tagged
|
## Not yet tagged
|
||||||
|
|
||||||
79 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
75 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
||||||
|
|
||||||
<details><summary>Show untagged requirements</summary>
|
<details><summary>Show untagged requirements</summary>
|
||||||
|
|
||||||
@@ -149,13 +153,11 @@ _None._
|
|||||||
- FR-DSP-3
|
- FR-DSP-3
|
||||||
- FR-DSP-4
|
- FR-DSP-4
|
||||||
- FR-DSP-5
|
- FR-DSP-5
|
||||||
- FR-DSP-8
|
|
||||||
- FR-NC-11
|
- FR-NC-11
|
||||||
- FR-PLAT-AND-2
|
- FR-PLAT-AND-2
|
||||||
- FR-PLAT-AND-4
|
- FR-PLAT-AND-4
|
||||||
- FR-PLAT-AND-5
|
- FR-PLAT-AND-5
|
||||||
- FR-PLAT-AND-6
|
- FR-PLAT-AND-6
|
||||||
- FR-PLAT-LIN-2
|
|
||||||
- FR-PLAT-LIN-3
|
- FR-PLAT-LIN-3
|
||||||
- FR-PLG-1
|
- FR-PLG-1
|
||||||
- FR-PLG-10
|
- FR-PLG-10
|
||||||
@@ -203,13 +205,11 @@ _None._
|
|||||||
- NFR-P7
|
- NFR-P7
|
||||||
- NFR-P8
|
- NFR-P8
|
||||||
- NFR-PORT-2
|
- NFR-PORT-2
|
||||||
- NFR-PORT-3
|
|
||||||
- NFR-R3
|
- NFR-R3
|
||||||
- NFR-R4
|
- NFR-R4
|
||||||
- NFR-R6
|
- NFR-R6
|
||||||
- NFR-RES-2
|
- NFR-RES-2
|
||||||
- NFR-RES-3
|
- NFR-RES-3
|
||||||
- NFR-SEC-2
|
|
||||||
- NFR-SEC-3
|
- NFR-SEC-3
|
||||||
- NFR-SEC-4
|
- NFR-SEC-4
|
||||||
- NFR-SEC-6
|
- NFR-SEC-6
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ log.workspace = true
|
|||||||
|
|
||||||
[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
|
[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
|
||||||
keyring.workspace = true
|
keyring.workspace = true
|
||||||
|
# The two display servers FR-PLAT-LIN-2 names, each asked for the profile it
|
||||||
|
# knows how to state (FR-DSP-8, `display.rs`). Both are already in the tree
|
||||||
|
# via winit, so this adds compilation of nothing that was not already built.
|
||||||
|
x11rb.workspace = true
|
||||||
|
wayland-client.workspace = true
|
||||||
|
wayland-protocols.workspace = true
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android-native-keyring-store.workspace = true
|
android-native-keyring-store.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//! What this machine's display server will say about its monitors.
|
||||||
|
//!
|
||||||
|
//! The same probe the application runs at startup, printed instead of used.
|
||||||
|
//! It exists for the reason `keyring_check` does: the answer depends entirely
|
||||||
|
//! on the desktop the code is running on, so the only way to know which
|
||||||
|
//! acquisition path a given machine takes is to ask it there.
|
||||||
|
//!
|
||||||
|
//! cargo run -p dr-plat --example display_probe
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let survey = dr_plat::DisplaySurvey::probe();
|
||||||
|
println!("display server: {}", survey.server.label());
|
||||||
|
println!();
|
||||||
|
for (index, display) in survey.displays.iter().enumerate() {
|
||||||
|
println!("[{index}] {}", display.name);
|
||||||
|
println!(" space: {}", display.profile.space.label());
|
||||||
|
println!(" reading: {}", display.profile.describe());
|
||||||
|
println!(
|
||||||
|
" source: {:?} measured: {} approximated: {}",
|
||||||
|
display.profile.source,
|
||||||
|
display.profile.source.is_measured(),
|
||||||
|
display.profile.approximated
|
||||||
|
);
|
||||||
|
match display.bounds {
|
||||||
|
Some(b) => println!(
|
||||||
|
" bounds: {}×{} at ({}, {})",
|
||||||
|
b.width, b.height, b.x, b.y
|
||||||
|
),
|
||||||
|
// Expected on Wayland; see `DisplaySurvey::containing`.
|
||||||
|
None => println!(" bounds: not stated by this display server"),
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
//! TRACES: FR-DSP-8 | FR-PLAT-LIN-2 | NFR-PORT-1
|
||||||
|
//! What colour the screen in front of the photographer actually is.
|
||||||
|
//!
|
||||||
|
//! The display path is colour-managed by encoding the render into the output
|
||||||
|
//! device's space (FR-DSP-6); that space is a parameter of composition, so
|
||||||
|
//! getting it right is a question of *asking the platform* rather than of
|
||||||
|
//! doing anything to the pipeline. This module is the asking.
|
||||||
|
//!
|
||||||
|
//! # Why this is a correctness problem and not a polish one
|
||||||
|
//!
|
||||||
|
//! FR-DSP-8 says so itself: "on a multi-monitor desktop with differing
|
||||||
|
//! profiles, showing wrong colours on the second display is a correctness
|
||||||
|
//! defect, not a polish item". Every other display requirement fails by being
|
||||||
|
//! slow. This one fails by being *quietly wrong* — a photographer grades a
|
||||||
|
//! skin tone on a wide-gamut panel, the app encodes sRGB into it, and the
|
||||||
|
//! result is oversaturated on screen and correct in the file, or the reverse.
|
||||||
|
//! Nothing about the image looks broken. That is the whole danger.
|
||||||
|
//!
|
||||||
|
//! # Acquisition is stated per display server, because it differs entirely
|
||||||
|
//!
|
||||||
|
//! - **X11** publishes ICC profiles as root-window properties: `_ICC_PROFILE`
|
||||||
|
//! for the first output and `_ICC_PROFILE_<n>` for the rest, a convention
|
||||||
|
//! from the ICC Profiles in X specification that colord, xiccd and
|
||||||
|
//! `xcalib` all write to. The bytes are a whole ICC profile.
|
||||||
|
//! - **Wayland** has no such back door — a client cannot read another
|
||||||
|
//! client's or the compositor's state — and the protocol that replaces it,
|
||||||
|
//! `wp_color_manager_v1`, is still *staging* upstream. Where the compositor
|
||||||
|
//! advertises it we ask it; where it does not, there is no mechanism at all
|
||||||
|
//! and we say so.
|
||||||
|
//!
|
||||||
|
//! FR-DSP-8 anticipates exactly that gap and demands "a defined fallback where
|
||||||
|
//! Wayland provides no profile". The fallback is sRGB, and the requirement's
|
||||||
|
//! wording — *defined*, not *silent* — is why [`ProfileSource`] carries the
|
||||||
|
//! reason as data rather than logging it: the About page renders it, so a
|
||||||
|
//! photographer being shown sRGB because the compositor would not say
|
||||||
|
//! otherwise can find that out rather than wonder.
|
||||||
|
//!
|
||||||
|
//! # Four spaces, and no ICC engine
|
||||||
|
//!
|
||||||
|
//! The pipeline can encode into four spaces ([`ColourSpace`]). A real display
|
||||||
|
//! profile is a measurement of one panel and is none of them. Rather than
|
||||||
|
//! grow a general ICC transform engine — a much larger piece of work, with a
|
||||||
|
//! CMM, rendering intents, LUT-based profiles and a per-frame cost to argue
|
||||||
|
//! about — this reduces the profile to its primaries and picks the nearest of
|
||||||
|
//! the four, and then *says that it did*. See [`nearest_space`].
|
||||||
|
//!
|
||||||
|
//! An approximation the photographer can see beats an exact answer they
|
||||||
|
//! cannot, and beats a silent approximation by a much larger margin.
|
||||||
|
|
||||||
|
use dr_types::colour::Chromaticities;
|
||||||
|
use dr_types::ColourSpace;
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(target_os = "android")))]
|
||||||
|
mod icc;
|
||||||
|
#[cfg(all(unix, not(target_os = "android")))]
|
||||||
|
mod wayland;
|
||||||
|
#[cfg(all(unix, not(target_os = "android")))]
|
||||||
|
mod x11;
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(target_os = "android")))]
|
||||||
|
pub use icc::{read_profile, IccSummary};
|
||||||
|
|
||||||
|
/// Which display server the session is running on.
|
||||||
|
///
|
||||||
|
/// Decided from the environment rather than by trying to connect, because the
|
||||||
|
/// answer decides *which* connection to attempt: `DISPLAY` is set inside a
|
||||||
|
/// Wayland session too (Xwayland sets it), so probing X11 first would put
|
||||||
|
/// every GNOME and KDE user on the X11 path and read an Xwayland root window
|
||||||
|
/// that no colour manager necessarily writes to.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DisplayServer {
|
||||||
|
Wayland,
|
||||||
|
X11,
|
||||||
|
/// Neither — a headless test runner, Android, or a build with no display
|
||||||
|
/// integration. Not an error: it is the case the fallback exists for.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayServer {
|
||||||
|
/// What the session says it is.
|
||||||
|
pub fn detect() -> Self {
|
||||||
|
// `WAYLAND_DISPLAY` first and unconditionally. See the type comment:
|
||||||
|
// both variables are set in a Wayland session, and only this order
|
||||||
|
// gets such a session onto the path its compositor actually owns.
|
||||||
|
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
||||||
|
Self::Wayland
|
||||||
|
} else if std::env::var_os("DISPLAY").is_some() {
|
||||||
|
Self::X11
|
||||||
|
} else {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Wayland => "Wayland",
|
||||||
|
Self::X11 => "X11",
|
||||||
|
Self::None => "no display server",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why a display ended up on the sRGB fallback.
|
||||||
|
///
|
||||||
|
/// Carried as data because the About page renders it. FR-DSP-8 asks for a
|
||||||
|
/// *defined* fallback, and a fallback whose reason cannot be shown is
|
||||||
|
/// indistinguishable from a display that genuinely is sRGB.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum FallbackReason {
|
||||||
|
/// Wayland, and the compositor does not advertise `wp_color_manager_v1`.
|
||||||
|
/// The common case today and the one FR-DSP-8's wording exists for.
|
||||||
|
NoWaylandProtocol,
|
||||||
|
/// The compositor or X server was asked and had nothing to say about this
|
||||||
|
/// display — no `_ICC_PROFILE` atom, or an image description that failed.
|
||||||
|
NoProfileForDisplay,
|
||||||
|
/// Something was found and could not be read. Distinguished from "nothing
|
||||||
|
/// was there" because it is a bug report rather than a configuration.
|
||||||
|
Unreadable(String),
|
||||||
|
/// The connection itself failed, or there is no display server.
|
||||||
|
NoConnection(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FallbackReason {
|
||||||
|
/// A phrase for the About page, reading after "sRGB assumed:".
|
||||||
|
pub fn describe(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::NoWaylandProtocol => {
|
||||||
|
"the compositor does not offer colour management".to_string()
|
||||||
|
}
|
||||||
|
Self::NoProfileForDisplay => "no profile is assigned to this display".to_string(),
|
||||||
|
Self::Unreadable(why) => format!("the profile could not be read ({why})"),
|
||||||
|
Self::NoConnection(why) => format!("no display server ({why})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How a display's colour was established.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ProfileSource {
|
||||||
|
/// An ICC profile read from an X11 root-window property, named here so a
|
||||||
|
/// bug report can quote which one.
|
||||||
|
X11RootProperty(String),
|
||||||
|
/// The compositor's own answer, over `wp_color_manager_v1`.
|
||||||
|
WaylandColourManagement,
|
||||||
|
/// FR-DSP-8's stated fallback.
|
||||||
|
FallbackSrgb(FallbackReason),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileSource {
|
||||||
|
/// Whether this is a profile the platform stated, rather than an assumption.
|
||||||
|
pub fn is_measured(&self) -> bool {
|
||||||
|
!matches!(self, Self::FallbackSrgb(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A short phrase naming the acquisition path, for the About page.
|
||||||
|
pub fn label(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::X11RootProperty(atom) => format!("X11 {atom}"),
|
||||||
|
Self::WaylandColourManagement => "Wayland colour management".to_string(),
|
||||||
|
Self::FallbackSrgb(_) => "fallback".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The colour of one display, reduced to something the pipeline can encode.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DisplayProfile {
|
||||||
|
/// The space to compose the canvas into while this display is showing it.
|
||||||
|
pub space: ColourSpace,
|
||||||
|
/// Where `space` came from.
|
||||||
|
pub source: ProfileSource,
|
||||||
|
/// The profile's own name, where it carried one. A photographer who
|
||||||
|
/// calibrated this panel recognises the string their calibrator wrote,
|
||||||
|
/// which is the quickest way to confirm we are reading the right profile.
|
||||||
|
pub described_as: Option<String>,
|
||||||
|
/// Whether `space` is the *nearest* of the four rather than a match.
|
||||||
|
///
|
||||||
|
/// True for essentially every measured panel, and that is the honest
|
||||||
|
/// answer rather than a defect: a calibration profile describes one piece
|
||||||
|
/// of glass and no standard space describes it exactly.
|
||||||
|
pub approximated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayProfile {
|
||||||
|
/// The sRGB fallback FR-DSP-8 defines, carrying why it was reached.
|
||||||
|
pub fn fallback(reason: FallbackReason) -> Self {
|
||||||
|
Self {
|
||||||
|
space: ColourSpace::Srgb,
|
||||||
|
source: ProfileSource::FallbackSrgb(reason),
|
||||||
|
described_as: None,
|
||||||
|
// Not an approximation of anything. sRGB is being *assumed*, which
|
||||||
|
// is a different claim and reads differently on the About page.
|
||||||
|
approximated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One line for the About page: the space, then how we know.
|
||||||
|
///
|
||||||
|
/// Written here rather than in the interface because the three cases —
|
||||||
|
/// measured and exact, measured and approximated, assumed — differ in
|
||||||
|
/// what they are claiming, and a format string assembled in Slint would
|
||||||
|
/// lose that distinction the first time someone edited it.
|
||||||
|
pub fn describe(&self) -> String {
|
||||||
|
let space = self.space.label();
|
||||||
|
match &self.source {
|
||||||
|
ProfileSource::FallbackSrgb(reason) => {
|
||||||
|
format!("{space} assumed — {}", reason.describe())
|
||||||
|
}
|
||||||
|
source if self.approximated => {
|
||||||
|
let named = self
|
||||||
|
.described_as
|
||||||
|
.as_deref()
|
||||||
|
.map_or_else(String::new, |d| format!("{d}, "));
|
||||||
|
format!("nearest to {space} ({named}via {})", source.label())
|
||||||
|
}
|
||||||
|
source => format!("{space} (via {})", source.label()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A display, as the platform describes it.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct DisplayInfo {
|
||||||
|
/// The connector or output name — "DP-1", "eDP-1" — where the server gave
|
||||||
|
/// one, otherwise an index. Shown in About so a two-monitor user can tell
|
||||||
|
/// which row is which.
|
||||||
|
pub name: String,
|
||||||
|
/// Where this display sits in the desktop's coordinate space, if the
|
||||||
|
/// server said. `None` on Wayland, which deliberately does not tell a
|
||||||
|
/// client where anything is; see [`DisplaySurvey::containing`].
|
||||||
|
pub bounds: Option<Bounds>,
|
||||||
|
pub profile: DisplayProfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A display's rectangle in the display server's own screen coordinates.
|
||||||
|
///
|
||||||
|
/// Physical device pixels on X11, which is what RandR states a CRTC's origin
|
||||||
|
/// and size in — and, not by coincidence, the same space Slint reports a
|
||||||
|
/// window's position in. The two are comparable without conversion, and a
|
||||||
|
/// conversion inserted "for tidiness" is how they would stop being.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Bounds {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounds {
|
||||||
|
pub fn contains(&self, x: i32, y: i32) -> bool {
|
||||||
|
x >= self.x
|
||||||
|
&& y >= self.y
|
||||||
|
&& x < self.x.saturating_add_unsigned(self.width)
|
||||||
|
&& y < self.y.saturating_add_unsigned(self.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every display the session has, and how each was asked.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct DisplaySurvey {
|
||||||
|
pub server: DisplayServer,
|
||||||
|
pub displays: Vec<DisplayInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplaySurvey {
|
||||||
|
/// Ask the platform. Never fails: a failure *is* the fallback.
|
||||||
|
///
|
||||||
|
/// Cheap enough to repeat — an X11 property fetch or a Wayland roundtrip —
|
||||||
|
/// but not free, so callers hold the result and re-survey on a display
|
||||||
|
/// change rather than per frame.
|
||||||
|
pub fn probe() -> Self {
|
||||||
|
let server = DisplayServer::detect();
|
||||||
|
#[cfg(all(unix, not(target_os = "android")))]
|
||||||
|
let displays = match server {
|
||||||
|
DisplayServer::Wayland => wayland::probe(),
|
||||||
|
DisplayServer::X11 => x11::probe(),
|
||||||
|
DisplayServer::None => Err(FallbackReason::NoConnection(
|
||||||
|
"neither WAYLAND_DISPLAY nor DISPLAY is set".to_string(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
// Android draws through the platform compositor and has its own
|
||||||
|
// wide-gamut path (FR-DSP-6); neither display server exists there, so
|
||||||
|
// the crates are not even compiled in. sRGB, stated as such.
|
||||||
|
#[cfg(not(all(unix, not(target_os = "android"))))]
|
||||||
|
let displays: Result<Vec<DisplayInfo>, FallbackReason> = Err(FallbackReason::NoConnection(
|
||||||
|
"no display-server integration on this platform".to_string(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let displays = match displays {
|
||||||
|
// A server that connected but listed nothing is the same situation
|
||||||
|
// as one that would not connect: there is no display to describe,
|
||||||
|
// and the canvas still has to be composed into *something*.
|
||||||
|
Ok(found) if !found.is_empty() => found,
|
||||||
|
Ok(_) => vec![Self::assumed(FallbackReason::NoProfileForDisplay)],
|
||||||
|
Err(reason) => vec![Self::assumed(reason)],
|
||||||
|
};
|
||||||
|
Self { server, displays }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assumed(reason: FallbackReason) -> DisplayInfo {
|
||||||
|
DisplayInfo {
|
||||||
|
name: "display".to_string(),
|
||||||
|
bounds: None,
|
||||||
|
profile: DisplayProfile::fallback(reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A survey with no platform in it, for tests and for the headless case.
|
||||||
|
pub fn assumed_srgb(reason: FallbackReason) -> Self {
|
||||||
|
Self {
|
||||||
|
server: DisplayServer::None,
|
||||||
|
displays: vec![Self::assumed(reason)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which display is showing a window at `(x, y)` in the display server's
|
||||||
|
/// screen coordinates, as an index into [`Self::displays`].
|
||||||
|
///
|
||||||
|
/// **The point is the window's centre, and the caller computes it.** A
|
||||||
|
/// window straddling two monitors is showing more of itself on one of
|
||||||
|
/// them, and its centre is the cheapest statement of which.
|
||||||
|
///
|
||||||
|
/// # Wayland returns 0, and that is the protocol's answer, not a bug
|
||||||
|
///
|
||||||
|
/// A Wayland client is not told where its window is — there is no
|
||||||
|
/// equivalent of `XGetGeometry` against the root, deliberately, and
|
||||||
|
/// `xdg_toplevel` carries no position. The protocol's own answer to "which
|
||||||
|
/// output am I on" is `wl_surface.enter`, or better,
|
||||||
|
/// `wp_color_management_surface_feedback_v1`, which hands the client the
|
||||||
|
/// preferred image description for *its surface* and re-sends it when the
|
||||||
|
/// window moves. Both require the application's `wl_surface`, which Slint
|
||||||
|
/// owns and does not expose, so this falls back to the first display and
|
||||||
|
/// the About page shows every display's profile rather than one.
|
||||||
|
///
|
||||||
|
/// The consequence is worth stating plainly: on a multi-monitor Wayland
|
||||||
|
/// desktop the canvas is composed for the *first* output. That is a
|
||||||
|
/// smaller error than composing for the wrong space entirely — the
|
||||||
|
/// profiles are read correctly, and a single-monitor session, which is
|
||||||
|
/// most of them, is exactly right — and the fix is a Slint surface handle
|
||||||
|
/// rather than anything in this module.
|
||||||
|
pub fn containing(&self, x: i32, y: i32) -> usize {
|
||||||
|
self.displays
|
||||||
|
.iter()
|
||||||
|
.position(|d| d.bounds.is_some_and(|b| b.contains(x, y)))
|
||||||
|
// Off every known rectangle: a window dragged past the edge of the
|
||||||
|
// desktop, or a server that gave no geometry. The first display is
|
||||||
|
// the primary one on both servers we speak to.
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The profile for a display index, tolerating an index that has gone
|
||||||
|
/// stale because a monitor was unplugged between survey and render.
|
||||||
|
pub fn profile(&self, index: usize) -> &DisplayProfile {
|
||||||
|
self.displays
|
||||||
|
.get(index)
|
||||||
|
.or_else(|| self.displays.first())
|
||||||
|
.map(|d| &d.profile)
|
||||||
|
.expect("probe always yields at least one display")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The nearest of the four spaces the pipeline can encode, and whether it is
|
||||||
|
/// near enough to call a match.
|
||||||
|
///
|
||||||
|
/// # Compared as colorants, not as chromaticity pairs
|
||||||
|
///
|
||||||
|
/// The obvious comparison — eight xy numbers against eight xy numbers — is
|
||||||
|
/// wrong, and wrong in a way that would pass a casual test. A profile's
|
||||||
|
/// colorants have already been chromatically adapted to the D50 connection
|
||||||
|
/// space; a colour space's *published* chromaticities have not. Comparing the
|
||||||
|
/// two directly makes every D65 space look like it has the wrong white point
|
||||||
|
/// and pushes matches towards ProPhoto, the only D50 member of the four.
|
||||||
|
///
|
||||||
|
/// So both sides go through [`Chromaticities::to_pcs_xyz`] and are compared as
|
||||||
|
/// the nine numbers a matrix/TRC profile would carry. That is the same
|
||||||
|
/// reduction `dr-export` writes into a file, which means a profile this
|
||||||
|
/// pipeline produced round-trips back to the space it was produced from — the
|
||||||
|
/// test at the bottom of this module.
|
||||||
|
///
|
||||||
|
/// # Primaries and not the tone curve
|
||||||
|
///
|
||||||
|
/// The four spaces are distinguished by gamut first: sRGB and Display P3
|
||||||
|
/// share a transfer function entirely. A measured panel's TRC is a tabulated
|
||||||
|
/// curve fitted to a target gamma and matching it against three analytic
|
||||||
|
/// curves would decide nothing the primaries have not already decided. The
|
||||||
|
/// transfer function comes along with whichever space the primaries choose,
|
||||||
|
/// which is the right pairing — encoding P3 primaries through an Adobe RGB
|
||||||
|
/// gamma would be a space nobody has.
|
||||||
|
pub fn nearest_space(measured: &Chromaticities) -> (ColourSpace, bool) {
|
||||||
|
nearest_by_colorants(&measured.to_pcs_xyz())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`nearest_space`], entered from the nine numbers directly.
|
||||||
|
///
|
||||||
|
/// The two acquisition paths arrive with different halves of the same thing:
|
||||||
|
/// an ICC profile carries D50-adapted colorants and nothing else, while
|
||||||
|
/// Wayland's `primaries` event carries xy chromaticities and no matrix. Both
|
||||||
|
/// reduce to this, so there is one metric and not two that can drift apart.
|
||||||
|
pub fn nearest_by_colorants(want: &[f32; 9]) -> (ColourSpace, bool) {
|
||||||
|
let (space, distance) = ColourSpace::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|candidate| {
|
||||||
|
let have = candidate.to_pcs_xyz();
|
||||||
|
let d: f32 = want
|
||||||
|
.iter()
|
||||||
|
.zip(have.iter())
|
||||||
|
.map(|(a, b)| (a - b) * (a - b))
|
||||||
|
.sum();
|
||||||
|
(candidate, d)
|
||||||
|
})
|
||||||
|
// `f32` and no NaN: every input is a finite colorant matrix, so
|
||||||
|
// `total_cmp` here is a formality that avoids `partial_cmp().unwrap()`.
|
||||||
|
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||||
|
.expect("ColourSpace::ALL is not empty");
|
||||||
|
|
||||||
|
// The threshold is a sum of nine squared differences over numbers of order
|
||||||
|
// one, so 1e-6 is roughly "every colorant agrees to three decimal places"
|
||||||
|
// — tight enough that a genuinely different gamut never passes, loose
|
||||||
|
// enough that a profile written from these same numbers and read back
|
||||||
|
// through s15Fixed16 rounding does. It is not a perceptual threshold and
|
||||||
|
// is not trying to be: this decides whether to *say* "approximated", and
|
||||||
|
// saying it when in doubt is the honest direction to err.
|
||||||
|
(space, distance < 1e-6)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn chromaticities_of(space: ColourSpace) -> Chromaticities {
|
||||||
|
space.chromaticities()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_known_space_recognises_itself_exactly() {
|
||||||
|
for space in ColourSpace::ALL {
|
||||||
|
let (found, exact) = nearest_space(&chromaticities_of(space));
|
||||||
|
assert_eq!(found, space, "{} matched the wrong space", space.label());
|
||||||
|
assert!(exact, "{} was not recognised as itself", space.label());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_panel_between_two_gamuts_is_reported_as_an_approximation() {
|
||||||
|
// A plausible measured wide-gamut panel: near P3's red and green, not
|
||||||
|
// on them. The requirement is not that it picks P3 — it is that it
|
||||||
|
// picks *something* and admits it is not exact.
|
||||||
|
let measured = Chromaticities {
|
||||||
|
red: [0.6790, 0.3190],
|
||||||
|
green: [0.2680, 0.6880],
|
||||||
|
blue: [0.1490, 0.0570],
|
||||||
|
white: [0.3127, 0.3290],
|
||||||
|
};
|
||||||
|
let (found, exact) = nearest_space(&measured);
|
||||||
|
assert_eq!(found, ColourSpace::DisplayP3);
|
||||||
|
assert!(!exact, "a measured panel must not claim to be a standard");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_d50_wide_gamut_panel_does_not_collapse_onto_srgb() {
|
||||||
|
// The failure this guards: comparing raw xy pairs rather than adapted
|
||||||
|
// colorants makes white-point differences dominate, and everything
|
||||||
|
// lands on whichever space shares its illuminant. ProPhoto's gamut is
|
||||||
|
// unmistakable, so if the metric is right this cannot be anything else.
|
||||||
|
let (found, _) = nearest_space(&chromaticities_of(ColourSpace::ProPhoto));
|
||||||
|
assert_eq!(found, ColourSpace::ProPhoto);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adobe_rgb_and_srgb_are_told_apart_despite_sharing_two_primaries() {
|
||||||
|
// Adobe RGB (1998) differs from sRGB in the green corner alone. A
|
||||||
|
// metric summing over all three colorants must still separate them,
|
||||||
|
// or a wide-gamut photo monitor is driven as sRGB.
|
||||||
|
let (found, exact) = nearest_space(&chromaticities_of(ColourSpace::AdobeRgb));
|
||||||
|
assert_eq!(found, ColourSpace::AdobeRgb);
|
||||||
|
assert!(exact);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_fallback_says_srgb_and_says_why() {
|
||||||
|
let profile = DisplayProfile::fallback(FallbackReason::NoWaylandProtocol);
|
||||||
|
assert_eq!(profile.space, ColourSpace::Srgb);
|
||||||
|
assert!(!profile.source.is_measured());
|
||||||
|
let described = profile.describe();
|
||||||
|
assert!(described.contains("sRGB"), "{described}");
|
||||||
|
assert!(described.contains("assumed"), "{described}");
|
||||||
|
// The specific reason has to survive into the string the About page
|
||||||
|
// renders, or the fallback is defined in the code and invisible on
|
||||||
|
// screen — which is the half of FR-DSP-8 that is easy to skip.
|
||||||
|
assert!(described.contains("colour management"), "{described}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_approximated_profile_says_nearest_rather_than_claiming_the_space() {
|
||||||
|
let profile = DisplayProfile {
|
||||||
|
space: ColourSpace::DisplayP3,
|
||||||
|
source: ProfileSource::X11RootProperty("_ICC_PROFILE".to_string()),
|
||||||
|
described_as: Some("Studio Display calibrated".to_string()),
|
||||||
|
approximated: true,
|
||||||
|
};
|
||||||
|
let described = profile.describe();
|
||||||
|
assert!(described.contains("nearest to Display P3"), "{described}");
|
||||||
|
assert!(
|
||||||
|
described.contains("Studio Display calibrated"),
|
||||||
|
"{described}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn probing_a_platform_that_cannot_answer_still_yields_a_display() {
|
||||||
|
// Whatever happens, composition needs a space. The one thing this
|
||||||
|
// module may never do is hand back nothing.
|
||||||
|
let survey = DisplaySurvey::probe();
|
||||||
|
assert!(!survey.displays.is_empty());
|
||||||
|
assert_eq!(survey.profile(99).space, survey.displays[0].profile.space);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_window_is_located_on_the_display_its_centre_falls_in() {
|
||||||
|
let survey = DisplaySurvey {
|
||||||
|
server: DisplayServer::X11,
|
||||||
|
displays: vec![
|
||||||
|
DisplayInfo {
|
||||||
|
name: "DP-1".to_string(),
|
||||||
|
bounds: Some(Bounds {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 2560,
|
||||||
|
height: 1440,
|
||||||
|
}),
|
||||||
|
profile: DisplayProfile::fallback(FallbackReason::NoProfileForDisplay),
|
||||||
|
},
|
||||||
|
DisplayInfo {
|
||||||
|
name: "DP-2".to_string(),
|
||||||
|
bounds: Some(Bounds {
|
||||||
|
x: 2560,
|
||||||
|
y: 0,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
}),
|
||||||
|
profile: DisplayProfile {
|
||||||
|
space: ColourSpace::AdobeRgb,
|
||||||
|
source: ProfileSource::X11RootProperty("_ICC_PROFILE_1".to_string()),
|
||||||
|
described_as: None,
|
||||||
|
approximated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(survey.containing(100, 100), 0);
|
||||||
|
assert_eq!(survey.containing(3000, 500), 1);
|
||||||
|
// The seam: the first pixel of the second monitor belongs to it, and
|
||||||
|
// the last pixel of the first does not. An inclusive upper bound here
|
||||||
|
// would flip the transform one pixel early on every drag across.
|
||||||
|
assert_eq!(survey.containing(2559, 0), 0);
|
||||||
|
assert_eq!(survey.containing(2560, 0), 1);
|
||||||
|
// Dragged off the desktop entirely — the primary, not a panic.
|
||||||
|
assert_eq!(survey.containing(-4000, 0), 0);
|
||||||
|
|
||||||
|
// And the point of the whole exercise: the two displays yield
|
||||||
|
// different output spaces.
|
||||||
|
assert_eq!(survey.profile(0).space, ColourSpace::Srgb);
|
||||||
|
assert_eq!(survey.profile(1).space, ColourSpace::AdobeRgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
//! TRACES: FR-DSP-8
|
||||||
|
//! Reading just enough of an ICC profile to say which of four spaces it is.
|
||||||
|
//!
|
||||||
|
//! # Deliberately not a profile parser
|
||||||
|
//!
|
||||||
|
//! ICC.1 is a large specification, and a complete reader of it is the front
|
||||||
|
//! half of a colour management module: LUT-based transforms, rendering
|
||||||
|
//! intents, per-intent tag sets, v2 and v4 divergence. None of that helps
|
||||||
|
//! here, because the pipeline does not *apply* a profile — it encodes into one
|
||||||
|
//! of four spaces it already knows the numbers for (`dr_types::colour`). The
|
||||||
|
//! only question a display profile has to answer is which of the four it is
|
||||||
|
//! nearest to, and three tags answer it.
|
||||||
|
//!
|
||||||
|
//! So this reads the tag table, pulls the three colorants and the description,
|
||||||
|
//! and refuses everything else with a reason the About page can show. A
|
||||||
|
//! profile it refuses is not a failure of the photographer's setup and must
|
||||||
|
//! not read like one; it means "this panel is described in a way we do not
|
||||||
|
//! approximate", and sRGB is assumed as FR-DSP-8 defines.
|
||||||
|
//!
|
||||||
|
//! # What it does read
|
||||||
|
//!
|
||||||
|
//! `rXYZ`, `gXYZ`, `bXYZ` — the three colorant tags of a matrix/TRC profile,
|
||||||
|
//! which are the space's primaries already adapted to the D50 connection
|
||||||
|
//! space. That is the same reduction `ColourSpace::to_pcs_xyz` produces, which
|
||||||
|
//! is what makes the comparison in [`super::nearest_by_colorants`] an
|
||||||
|
//! apples-to-apples one, and what makes a profile this project *wrote*
|
||||||
|
//! round-trip back to the space it was written from.
|
||||||
|
//!
|
||||||
|
//! `desc` (v2) or `mluc` (v4) for the profile's own name, which exists only to
|
||||||
|
//! be shown: a photographer recognises the string their calibrator wrote and
|
||||||
|
//! can tell at a glance whether we are reading the profile they think we are.
|
||||||
|
|
||||||
|
/// What a display profile was reduced to.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct IccSummary {
|
||||||
|
/// The three colorant columns, row-major, in the D50 connection space.
|
||||||
|
pub colorants: [f32; 9],
|
||||||
|
/// The profile's own description, where it carried a readable one.
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ICC header's fixed size. Everything before the tag count.
|
||||||
|
const HEADER: usize = 128;
|
||||||
|
|
||||||
|
/// Reduce a profile to the parts that decide an output space.
|
||||||
|
///
|
||||||
|
/// `Err` carries a phrase for [`super::FallbackReason::Unreadable`], so it is
|
||||||
|
/// written to read after "the profile could not be read (…)" on the About page
|
||||||
|
/// rather than as a developer's error string.
|
||||||
|
pub fn read_profile(bytes: &[u8]) -> Result<IccSummary, String> {
|
||||||
|
if bytes.len() < HEADER + 4 {
|
||||||
|
return Err("it is too short to be a profile".to_string());
|
||||||
|
}
|
||||||
|
// Offset 36 is `acsp`, the profile file signature. Checked because the X11
|
||||||
|
// property is a byte array with no type discipline at all: anything can
|
||||||
|
// write anything to a root window, and a profile-shaped blob that is not
|
||||||
|
// one would otherwise be read as colorants of arbitrary magnitude.
|
||||||
|
if &bytes[36..40] != b"acsp" {
|
||||||
|
return Err("it does not carry the ICC file signature".to_string());
|
||||||
|
}
|
||||||
|
// The header's own length field. A profile whose declared size exceeds the
|
||||||
|
// property is truncated — X11 properties have a fetch length, and reading
|
||||||
|
// colorants out of a half-transferred profile would be silently wrong.
|
||||||
|
let declared = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
|
||||||
|
if declared > bytes.len() {
|
||||||
|
return Err(format!(
|
||||||
|
"it declares {declared} bytes and only {} arrived",
|
||||||
|
bytes.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = be_u32(bytes, HEADER)? as usize;
|
||||||
|
// 12 bytes per tag table entry. The bound is not paranoia: `count` comes
|
||||||
|
// from the profile, and a corrupt one multiplied out is how a reader ends
|
||||||
|
// up indexing megabytes past the end.
|
||||||
|
let table_end = HEADER + 4 + count.checked_mul(12).ok_or("its tag table is absurd")?;
|
||||||
|
if table_end > bytes.len() {
|
||||||
|
return Err("its tag table runs past the end of the profile".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tags: Vec<([u8; 4], usize, usize)> = Vec::with_capacity(count);
|
||||||
|
for i in 0..count {
|
||||||
|
let at = HEADER + 4 + i * 12;
|
||||||
|
let sig = [bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]];
|
||||||
|
let offset = be_u32(bytes, at + 4)? as usize;
|
||||||
|
let size = be_u32(bytes, at + 8)? as usize;
|
||||||
|
// Silently skipping a tag that does not fit rather than rejecting the
|
||||||
|
// whole profile: the three we want may all be well-formed while some
|
||||||
|
// fourth tag we will never look at is not.
|
||||||
|
if offset
|
||||||
|
.checked_add(size)
|
||||||
|
.is_some_and(|end| end <= bytes.len())
|
||||||
|
{
|
||||||
|
tags.push((sig, offset, size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let find = |want: &[u8; 4]| {
|
||||||
|
tags.iter()
|
||||||
|
.find(|(sig, _, _)| sig == want)
|
||||||
|
.map(|&(_, offset, size)| &bytes[offset..offset + size])
|
||||||
|
};
|
||||||
|
|
||||||
|
let (Some(r), Some(g), Some(b)) = (find(b"rXYZ"), find(b"gXYZ"), find(b"bXYZ")) else {
|
||||||
|
// The honest description of a LUT-based profile, which is what a
|
||||||
|
// hardware calibrator often produces. It describes the panel more
|
||||||
|
// accurately than a matrix could, and approximating it would need the
|
||||||
|
// CMM this module exists to avoid. Named as a shape rather than as an
|
||||||
|
// error, because nothing is wrong with such a profile.
|
||||||
|
return Err("it is not a matrix/TRC profile".to_string());
|
||||||
|
};
|
||||||
|
let r = xyz_tag(r)?;
|
||||||
|
let g = xyz_tag(g)?;
|
||||||
|
let b = xyz_tag(b)?;
|
||||||
|
|
||||||
|
Ok(IccSummary {
|
||||||
|
// Row-major, columns R/G/B — the layout `to_pcs_xyz` produces, so the
|
||||||
|
// two can be subtracted entry by entry. Transposing one of them is the
|
||||||
|
// single most plausible bug in this file, which is why the round-trip
|
||||||
|
// test asserts a written-then-read profile lands back on its own space
|
||||||
|
// rather than merely on *some* space.
|
||||||
|
colorants: [r[0], g[0], b[0], r[1], g[1], b[1], r[2], g[2], b[2]],
|
||||||
|
description: find(b"desc").and_then(text_tag),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An `XYZType` tag: signature, four reserved bytes, then s15Fixed16 triples.
|
||||||
|
///
|
||||||
|
/// Only the first triple is read. A colorant tag carries exactly one; the
|
||||||
|
/// array form exists for other tags that share the type.
|
||||||
|
fn xyz_tag(data: &[u8]) -> Result<[f32; 3], String> {
|
||||||
|
if data.len() < 20 || &data[0..4] != b"XYZ " {
|
||||||
|
return Err("a colorant tag is not an XYZ value".to_string());
|
||||||
|
}
|
||||||
|
Ok([
|
||||||
|
s15_fixed16(be_i32(data, 8)?),
|
||||||
|
s15_fixed16(be_i32(data, 12)?),
|
||||||
|
s15_fixed16(be_i32(data, 16)?),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A profile's description, from either of the two types that carry one.
|
||||||
|
///
|
||||||
|
/// Returns `None` rather than an error throughout: the name is decoration. A
|
||||||
|
/// profile with unreadable text still has perfectly good colorants, and
|
||||||
|
/// refusing it over a string would put a correctly-described display on the
|
||||||
|
/// fallback for a cosmetic reason.
|
||||||
|
fn text_tag(data: &[u8]) -> Option<String> {
|
||||||
|
match data.get(0..4)? {
|
||||||
|
// ICC v2 `textDescriptionType`: signature, reserved, an ASCII byte
|
||||||
|
// count, then that many bytes with a trailing NUL included in the
|
||||||
|
// count.
|
||||||
|
b"desc" => {
|
||||||
|
let count = be_u32(data, 8).ok()? as usize;
|
||||||
|
let text = data.get(12..12 + count)?;
|
||||||
|
let text = text.split(|&b| b == 0).next().unwrap_or(text);
|
||||||
|
Some(String::from_utf8_lossy(text).trim().to_string()).filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
// ICC v4 `multiLocalizedUnicodeType`: UTF-16BE records. The first
|
||||||
|
// record is taken rather than the one matching the user's locale — a
|
||||||
|
// profile name is an identifier here, shown so it can be recognised,
|
||||||
|
// and picking a locale would be answering a question nobody asked.
|
||||||
|
b"mluc" => {
|
||||||
|
let records = be_u32(data, 8).ok()? as usize;
|
||||||
|
let size = be_u32(data, 12).ok()? as usize;
|
||||||
|
if records == 0 || size < 12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let length = be_u32(data, 16 + 4).ok()? as usize;
|
||||||
|
let offset = be_u32(data, 16 + 8).ok()? as usize;
|
||||||
|
let raw = data.get(offset..offset + length)?;
|
||||||
|
let units: Vec<u16> = raw
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|p| u16::from_be_bytes([p[0], p[1]]))
|
||||||
|
.collect();
|
||||||
|
Some(String::from_utf16_lossy(&units).trim().to_string()).filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn be_u32(bytes: &[u8], at: usize) -> Result<u32, String> {
|
||||||
|
bytes
|
||||||
|
.get(at..at + 4)
|
||||||
|
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
|
||||||
|
.ok_or_else(|| "it ends in the middle of a field".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn be_i32(bytes: &[u8], at: usize) -> Result<i32, String> {
|
||||||
|
be_u32(bytes, at).map(|v| v as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ICC's fixed-point number: sixteen integer bits, sixteen fractional.
|
||||||
|
fn s15_fixed16(v: i32) -> f32 {
|
||||||
|
v as f32 / 65536.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::display::{nearest_by_colorants, nearest_space};
|
||||||
|
use dr_types::ColourSpace;
|
||||||
|
|
||||||
|
/// A minimal matrix/TRC profile carrying one space's colorants.
|
||||||
|
///
|
||||||
|
/// Assembled here rather than taken from `dr-export`, deliberately. That
|
||||||
|
/// crate writes profiles from the same `to_pcs_xyz` this reads back, so a
|
||||||
|
/// round-trip through it would confirm the two halves of *one* set of
|
||||||
|
/// assumptions agree with each other and would still pass if both were
|
||||||
|
/// transposed. Laying the bytes out by hand against ICC.1's field offsets
|
||||||
|
/// is the independent statement.
|
||||||
|
fn profile_for(space: ColourSpace, name: &str) -> Vec<u8> {
|
||||||
|
let m = space.to_pcs_xyz();
|
||||||
|
let colorant = |col: usize| {
|
||||||
|
let mut tag = b"XYZ \0\0\0\0".to_vec();
|
||||||
|
for row in 0..3 {
|
||||||
|
let v = (m[row * 3 + col] * 65536.0).round() as i32;
|
||||||
|
tag.extend_from_slice(&v.to_be_bytes());
|
||||||
|
}
|
||||||
|
tag
|
||||||
|
};
|
||||||
|
let mut desc = b"desc\0\0\0\0".to_vec();
|
||||||
|
let text = format!("{name}\0");
|
||||||
|
desc.extend_from_slice(&(text.len() as u32).to_be_bytes());
|
||||||
|
desc.extend_from_slice(text.as_bytes());
|
||||||
|
|
||||||
|
let tags: Vec<(&[u8; 4], Vec<u8>)> = vec![
|
||||||
|
(b"rXYZ", colorant(0)),
|
||||||
|
(b"gXYZ", colorant(1)),
|
||||||
|
(b"bXYZ", colorant(2)),
|
||||||
|
(b"desc", desc),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut header = vec![0u8; HEADER];
|
||||||
|
header[36..40].copy_from_slice(b"acsp");
|
||||||
|
let mut table = (tags.len() as u32).to_be_bytes().to_vec();
|
||||||
|
let mut body = Vec::new();
|
||||||
|
let base = HEADER + 4 + tags.len() * 12;
|
||||||
|
for (sig, data) in &tags {
|
||||||
|
table.extend_from_slice(*sig);
|
||||||
|
table.extend_from_slice(&((base + body.len()) as u32).to_be_bytes());
|
||||||
|
table.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||||
|
body.extend_from_slice(data);
|
||||||
|
}
|
||||||
|
let mut out = header;
|
||||||
|
out.extend_from_slice(&table);
|
||||||
|
out.extend_from_slice(&body);
|
||||||
|
let len = out.len() as u32;
|
||||||
|
out[0..4].copy_from_slice(&len.to_be_bytes());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_profile_is_recognised_as_the_space_it_describes() {
|
||||||
|
// The whole acquisition path in one assertion: bytes that a colour
|
||||||
|
// manager would publish for a P3 panel must reach the pipeline as
|
||||||
|
// Display P3, and not as sRGB.
|
||||||
|
for space in ColourSpace::ALL {
|
||||||
|
let bytes = profile_for(space, space.label());
|
||||||
|
let summary = read_profile(&bytes).expect("a well-formed profile");
|
||||||
|
let (found, exact) = nearest_by_colorants(&summary.colorants);
|
||||||
|
assert_eq!(found, space, "{} was read as {found:?}", space.label());
|
||||||
|
assert!(
|
||||||
|
exact,
|
||||||
|
"{} did not survive s15Fixed16 rounding",
|
||||||
|
space.label()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_colorant_matrix_is_not_transposed() {
|
||||||
|
// Reading the three tags into rows rather than columns produces a
|
||||||
|
// matrix that is still plausible, still finite, and describes a
|
||||||
|
// different gamut. Adobe RGB is the case that catches it: it differs
|
||||||
|
// from sRGB in one primary, so a transposition moves it onto a space
|
||||||
|
// that is genuinely close and the nearest-match would hide it.
|
||||||
|
let bytes = profile_for(ColourSpace::AdobeRgb, "Adobe RGB");
|
||||||
|
let summary = read_profile(&bytes).expect("a well-formed profile");
|
||||||
|
let want = ColourSpace::AdobeRgb.to_pcs_xyz();
|
||||||
|
|
||||||
|
// Not `assert_eq`: the values went out through s15Fixed16 and came
|
||||||
|
// back, so they agree to about 1e-5 and never exactly. The tolerance
|
||||||
|
// is three orders of magnitude tighter than the smallest off-diagonal
|
||||||
|
// difference below, so it still catches the thing it is here for.
|
||||||
|
for (have, want) in summary.colorants.iter().zip(want.iter()) {
|
||||||
|
assert!((have - want).abs() < 1e-4, "{:?}", summary.colorants);
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the guard that makes the assertion above mean something: a
|
||||||
|
// symmetric matrix would satisfy it transposed as well.
|
||||||
|
assert!(
|
||||||
|
(want[1] - want[3]).abs() > 1e-2,
|
||||||
|
"the colorant matrix must not be symmetric or this proves nothing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_profile_names_itself_where_it_can() {
|
||||||
|
let bytes = profile_for(ColourSpace::Srgb, "EIZO CG279X calibrated 2024-03");
|
||||||
|
let summary = read_profile(&bytes).expect("a well-formed profile");
|
||||||
|
assert_eq!(
|
||||||
|
summary.description.as_deref(),
|
||||||
|
Some("EIZO CG279X calibrated 2024-03")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_v4_profile_names_itself_from_its_unicode_records() {
|
||||||
|
let mut mluc = b"mluc\0\0\0\0".to_vec();
|
||||||
|
let text: Vec<u8> = "LG UltraFine"
|
||||||
|
.encode_utf16()
|
||||||
|
.flat_map(u16::to_be_bytes)
|
||||||
|
.collect();
|
||||||
|
mluc.extend_from_slice(&1u32.to_be_bytes()); // one record
|
||||||
|
mluc.extend_from_slice(&12u32.to_be_bytes()); // record size
|
||||||
|
mluc.extend_from_slice(b"enUS");
|
||||||
|
mluc.extend_from_slice(&(text.len() as u32).to_be_bytes());
|
||||||
|
mluc.extend_from_slice(&28u32.to_be_bytes()); // offset within the tag
|
||||||
|
mluc.extend_from_slice(&text);
|
||||||
|
assert_eq!(text_tag(&mluc).as_deref(), Some("LG UltraFine"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_lut_profile_is_refused_by_shape_and_not_by_crashing() {
|
||||||
|
// A hardware calibrator's `mAB `-based profile. There is nothing wrong
|
||||||
|
// with it; we simply cannot approximate it without the CMM this module
|
||||||
|
// exists to avoid, and the About page has to be able to say so.
|
||||||
|
let mut header = vec![0u8; HEADER];
|
||||||
|
header[36..40].copy_from_slice(b"acsp");
|
||||||
|
header.extend_from_slice(&0u32.to_be_bytes());
|
||||||
|
let len = header.len() as u32;
|
||||||
|
header[0..4].copy_from_slice(&len.to_be_bytes());
|
||||||
|
let err = read_profile(&header).expect_err("no colorants to read");
|
||||||
|
assert!(err.contains("matrix/TRC"), "{err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rubbish_on_a_root_window_is_refused_rather_than_read_as_colour() {
|
||||||
|
// An X11 property is untyped and world-writable by convention. None of
|
||||||
|
// these may panic, and none may produce a colour space.
|
||||||
|
assert!(read_profile(&[]).is_err());
|
||||||
|
assert!(read_profile(&[0u8; 200]).is_err());
|
||||||
|
|
||||||
|
let mut truncated = profile_for(ColourSpace::Srgb, "sRGB");
|
||||||
|
truncated.truncate(truncated.len() / 2);
|
||||||
|
assert!(read_profile(&truncated).is_err());
|
||||||
|
|
||||||
|
// A profile whose tag table claims far more tags than there are bytes.
|
||||||
|
let mut absurd = profile_for(ColourSpace::Srgb, "sRGB");
|
||||||
|
absurd[HEADER..HEADER + 4].copy_from_slice(&u32::MAX.to_be_bytes());
|
||||||
|
assert!(read_profile(&absurd).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_two_entry_points_agree() {
|
||||||
|
// `nearest_space` (chromaticities, from Wayland) and
|
||||||
|
// `nearest_by_colorants` (a matrix, from ICC) must not drift apart:
|
||||||
|
// they are the same decision reached from the two halves of the same
|
||||||
|
// definition, and a session that answered differently depending on
|
||||||
|
// which display server it was on would be the worst kind of bug to
|
||||||
|
// reproduce.
|
||||||
|
for space in ColourSpace::ALL {
|
||||||
|
let bytes = profile_for(space, "x");
|
||||||
|
let summary = read_profile(&bytes).expect("a well-formed profile");
|
||||||
|
assert_eq!(
|
||||||
|
nearest_by_colorants(&summary.colorants).0,
|
||||||
|
nearest_space(&space.chromaticities()).0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
//! TRACES: FR-DSP-8 | FR-PLAT-LIN-2
|
||||||
|
//! Acquisition on Wayland: ask the compositor, or admit that we cannot.
|
||||||
|
//!
|
||||||
|
//! # Why there is no back door here
|
||||||
|
//!
|
||||||
|
//! X11's mechanism works because any client may read any root-window
|
||||||
|
//! property. Wayland has no such shared state by design — a client sees its
|
||||||
|
//! own surfaces and nothing else — so the profile has to be *given* to us, and
|
||||||
|
//! the protocol that gives it, `wp_color_manager_v1`, is still staging
|
||||||
|
//! upstream. That is the concrete form of FR-DSP-8's "a defined fallback where
|
||||||
|
//! Wayland provides no profile": on a compositor that does not advertise the
|
||||||
|
//! global there is no mechanism to fall back *from*, and pretending otherwise
|
||||||
|
//! would mean reading the Xwayland root window and presenting a value the
|
||||||
|
//! compositor never agreed to.
|
||||||
|
//!
|
||||||
|
//! So: bind the global if it is there, ask each `wl_output` for its image
|
||||||
|
//! description, and where it is not there return
|
||||||
|
//! [`FallbackReason::NoWaylandProtocol`], which the About page renders in
|
||||||
|
//! words.
|
||||||
|
//!
|
||||||
|
//! # Two forms of the same answer
|
||||||
|
//!
|
||||||
|
//! The compositor may describe an output either by handing over an ICC profile
|
||||||
|
//! on a file descriptor or by stating primaries as CIE xy chromaticities.
|
||||||
|
//! Both are accepted, and both reduce to the same nearest-space decision — see
|
||||||
|
//! `super::nearest_by_colorants` for why that reduction has to happen in one
|
||||||
|
//! place. The ICC form is preferred where both arrive, because it carries the
|
||||||
|
//! profile's own name, which is the string a photographer recognises.
|
||||||
|
//!
|
||||||
|
//! # Position is deliberately not asked for
|
||||||
|
//!
|
||||||
|
//! [`super::DisplaySurvey::containing`] explains the consequence at length:
|
||||||
|
//! `bounds` is `None` on every Wayland output, not because `wl_output` refuses
|
||||||
|
//! to state its position but because a client is never told where its *own*
|
||||||
|
//! window is, so a rectangle to test against would have no point to test.
|
||||||
|
|
||||||
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
|
|
||||||
|
use wayland_client::protocol::{wl_output, wl_registry};
|
||||||
|
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||||
|
use wayland_protocols::wp::color_management::v1::client::{
|
||||||
|
wp_color_management_output_v1, wp_color_manager_v1, wp_image_description_info_v1,
|
||||||
|
wp_image_description_v1,
|
||||||
|
};
|
||||||
|
|
||||||
|
use dr_types::colour::Chromaticities;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
nearest_by_colorants, nearest_space, DisplayInfo, DisplayProfile, FallbackReason, ProfileSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The highest protocol version this client knows how to receive.
|
||||||
|
///
|
||||||
|
/// Bound as high as the compositor offers rather than at 1, because
|
||||||
|
/// `get_image_description` fails with `low_version` when the client cannot
|
||||||
|
/// receive every event the output's description needs — so asking for less
|
||||||
|
/// than we understand turns a describable display into a fallback.
|
||||||
|
const MAX_MANAGER_VERSION: u32 = 3;
|
||||||
|
|
||||||
|
/// `wl_output.name` arrived in version 4. Below it an output has no stable
|
||||||
|
/// identifier and is listed by index, which is cosmetic only.
|
||||||
|
const MAX_OUTPUT_VERSION: u32 = 4;
|
||||||
|
|
||||||
|
/// What one output told us, accumulated across the events that describe it.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Pending {
|
||||||
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
/// The ICC profile the compositor handed over, if it did.
|
||||||
|
icc: Option<Vec<u8>>,
|
||||||
|
/// Primaries as CIE xy, if it stated them instead.
|
||||||
|
primaries: Option<Chromaticities>,
|
||||||
|
/// Set when the image description could not be produced at all.
|
||||||
|
failed: Option<String>,
|
||||||
|
/// Whether anything at all came back for this output.
|
||||||
|
answered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
manager: Option<wp_color_manager_v1::WpColorManagerV1>,
|
||||||
|
outputs: Vec<wl_output::WlOutput>,
|
||||||
|
pending: Vec<Pending>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every output the compositor has, with whatever colour it will state.
|
||||||
|
pub fn probe() -> Result<Vec<DisplayInfo>, FallbackReason> {
|
||||||
|
let conn = Connection::connect_to_env()
|
||||||
|
.map_err(|e| FallbackReason::NoConnection(format!("Wayland: {e}")))?;
|
||||||
|
let mut queue = conn.new_event_queue();
|
||||||
|
let qh = queue.handle();
|
||||||
|
conn.display().get_registry(&qh, ());
|
||||||
|
|
||||||
|
let mut state = State {
|
||||||
|
manager: None,
|
||||||
|
outputs: Vec::new(),
|
||||||
|
pending: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// First roundtrip: the registry's globals, which is where both the manager
|
||||||
|
// and the outputs are bound.
|
||||||
|
roundtrip(&mut queue, &mut state)?;
|
||||||
|
// Second: the events those newly bound objects emit — `wl_output.name`,
|
||||||
|
// and the manager's capability advertisements. A single roundtrip cannot
|
||||||
|
// carry them, because the objects did not exist when the first request
|
||||||
|
// went out.
|
||||||
|
roundtrip(&mut queue, &mut state)?;
|
||||||
|
|
||||||
|
let Some(manager) = state.manager.clone() else {
|
||||||
|
// The case FR-DSP-8's fallback clause is written for, and today the
|
||||||
|
// common one. Reported as its own reason rather than folded into
|
||||||
|
// "no profile", because the two lead a user somewhere different: this
|
||||||
|
// one is the compositor's feature set, not their calibration.
|
||||||
|
return Err(FallbackReason::NoWaylandProtocol);
|
||||||
|
};
|
||||||
|
if state.outputs.is_empty() {
|
||||||
|
return Err(FallbackReason::NoConnection(
|
||||||
|
"Wayland: the compositor listed no outputs".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask every output at once and let one roundtrip settle them all. Asking
|
||||||
|
// serially would be a roundtrip per monitor on a path that runs at startup
|
||||||
|
// and on every display change.
|
||||||
|
for (index, output) in state.outputs.clone().iter().enumerate() {
|
||||||
|
let cm_output = manager.get_output(output, &qh, index);
|
||||||
|
cm_output.get_image_description(&qh, index);
|
||||||
|
}
|
||||||
|
roundtrip(&mut queue, &mut state)?;
|
||||||
|
// `get_information` is only legal once the description is ready, so it
|
||||||
|
// cannot be batched with the request above; this is the second and last
|
||||||
|
// roundtrip of the exchange.
|
||||||
|
roundtrip(&mut queue, &mut state)?;
|
||||||
|
|
||||||
|
Ok(state
|
||||||
|
.pending
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, pending)| DisplayInfo {
|
||||||
|
name: pending
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("display {}", index + 1)),
|
||||||
|
// See the module comment: a rectangle with no point to test.
|
||||||
|
bounds: None,
|
||||||
|
profile: resolve(pending),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn one output's accumulated events into an output space.
|
||||||
|
fn resolve(pending: &Pending) -> DisplayProfile {
|
||||||
|
if let Some(why) = &pending.failed {
|
||||||
|
return DisplayProfile::fallback(FallbackReason::Unreadable(format!("Wayland: {why}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ICC form first: it is the only one that carries a name, and a named
|
||||||
|
// profile is what lets a photographer confirm we are reading theirs.
|
||||||
|
if let Some(bytes) = &pending.icc {
|
||||||
|
return match super::icc::read_profile(bytes) {
|
||||||
|
Ok(summary) => {
|
||||||
|
let (space, exact) = nearest_by_colorants(&summary.colorants);
|
||||||
|
DisplayProfile {
|
||||||
|
space,
|
||||||
|
source: ProfileSource::WaylandColourManagement,
|
||||||
|
described_as: summary.description.or_else(|| pending.description.clone()),
|
||||||
|
approximated: !exact,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(why) => DisplayProfile::fallback(FallbackReason::Unreadable(why)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(primaries) = pending.primaries {
|
||||||
|
let (space, exact) = nearest_space(&primaries);
|
||||||
|
return DisplayProfile {
|
||||||
|
space,
|
||||||
|
source: ProfileSource::WaylandColourManagement,
|
||||||
|
described_as: pending.description.clone(),
|
||||||
|
approximated: !exact,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The protocol was there and the output had nothing to say through it.
|
||||||
|
DisplayProfile::fallback(if pending.answered {
|
||||||
|
FallbackReason::NoProfileForDisplay
|
||||||
|
} else {
|
||||||
|
FallbackReason::Unreadable("Wayland: the compositor did not answer".to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn roundtrip(
|
||||||
|
queue: &mut wayland_client::EventQueue<State>,
|
||||||
|
state: &mut State,
|
||||||
|
) -> Result<(), FallbackReason> {
|
||||||
|
queue
|
||||||
|
.roundtrip(state)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| FallbackReason::NoConnection(format!("Wayland: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
registry: &wl_registry::WlRegistry,
|
||||||
|
event: wl_registry::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
let wl_registry::Event::Global {
|
||||||
|
name,
|
||||||
|
interface,
|
||||||
|
version,
|
||||||
|
} = event
|
||||||
|
else {
|
||||||
|
// `GlobalRemove` is a monitor being unplugged mid-probe. Ignored
|
||||||
|
// here: the survey is re-run on a display change anyway, and
|
||||||
|
// reacting to it inside a two-roundtrip exchange would only make
|
||||||
|
// the indices disagree with the vector.
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if interface == wp_color_manager_v1::WpColorManagerV1::interface().name {
|
||||||
|
state.manager = Some(registry.bind(name, version.min(MAX_MANAGER_VERSION), qh, ()));
|
||||||
|
} else if interface == wl_output::WlOutput::interface().name {
|
||||||
|
let index = state.outputs.len();
|
||||||
|
state.pending.push(Pending::default());
|
||||||
|
state
|
||||||
|
.outputs
|
||||||
|
.push(registry.bind(name, version.min(MAX_OUTPUT_VERSION), qh, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wl_output::WlOutput, usize> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &wl_output::WlOutput,
|
||||||
|
event: wl_output::Event,
|
||||||
|
index: &usize,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
let Some(pending) = state.pending.get_mut(*index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match event {
|
||||||
|
// The connector name — "DP-1", "eDP-1" — which is what a user with
|
||||||
|
// two monitors recognises in a list.
|
||||||
|
wl_output::Event::Name { name } => pending.name = Some(name),
|
||||||
|
wl_output::Event::Description { description } => {
|
||||||
|
pending.description = Some(description)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wp_color_manager_v1::WpColorManagerV1, ()> for State {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
_: &wp_color_manager_v1::WpColorManagerV1,
|
||||||
|
_: wp_color_manager_v1::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
// The manager advertises which intents, features, transfer functions
|
||||||
|
// and named primaries it supports. All of that constrains what a
|
||||||
|
// client may *create*; this one only reads what the outputs already
|
||||||
|
// are, so none of it applies.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wp_color_management_output_v1::WpColorManagementOutputV1, usize> for State {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
_: &wp_color_management_output_v1::WpColorManagementOutputV1,
|
||||||
|
_: wp_color_management_output_v1::Event,
|
||||||
|
_: &usize,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
// `image_description_changed` says the output's colour has changed —
|
||||||
|
// a user switching their monitor's picture mode, or assigning a new
|
||||||
|
// profile. Not acted on here: this object lives for the length of one
|
||||||
|
// probe and is dropped with it. The application re-surveys instead,
|
||||||
|
// which also covers the X11 side, where there is no such event.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wp_image_description_v1::WpImageDescriptionV1, usize> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
description: &wp_image_description_v1::WpImageDescriptionV1,
|
||||||
|
event: wp_image_description_v1::Event,
|
||||||
|
index: &usize,
|
||||||
|
_: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
let Some(pending) = state.pending.get_mut(*index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match event {
|
||||||
|
// `ready` at protocol version 1, `ready2` from version 2. Both
|
||||||
|
// mean the same thing and both are handled, because the version
|
||||||
|
// bound depends on the compositor.
|
||||||
|
wp_image_description_v1::Event::Ready { .. }
|
||||||
|
| wp_image_description_v1::Event::Ready2 { .. } => {
|
||||||
|
pending.answered = true;
|
||||||
|
description.get_information(qh, *index);
|
||||||
|
}
|
||||||
|
wp_image_description_v1::Event::Failed { cause, msg } => {
|
||||||
|
pending.answered = true;
|
||||||
|
pending.failed = Some(format!("{cause:?}: {msg}"));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wp_image_description_info_v1::WpImageDescriptionInfoV1, usize> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &wp_image_description_info_v1::WpImageDescriptionInfoV1,
|
||||||
|
event: wp_image_description_info_v1::Event,
|
||||||
|
index: &usize,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
let Some(pending) = state.pending.get_mut(*index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match event {
|
||||||
|
wp_image_description_info_v1::Event::IccFile { icc, icc_size } => {
|
||||||
|
match read_fd(icc, icc_size as usize) {
|
||||||
|
Ok(bytes) => pending.icc = Some(bytes),
|
||||||
|
Err(e) => log::warn!("reading the compositor's ICC profile: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wp_image_description_info_v1::Event::Primaries {
|
||||||
|
r_x,
|
||||||
|
r_y,
|
||||||
|
g_x,
|
||||||
|
g_y,
|
||||||
|
b_x,
|
||||||
|
b_y,
|
||||||
|
w_x,
|
||||||
|
w_y,
|
||||||
|
} => {
|
||||||
|
// The protocol carries six decimals by multiplying by a
|
||||||
|
// million; undoing it here rather than anywhere downstream
|
||||||
|
// keeps `Chromaticities` meaning one thing everywhere.
|
||||||
|
let xy = |x: i32, y: i32| [f64::from(x) / 1e6, f64::from(y) / 1e6];
|
||||||
|
pending.primaries = Some(Chromaticities {
|
||||||
|
red: xy(r_x, r_y),
|
||||||
|
green: xy(g_x, g_y),
|
||||||
|
blue: xy(b_x, b_y),
|
||||||
|
white: xy(w_x, w_y),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a profile out of the file descriptor the compositor passed.
|
||||||
|
///
|
||||||
|
/// Seeks to the start first. The descriptor is a dup of the compositor's own,
|
||||||
|
/// so it shares a file offset with it; assuming zero is the kind of assumption
|
||||||
|
/// that works on every compositor until it does not.
|
||||||
|
fn read_fd(fd: std::os::fd::OwnedFd, size: usize) -> std::io::Result<Vec<u8>> {
|
||||||
|
let mut file = std::fs::File::from(fd);
|
||||||
|
file.seek(SeekFrom::Start(0))?;
|
||||||
|
let mut bytes = Vec::with_capacity(size.min(1 << 20));
|
||||||
|
// Bounded by what the compositor declared, and separately by a megabyte:
|
||||||
|
// a display profile is a few kilobytes, and the size is a number from
|
||||||
|
// another process.
|
||||||
|
file.take(size.min(1 << 20) as u64)
|
||||||
|
.read_to_end(&mut bytes)?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
//! TRACES: FR-DSP-8 | FR-PLAT-LIN-2
|
||||||
|
//! Acquisition on X11: ICC profiles as root-window properties.
|
||||||
|
//!
|
||||||
|
//! The mechanism is the ICC Profiles in X specification, and it predates
|
||||||
|
//! everything else in this module by two decades. A colour manager — colord
|
||||||
|
//! under GNOME, `xiccd`, `xcalib`, or the desktop's own — loads the profile
|
||||||
|
//! for each output and publishes its bytes on the *root window* as a property:
|
||||||
|
//! `_ICC_PROFILE` for the first output, `_ICC_PROFILE_1`, `_ICC_PROFILE_2` and
|
||||||
|
//! so on for the rest, numbered by the output's position in RandR's list.
|
||||||
|
//!
|
||||||
|
//! Any client can read them, which is exactly why this works and exactly why
|
||||||
|
//! [`super::icc::read_profile`] validates so carefully: a root-window property
|
||||||
|
//! is untyped shared state that anything on the display may write.
|
||||||
|
//!
|
||||||
|
//! # Geometry comes from RandR, and it is what makes a move detectable
|
||||||
|
//!
|
||||||
|
//! X11 will also say where each output sits in the desktop's coordinate space,
|
||||||
|
//! and will tell a client where its own window is. Those two facts together
|
||||||
|
//! are the whole of FR-DSP-8's "updates when the window moves between
|
||||||
|
//! displays" on this display server — the window's centre falls in one
|
||||||
|
//! rectangle or another, and the answer changes as it is dragged. Wayland
|
||||||
|
//! gives neither, which is why `wayland.rs` says what it says.
|
||||||
|
|
||||||
|
use x11rb::connection::Connection;
|
||||||
|
use x11rb::protocol::randr::ConnectionExt as RandrExt;
|
||||||
|
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt as XExt};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
nearest_by_colorants, Bounds, DisplayInfo, DisplayProfile, FallbackReason, ProfileSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Every output the X server has, with whatever profile is published for it.
|
||||||
|
pub fn probe() -> Result<Vec<DisplayInfo>, FallbackReason> {
|
||||||
|
let (conn, screen_num) =
|
||||||
|
x11rb::connect(None).map_err(|e| FallbackReason::NoConnection(format!("X11: {e}")))?;
|
||||||
|
let root = conn
|
||||||
|
.setup()
|
||||||
|
.roots
|
||||||
|
.get(screen_num)
|
||||||
|
.ok_or_else(|| FallbackReason::NoConnection("X11: no screen".to_string()))?
|
||||||
|
.root;
|
||||||
|
|
||||||
|
// RandR describes the outputs. Its absence is survivable: a server without
|
||||||
|
// it still has a root window with `_ICC_PROFILE` on it, so fall through to
|
||||||
|
// a single unnamed display carrying the first property rather than giving
|
||||||
|
// up on colour management entirely.
|
||||||
|
let outputs = enumerate(&conn, root).unwrap_or_default();
|
||||||
|
let outputs = if outputs.is_empty() {
|
||||||
|
vec![(String::from("display"), None)]
|
||||||
|
} else {
|
||||||
|
outputs
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(outputs
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, (name, bounds))| DisplayInfo {
|
||||||
|
name,
|
||||||
|
bounds,
|
||||||
|
profile: profile_for(&conn, root, index),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An output's connector name and the rectangle it occupies, where RandR
|
||||||
|
/// would say. Named so that the order of the two is fixed in one place.
|
||||||
|
type Output = (String, Option<Bounds>);
|
||||||
|
|
||||||
|
/// The connected outputs, in RandR order, with their desktop rectangles.
|
||||||
|
///
|
||||||
|
/// Order is load-bearing rather than cosmetic: it is what numbers the
|
||||||
|
/// `_ICC_PROFILE_<n>` properties, so reordering this list would hand every
|
||||||
|
/// monitor its neighbour's profile.
|
||||||
|
fn enumerate(conn: &impl Connection, root: u32) -> Result<Vec<Output>, Box<dyn std::error::Error>> {
|
||||||
|
let resources = conn.randr_get_screen_resources_current(root)?.reply()?;
|
||||||
|
let stamp = resources.config_timestamp;
|
||||||
|
let mut found = Vec::new();
|
||||||
|
for output in resources.outputs {
|
||||||
|
let Ok(info) = conn.randr_get_output_info(output, stamp)?.reply() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// `crtc == 0` is an output with no CRTC driving it: a port with
|
||||||
|
// nothing plugged in, or a disabled monitor. It occupies no part of
|
||||||
|
// the desktop and takes no place in the `_ICC_PROFILE_<n>` numbering.
|
||||||
|
if info.crtc == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = String::from_utf8_lossy(&info.name).to_string();
|
||||||
|
let bounds = conn
|
||||||
|
.randr_get_crtc_info(info.crtc, stamp)
|
||||||
|
.ok()
|
||||||
|
.and_then(|c| c.reply().ok())
|
||||||
|
.map(|c| Bounds {
|
||||||
|
x: i32::from(c.x),
|
||||||
|
y: i32::from(c.y),
|
||||||
|
width: u32::from(c.width),
|
||||||
|
height: u32::from(c.height),
|
||||||
|
});
|
||||||
|
found.push((name, bounds));
|
||||||
|
}
|
||||||
|
Ok(found)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The property this output's profile would be published on, read and reduced.
|
||||||
|
fn profile_for(conn: &impl Connection, root: u32, index: usize) -> DisplayProfile {
|
||||||
|
// The specification's numbering: the first output's property carries no
|
||||||
|
// suffix. Writing `_ICC_PROFILE_0` instead reads nothing on every desktop.
|
||||||
|
let name = if index == 0 {
|
||||||
|
"_ICC_PROFILE".to_string()
|
||||||
|
} else {
|
||||||
|
format!("_ICC_PROFILE_{index}")
|
||||||
|
};
|
||||||
|
|
||||||
|
let bytes = match fetch(conn, root, &name) {
|
||||||
|
Ok(Some(bytes)) => bytes,
|
||||||
|
// No property. The overwhelmingly common case on a stock desktop with
|
||||||
|
// no calibration installed, and not a fault of any kind.
|
||||||
|
Ok(None) => return DisplayProfile::fallback(FallbackReason::NoProfileForDisplay),
|
||||||
|
Err(e) => return DisplayProfile::fallback(FallbackReason::Unreadable(format!("X11: {e}"))),
|
||||||
|
};
|
||||||
|
|
||||||
|
match super::icc::read_profile(&bytes) {
|
||||||
|
Ok(summary) => {
|
||||||
|
let (space, exact) = nearest_by_colorants(&summary.colorants);
|
||||||
|
DisplayProfile {
|
||||||
|
space,
|
||||||
|
source: ProfileSource::X11RootProperty(name),
|
||||||
|
described_as: summary.description,
|
||||||
|
approximated: !exact,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(why) => DisplayProfile::fallback(FallbackReason::Unreadable(why)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a root-window property whole.
|
||||||
|
///
|
||||||
|
/// The length is asked for in 32-bit units, which is X11's unit for this call
|
||||||
|
/// and a trap worth naming: a display profile is a few kilobytes and a request
|
||||||
|
/// stated in bytes would truncate it into a shape `read_profile` then rejects
|
||||||
|
/// as corrupt. `u32::MAX / 4` asks for all of it.
|
||||||
|
fn fetch(
|
||||||
|
conn: &impl Connection,
|
||||||
|
root: u32,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||||
|
let atom = conn.intern_atom(true, name.as_bytes())?.reply()?.atom;
|
||||||
|
// `only_if_exists` above: an atom that no one has interned is a property
|
||||||
|
// no one has set, and interning it ourselves would litter the server with
|
||||||
|
// atoms for the monitors this desktop does not have.
|
||||||
|
if atom == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let reply = conn
|
||||||
|
.get_property(false, root, atom, AtomEnum::CARDINAL, 0, u32::MAX / 4)?
|
||||||
|
.reply()?;
|
||||||
|
if reply.value.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(reply.value))
|
||||||
|
}
|
||||||
@@ -4,10 +4,15 @@
|
|||||||
//! construction, so `core/` contains no `#[cfg(target_os)]` (NFR-PORT-1,
|
//! construction, so `core/` contains no `#[cfg(target_os)]` (NFR-PORT-1,
|
||||||
//! ARCH §10).
|
//! ARCH §10).
|
||||||
|
|
||||||
|
pub mod display;
|
||||||
pub mod secrets;
|
pub mod secrets;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod volumes;
|
pub mod volumes;
|
||||||
|
|
||||||
|
pub use display::{
|
||||||
|
Bounds, DisplayInfo, DisplayProfile, DisplayServer, DisplaySurvey, FallbackReason,
|
||||||
|
ProfileSource,
|
||||||
|
};
|
||||||
pub use secrets::{
|
pub use secrets::{
|
||||||
EphemeralSecretStore, PlatformSecretStore, SecretError, SecretKind, SecretRef, SecretStore,
|
EphemeralSecretStore, PlatformSecretStore, SecretError, SecretKind, SecretRef, SecretStore,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ use anyhow::{bail, Context, Result};
|
|||||||
use traceability::*;
|
use traceability::*;
|
||||||
|
|
||||||
/// Directories scanned for tags.
|
/// Directories scanned for tags.
|
||||||
const SOURCE_ROOTS: &[&str] = &["core", "ui", "apps", "tools"];
|
///
|
||||||
|
/// `platform` belongs here as much as the rest: it is where the display-server
|
||||||
|
/// and secret-store integrations live, so leaving it out made every
|
||||||
|
/// FR-PLAT-\* and NFR-PORT-\* tag in `dr-plat` invisible and understated the
|
||||||
|
/// matrix by exactly the requirements the platform layer exists to satisfy.
|
||||||
|
const SOURCE_ROOTS: &[&str] = &["core", "ui", "platform", "apps", "tools"];
|
||||||
|
|
||||||
/// Minimum coverage the gate accepts.
|
/// Minimum coverage the gate accepts.
|
||||||
///
|
///
|
||||||
|
|||||||
+168
-3
@@ -686,6 +686,22 @@ pub struct DevelopSession {
|
|||||||
/// open. One value rather than one per operation, for the same reason
|
/// open. One value rather than one per operation, for the same reason
|
||||||
/// `curve_samples` is one polyline: the panel draws one curve.
|
/// `curve_samples` is one polyline: the panel draws one curve.
|
||||||
curve_channel: usize,
|
curve_channel: usize,
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// The space the canvas is encoded into, for the display now showing it.
|
||||||
|
///
|
||||||
|
/// **Not part of the edit, and not interface state either.** It is a fact
|
||||||
|
/// about the glass in front of the photographer: the same graph on the
|
||||||
|
/// same file composes differently on a wide-gamut second monitor, and
|
||||||
|
/// neither the sidecar nor the undo stack has any business knowing about
|
||||||
|
/// it. That is also why it lives here rather than on the `EditGraph` —
|
||||||
|
/// `compose_for` deliberately takes the space per call because "the same
|
||||||
|
/// edit goes to the screen in the display's space and to a file in
|
||||||
|
/// whatever the export asks for, and neither is more authoritative".
|
||||||
|
///
|
||||||
|
/// sRGB until the application says otherwise, which is the same answer
|
||||||
|
/// `dr_plat::display`'s fallback gives and means a session constructed in
|
||||||
|
/// a test behaves exactly as it did before this existed.
|
||||||
|
display_space: dr_types::ColourSpace,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DevelopSession {
|
impl DevelopSession {
|
||||||
@@ -756,6 +772,7 @@ impl DevelopSession {
|
|||||||
show_overlay: false,
|
show_overlay: false,
|
||||||
active_tab: None,
|
active_tab: None,
|
||||||
curve_channel: 0,
|
curve_channel: 0,
|
||||||
|
display_space: dr_types::ColourSpace::Srgb,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2624,6 +2641,30 @@ impl DevelopSession {
|
|||||||
Some((op.id, param.id))
|
Some((op.id, param.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// Encode the canvas for a different display from now on.
|
||||||
|
///
|
||||||
|
/// Returns whether anything changed, so a caller polling for window moves
|
||||||
|
/// can redraw only when the answer is genuinely different — the poll runs
|
||||||
|
/// far more often than a monitor is changed, and a redraw per poll would
|
||||||
|
/// undo the point of rendering on demand.
|
||||||
|
///
|
||||||
|
/// Nothing is invalidated here and nothing needs to be. The next
|
||||||
|
/// [`Self::render`] composes against the new space, the pipeline cache
|
||||||
|
/// distinguishes the two shaders by the structure hash the space enters,
|
||||||
|
/// and the mask array — rasterised in source space, sampled through the
|
||||||
|
/// framing — is unaffected because a colour space is not a geometry.
|
||||||
|
pub fn set_display_space(&mut self, space: dr_types::ColourSpace) -> bool {
|
||||||
|
let changed = self.display_space != space;
|
||||||
|
self.display_space = space;
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The space the canvas is currently being encoded into.
|
||||||
|
pub fn display_space(&self) -> dr_types::ColourSpace {
|
||||||
|
self.display_space
|
||||||
|
}
|
||||||
|
|
||||||
/// TRACES: FR-DSP-1 | AC-8
|
/// TRACES: FR-DSP-1 | AC-8
|
||||||
/// Render at the requested display size and hand back a Slint image.
|
/// Render at the requested display size and hand back a Slint image.
|
||||||
///
|
///
|
||||||
@@ -2654,11 +2695,24 @@ impl DevelopSession {
|
|||||||
let (fw, fh) = self.graph.output_size(sw, sh);
|
let (fw, fh) = self.graph.output_size(sw, sh);
|
||||||
let (w, h) = fit(fw, fh, width.max(1), height.max(1));
|
let (w, h) = fit(fw, fh, width.max(1), height.max(1));
|
||||||
|
|
||||||
let shader = self.graph.compose();
|
// TRACES: FR-DSP-8 | FR-DSP-6
|
||||||
|
// **Composed for the display that is showing this canvas**, not for
|
||||||
|
// sRGB. This is the whole of FR-DSP-8's second half arriving at the
|
||||||
|
// pipeline: a display change is a *recomposition* and nothing more,
|
||||||
|
// because the output space was always a parameter of composition and
|
||||||
|
// always entered the structure hash. Moving the window to a P3 panel
|
||||||
|
// therefore costs one shader compile and no pipeline change at all.
|
||||||
|
let space = self.display_space;
|
||||||
|
let shader = self.graph.compose_for(space);
|
||||||
|
|
||||||
// Rasterise the masks first: the shader addresses array slices by
|
// Rasterise the masks first: the shader addresses array slices by
|
||||||
// index, so the array has to describe *this* stack before it is bound.
|
// index, so the array has to describe *this* stack before it is bound.
|
||||||
self.render_with_masks(&shader, w, h, dr_types::ColourSpace::Srgb)?;
|
//
|
||||||
|
// The same `space` to both, necessarily: where a detail stage exists
|
||||||
|
// it is the *last* pass that performs the output transform, and two
|
||||||
|
// halves composed for different spaces would encode the frame twice
|
||||||
|
// or not at all.
|
||||||
|
self.render_with_masks(&shader, w, h, space)?;
|
||||||
let texture = self.adjust.output().ok_or("nothing was rendered")?;
|
let texture = self.adjust.output().ok_or("nothing was rendered")?;
|
||||||
|
|
||||||
// The import is fallible on format and usage only, and both are fixed
|
// The import is fallible on format and usage only, and both are fixed
|
||||||
@@ -2702,7 +2756,11 @@ impl DevelopSession {
|
|||||||
/// explicit about. It is in the output colour space, which is what
|
/// explicit about. It is in the output colour space, which is what
|
||||||
/// FR-DSP-7 asks for — the levels counted are the levels the display will
|
/// FR-DSP-7 asks for — the levels counted are the levels the display will
|
||||||
/// show, so a clipped bin means a highlight that is actually gone rather
|
/// show, so a clipped bin means a highlight that is actually gone rather
|
||||||
/// than one the transform might still recover. And when the view is zoomed
|
/// than one the transform might still recover. Since FR-DSP-8 that is the
|
||||||
|
/// space of *this display* rather than sRGB, which makes the reading more
|
||||||
|
/// truthful and not less: a highlight that survives on a wide-gamut panel
|
||||||
|
/// and clips on the laptop's screen genuinely is two different facts, and
|
||||||
|
/// the histogram now reports whichever one the photographer is looking at. And when the view is zoomed
|
||||||
/// or cropped it describes the visible region, not the whole file: a
|
/// or cropped it describes the visible region, not the whole file: a
|
||||||
/// photographer inspecting a highlight at 4× is asking about *that*
|
/// photographer inspecting a highlight at 4× is asking about *that*
|
||||||
/// highlight, and a histogram of the parts of the frame off screen would
|
/// highlight, and a histogram of the parts of the frame off screen would
|
||||||
@@ -5407,4 +5465,111 @@ mod tests {
|
|||||||
session.set_active_tab(99);
|
session.set_active_tab(99);
|
||||||
assert_eq!(session.rows().len(), all);
|
assert_eq!(session.rows().len(), all);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Per-display colour (FR-DSP-8)
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8 | FR-DSP-6
|
||||||
|
/// The canvas is encoded for the display, not always for sRGB.
|
||||||
|
///
|
||||||
|
/// This is the assertion the requirement is actually about. Before it,
|
||||||
|
/// `render` composed `ColourSpace::Srgb` unconditionally, and a second
|
||||||
|
/// monitor with a different profile got sRGB pixels *labelled* as its own
|
||||||
|
/// space by the compositor — the silent wrongness FR-DSP-8 calls a
|
||||||
|
/// correctness defect. If someone re-hardcodes the space, these pixels
|
||||||
|
/// stop differing and this fails.
|
||||||
|
///
|
||||||
|
/// A saturated red is the probe deliberately: it sits near the edge of
|
||||||
|
/// sRGB's gamut, so re-encoding it into a wider one moves it a long way,
|
||||||
|
/// where a mid grey would move by almost nothing in any of the four and
|
||||||
|
/// the test would pass on a broken build.
|
||||||
|
#[test]
|
||||||
|
fn the_canvas_is_encoded_for_the_display_showing_it() {
|
||||||
|
let Some(ctx) = headless() else { return };
|
||||||
|
let rgba: Vec<u8> = (0..64 * 64).flat_map(|_| [230u8, 20, 20, 255]).collect();
|
||||||
|
let mut session =
|
||||||
|
DevelopSession::open_rgb(&ctx, &rgba, 64, 64, dr_types::Orientation::NORMAL)
|
||||||
|
.expect("session");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
session.display_space(),
|
||||||
|
dr_types::ColourSpace::Srgb,
|
||||||
|
"a session starts on the fallback, so nothing changes for a \
|
||||||
|
desktop whose display server will not say otherwise"
|
||||||
|
);
|
||||||
|
let on_srgb = read_back(&ctx, &session.render(64, 64).expect("render"));
|
||||||
|
|
||||||
|
assert!(session.set_display_space(dr_types::ColourSpace::AdobeRgb));
|
||||||
|
let on_wide = read_back(&ctx, &session.render(64, 64).expect("render"));
|
||||||
|
|
||||||
|
assert_ne!(
|
||||||
|
on_srgb, on_wide,
|
||||||
|
"the same edit rendered for two displays produced the same pixels"
|
||||||
|
);
|
||||||
|
|
||||||
|
// And back again, because a photographer dragging a window between
|
||||||
|
// two monitors expects the first one to look as it did rather than to
|
||||||
|
// accumulate a transform.
|
||||||
|
assert!(session.set_display_space(dr_types::ColourSpace::Srgb));
|
||||||
|
let returned = read_back(&ctx, &session.render(64, 64).expect("render"));
|
||||||
|
assert_eq!(on_srgb, returned);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// A move that changes nothing reports nothing, so nothing is redrawn.
|
||||||
|
///
|
||||||
|
/// The window's position is polled twice a second and two displays often
|
||||||
|
/// share a profile. A setter that reported a change every time it was
|
||||||
|
/// called would turn that poll into a redraw loop.
|
||||||
|
#[test]
|
||||||
|
fn setting_the_same_display_space_twice_is_not_a_change() {
|
||||||
|
let Some(ctx) = headless() else { return };
|
||||||
|
let rgba: Vec<u8> = (0..8 * 8).flat_map(|_| [128u8, 128, 128, 255]).collect();
|
||||||
|
let mut session =
|
||||||
|
DevelopSession::open_rgb(&ctx, &rgba, 8, 8, dr_types::Orientation::NORMAL)
|
||||||
|
.expect("session");
|
||||||
|
|
||||||
|
assert!(session.set_display_space(dr_types::ColourSpace::DisplayP3));
|
||||||
|
assert!(!session.set_display_space(dr_types::ColourSpace::DisplayP3));
|
||||||
|
assert_eq!(session.display_space(), dr_types::ColourSpace::DisplayP3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8 | FR-EXP-2
|
||||||
|
/// The display's space is the *canvas's*, and reaches nothing else.
|
||||||
|
///
|
||||||
|
/// A thumbnail goes into a shard that syncs between devices and an export
|
||||||
|
/// claims the space the export dialogue asked for. Letting the monitor in
|
||||||
|
/// front of the photographer decide either would write a file whose
|
||||||
|
/// profile describes the desk it was made at.
|
||||||
|
#[test]
|
||||||
|
fn a_wide_gamut_monitor_does_not_reach_the_thumbnail_or_the_export() {
|
||||||
|
let Some(ctx) = headless() else { return };
|
||||||
|
let rgba: Vec<u8> = (0..32 * 32).flat_map(|_| [230u8, 20, 20, 255]).collect();
|
||||||
|
let mut session =
|
||||||
|
DevelopSession::open_rgb(&ctx, &rgba, 32, 32, dr_types::Orientation::NORMAL)
|
||||||
|
.expect("session");
|
||||||
|
|
||||||
|
let thumb_before = session.render_thumbnail(16).expect("thumbnail");
|
||||||
|
let export_before = session
|
||||||
|
.render_for_export(dr_types::ColourSpace::Srgb)
|
||||||
|
.expect("export")
|
||||||
|
.rgba;
|
||||||
|
|
||||||
|
session.set_display_space(dr_types::ColourSpace::ProPhoto);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
session.render_thumbnail(16).expect("thumbnail"),
|
||||||
|
thumb_before,
|
||||||
|
"the grid's thumbnail followed the monitor"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
session
|
||||||
|
.render_for_export(dr_types::ColourSpace::Srgb)
|
||||||
|
.expect("export")
|
||||||
|
.rgba,
|
||||||
|
export_before,
|
||||||
|
"an sRGB export followed the monitor"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
//! TRACES: FR-DSP-8
|
||||||
|
//! Following the canvas from one display to the next.
|
||||||
|
//!
|
||||||
|
//! `dr_plat::display` establishes what each display *is*. This decides which
|
||||||
|
//! of them is showing the canvas right now, keeps the develop session's output
|
||||||
|
//! space pointed at it, and renders at that display's physical pixel size
|
||||||
|
//! rather than its logical one.
|
||||||
|
//!
|
||||||
|
//! # Why a poll and not a callback
|
||||||
|
//!
|
||||||
|
//! Slint reports a window's size and its scale factor and does not report that
|
||||||
|
//! it has moved — there is no `on_moved`, on any backend. What there is, is
|
||||||
|
//! `Window::position()`, so this samples it. A sample is three integers
|
||||||
|
//! compared against three integers and happens twice a second; a monitor is
|
||||||
|
//! changed at human speed, and nothing here runs on the frame path.
|
||||||
|
//!
|
||||||
|
//! The re-survey is what costs something — a Wayland roundtrip pair, or an X11
|
||||||
|
//! property fetch — so it is deliberately *not* done every tick. It runs when
|
||||||
|
//! the geometry actually changed, which is also the moment a monitor could
|
||||||
|
//! have been plugged in or unplugged: both of those move or rescale the window
|
||||||
|
//! on every desktop that does anything sensible.
|
||||||
|
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use dr_plat::DisplaySurvey;
|
||||||
|
use slint::ComponentHandle;
|
||||||
|
|
||||||
|
use crate::AppWindow;
|
||||||
|
|
||||||
|
/// How often the window's geometry is sampled.
|
||||||
|
const POLL: std::time::Duration = std::time::Duration::from_millis(500);
|
||||||
|
|
||||||
|
/// The render-target size for a canvas that occupies `logical` logical pixels
|
||||||
|
/// on a display scaled by `scale`.
|
||||||
|
///
|
||||||
|
/// # FR-DSP-8's scaling clause, in one multiplication
|
||||||
|
///
|
||||||
|
/// "Fractional and mixed DPI scaling are handled without resampling artefacts
|
||||||
|
/// in the canvas." The canvas is a `wgpu::Texture` handed straight to the
|
||||||
|
/// compositor (ARCH §6.1), which draws it into a box measured in *logical*
|
||||||
|
/// pixels. Render 1600 logical pixels wide onto a display at 1.25 and the
|
||||||
|
/// compositor has 1600 samples to fill 2000 device pixels, so every frame is
|
||||||
|
/// resampled up by a quarter — a softness that reads like a bad demosaic
|
||||||
|
/// rather than like a scaling bug, which is exactly why the requirement calls
|
||||||
|
/// it out and why it is worth an explicit test.
|
||||||
|
///
|
||||||
|
/// Rendering the physical count instead makes the presentation 1:1. It costs
|
||||||
|
/// what the extra pixels cost — 56% more work at 1.25 — and that cost is the
|
||||||
|
/// requirement: the alternative is not cheaper, it is blurrier.
|
||||||
|
///
|
||||||
|
/// A `scale` of zero or worse is clamped rather than trusted. It arrives from
|
||||||
|
/// the windowing system, and a zero would collapse the render target to one
|
||||||
|
/// pixel and blank the canvas.
|
||||||
|
pub(crate) fn physical(logical: (u32, u32), scale: f32) -> (u32, u32) {
|
||||||
|
let scale = if scale.is_finite() && scale > 0.01 {
|
||||||
|
scale
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let convert = |v: u32| ((v as f32) * scale).round().max(1.0) as u32;
|
||||||
|
(convert(logical.0), convert(logical.1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The geometry the window had when the display was last resolved.
|
||||||
|
///
|
||||||
|
/// Position *and* scale factor, because either changing can mean a different
|
||||||
|
/// display: dragging across a seam moves the window, and a compositor that
|
||||||
|
/// rescales in place — a fractional-scaling change, or a monitor unplugged
|
||||||
|
/// from under a window — changes only the second.
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
struct Geometry {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
scale_milli: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Geometry {
|
||||||
|
fn of(window: &AppWindow) -> Self {
|
||||||
|
let position = window.window().position();
|
||||||
|
Self {
|
||||||
|
x: position.x,
|
||||||
|
y: position.y,
|
||||||
|
// Compared as thousandths rather than as a float: this is an
|
||||||
|
// equality test running twice a second, and a scale factor that
|
||||||
|
// differs in its last bit must not read as a display change and
|
||||||
|
// provoke a re-survey on every tick.
|
||||||
|
scale_milli: (window.window().scale_factor().max(0.0) * 1000.0) as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything the window needs to keep the canvas on the right display.
|
||||||
|
pub(crate) struct DisplayWatch {
|
||||||
|
survey: RefCell<DisplaySurvey>,
|
||||||
|
/// Which entry of the survey is showing the canvas.
|
||||||
|
current: Cell<usize>,
|
||||||
|
seen: Cell<Option<Geometry>>,
|
||||||
|
/// The canvas's size in *logical* pixels, as Slint last reported it.
|
||||||
|
///
|
||||||
|
/// Kept so that a scale-factor change can re-derive the physical size
|
||||||
|
/// without waiting for a resize that may never come: moving a window
|
||||||
|
/// between a 1× and a 2× display changes what to render and not how large
|
||||||
|
/// the box is.
|
||||||
|
logical_canvas: Cell<(u32, u32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayWatch {
|
||||||
|
/// Ask the platform once, at startup.
|
||||||
|
pub(crate) fn probe() -> Rc<Self> {
|
||||||
|
let survey = DisplaySurvey::probe();
|
||||||
|
log::info!(
|
||||||
|
"display server: {}, {} display(s)",
|
||||||
|
survey.server.label(),
|
||||||
|
survey.displays.len()
|
||||||
|
);
|
||||||
|
for display in &survey.displays {
|
||||||
|
log::info!(" {}: {}", display.name, display.profile.describe());
|
||||||
|
}
|
||||||
|
Rc::new(Self {
|
||||||
|
survey: RefCell::new(survey),
|
||||||
|
current: Cell::new(0),
|
||||||
|
seen: Cell::new(None),
|
||||||
|
logical_canvas: Cell::new((1024, 768)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The output space the canvas should be encoded into right now.
|
||||||
|
pub(crate) fn space(&self) -> dr_types::ColourSpace {
|
||||||
|
self.survey.borrow().profile(self.current.get()).space
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record the canvas's logical size and return the size to render at.
|
||||||
|
pub(crate) fn canvas_resized(&self, window: &AppWindow, logical: (u32, u32)) -> (u32, u32) {
|
||||||
|
self.logical_canvas.set(logical);
|
||||||
|
physical(logical, window.window().scale_factor())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The size the canvas should be rendered at, from the last known logical
|
||||||
|
/// size and the current scale factor.
|
||||||
|
fn canvas_physical(&self, window: &AppWindow) -> (u32, u32) {
|
||||||
|
physical(self.logical_canvas.get(), window.window().scale_factor())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push the About-page readouts.
|
||||||
|
fn publish(&self, window: &AppWindow) {
|
||||||
|
let Some(readouts) = readouts(&self.survey.borrow(), self.current.get()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
window.set_display_name(readouts.name.into());
|
||||||
|
window.set_display_colour(readouts.colour.into());
|
||||||
|
window.set_display_others(readouts.others.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-ask the platform and re-resolve which display is showing the canvas.
|
||||||
|
///
|
||||||
|
/// Returns whether the output space changed, which is what decides a
|
||||||
|
/// redraw. Nothing is pushed into the develop session from here: the
|
||||||
|
/// session is asked for its space on the way into every render (see
|
||||||
|
/// `render_now` in the crate root), so "the canvas is composed for the
|
||||||
|
/// display showing it" is structural rather than something a callback has
|
||||||
|
/// to remember to do. A session opened while the window sits on the second
|
||||||
|
/// monitor is then right on its first frame, without this having to know
|
||||||
|
/// that a photograph was opened at all.
|
||||||
|
fn resolve(&self, window: &AppWindow) -> bool {
|
||||||
|
let before = self.space();
|
||||||
|
*self.survey.borrow_mut() = DisplaySurvey::probe();
|
||||||
|
|
||||||
|
// The window's *centre*, in the display server's own screen
|
||||||
|
// coordinates. A window straddling a seam is showing more of itself on
|
||||||
|
// one side, and its centre says which — where its top-left corner
|
||||||
|
// would flip the transform as soon as one pixel crossed.
|
||||||
|
let position = window.window().position();
|
||||||
|
let size = window.window().size();
|
||||||
|
let centre_x = position.x.saturating_add_unsigned(size.width / 2);
|
||||||
|
let centre_y = position.y.saturating_add_unsigned(size.height / 2);
|
||||||
|
|
||||||
|
let index = self.survey.borrow().containing(centre_x, centre_y);
|
||||||
|
self.current.set(index);
|
||||||
|
self.publish(window);
|
||||||
|
self.space() != before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The three strings the About page shows about the session's colour path.
|
||||||
|
pub(crate) struct Readouts {
|
||||||
|
pub name: String,
|
||||||
|
pub colour: String,
|
||||||
|
pub others: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// Turn a survey into what the About page says.
|
||||||
|
///
|
||||||
|
/// A free function over the survey rather than a method that writes into the
|
||||||
|
/// window, because the requirement's visibility clause is the part worth
|
||||||
|
/// asserting: FR-DSP-8 asks for a fallback that is *defined*, and a reason
|
||||||
|
/// that reaches the code but never the screen satisfies half of that. This is
|
||||||
|
/// where a test can hold the strings.
|
||||||
|
///
|
||||||
|
/// `None` only for a survey with no displays at all, which `probe` does not
|
||||||
|
/// produce.
|
||||||
|
pub(crate) fn readouts(survey: &DisplaySurvey, index: usize) -> Option<Readouts> {
|
||||||
|
// Clamped rather than indexed: a monitor unplugged between the survey and
|
||||||
|
// this call leaves an index pointing past the end, and the page must
|
||||||
|
// describe *a* display rather than nothing.
|
||||||
|
let index = if index < survey.displays.len() {
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let here = survey.displays.get(index)?;
|
||||||
|
|
||||||
|
// The other monitors, listed rather than hidden. FR-DSP-8 is a
|
||||||
|
// multi-monitor requirement and the failure it names — the second display
|
||||||
|
// showing wrong colours — is invisible from the first, so a page that
|
||||||
|
// described only the display it was being read on would report nothing
|
||||||
|
// about the case that matters.
|
||||||
|
let others: Vec<String> = survey
|
||||||
|
.displays
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| *i != index)
|
||||||
|
.map(|(_, d)| format!("{}: {}", d.name, d.profile.describe()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(Readouts {
|
||||||
|
name: format!("{} ({})", here.name, survey.server.label()),
|
||||||
|
colour: here.profile.describe(),
|
||||||
|
others: others.join(" · "),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start following the window's display, and answer once immediately.
|
||||||
|
///
|
||||||
|
/// `redraw` is the window's ordinary re-render, so a display change costs
|
||||||
|
/// exactly one recomposition and one dispatch — the property the fused design
|
||||||
|
/// was always going to give us here (`compose_with_framing`'s output space is
|
||||||
|
/// a parameter, and enters the structure hash).
|
||||||
|
pub(crate) fn attach(
|
||||||
|
window: &AppWindow,
|
||||||
|
watch: &Rc<DisplayWatch>,
|
||||||
|
viewport: &Rc<RefCell<(u32, u32)>>,
|
||||||
|
redraw: Rc<dyn Fn(&AppWindow)>,
|
||||||
|
) {
|
||||||
|
// The first answer, before any frame is drawn. Without it the canvas's
|
||||||
|
// first render is sRGB on every desktop and corrects itself half a second
|
||||||
|
// later, which on a wide-gamut panel is a visible flash of the wrong
|
||||||
|
// colour on every image opened.
|
||||||
|
watch.seen.set(Some(Geometry::of(window)));
|
||||||
|
watch.resolve(window);
|
||||||
|
|
||||||
|
let timer = slint::Timer::default();
|
||||||
|
let weak = window.as_weak();
|
||||||
|
let watch = watch.clone();
|
||||||
|
let viewport = viewport.clone();
|
||||||
|
timer.start(slint::TimerMode::Repeated, POLL, move || {
|
||||||
|
let Some(window) = weak.upgrade() else { return };
|
||||||
|
let now = Geometry::of(&window);
|
||||||
|
if watch.seen.get() == Some(now) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
watch.seen.set(Some(now));
|
||||||
|
|
||||||
|
// The physical size follows the scale factor, so a move onto a
|
||||||
|
// differently-scaled display changes what to render as well as what
|
||||||
|
// to render it into (FR-DSP-8's two halves arriving together).
|
||||||
|
let size = watch.canvas_physical(&window);
|
||||||
|
let resized = *viewport.borrow() != size;
|
||||||
|
if resized {
|
||||||
|
*viewport.borrow_mut() = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if watch.resolve(&window) || resized {
|
||||||
|
redraw(&window);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// The timer stops when it is dropped, and it must outlive this function.
|
||||||
|
// Leaked deliberately rather than threaded through the window's state:
|
||||||
|
// it lives exactly as long as the process, and there is nothing that
|
||||||
|
// could sensibly stop it.
|
||||||
|
std::mem::forget(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use dr_plat::{Bounds, DisplayInfo, DisplayProfile, DisplayServer, FallbackReason};
|
||||||
|
|
||||||
|
fn two_monitors() -> DisplaySurvey {
|
||||||
|
DisplaySurvey {
|
||||||
|
server: DisplayServer::X11,
|
||||||
|
displays: vec![
|
||||||
|
DisplayInfo {
|
||||||
|
name: "eDP-1".to_string(),
|
||||||
|
bounds: Some(Bounds {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 1920,
|
||||||
|
height: 1200,
|
||||||
|
}),
|
||||||
|
profile: DisplayProfile::fallback(FallbackReason::NoWaylandProtocol),
|
||||||
|
},
|
||||||
|
DisplayInfo {
|
||||||
|
name: "DP-2".to_string(),
|
||||||
|
bounds: Some(Bounds {
|
||||||
|
x: 1920,
|
||||||
|
y: 0,
|
||||||
|
width: 2560,
|
||||||
|
height: 1440,
|
||||||
|
}),
|
||||||
|
profile: DisplayProfile {
|
||||||
|
space: dr_types::ColourSpace::DisplayP3,
|
||||||
|
source: dr_plat::ProfileSource::X11RootProperty(
|
||||||
|
"_ICC_PROFILE_1".to_string(),
|
||||||
|
),
|
||||||
|
described_as: Some("EIZO CG279X".to_string()),
|
||||||
|
approximated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// The fallback is defined *and visible*, which is what the requirement
|
||||||
|
/// asks for and the half that is easy to leave out.
|
||||||
|
#[test]
|
||||||
|
fn the_about_page_says_which_acquisition_path_the_session_is_on() {
|
||||||
|
let survey = two_monitors();
|
||||||
|
let shown = readouts(&survey, 0).expect("a display");
|
||||||
|
|
||||||
|
assert!(shown.name.contains("eDP-1"), "{}", shown.name);
|
||||||
|
assert!(shown.name.contains("X11"), "{}", shown.name);
|
||||||
|
// A photographer being shown sRGB because the compositor would not say
|
||||||
|
// otherwise must be able to find that out rather than wonder.
|
||||||
|
assert!(shown.colour.contains("sRGB"), "{}", shown.colour);
|
||||||
|
assert!(shown.colour.contains("assumed"), "{}", shown.colour);
|
||||||
|
assert!(
|
||||||
|
shown.colour.contains("colour management"),
|
||||||
|
"the reason for the fallback did not reach the page: {}",
|
||||||
|
shown.colour
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// The second monitor is described on the page read on the first.
|
||||||
|
///
|
||||||
|
/// The requirement's whole subject is the display the reader is *not*
|
||||||
|
/// looking at: "showing wrong colours on the second display is a
|
||||||
|
/// correctness defect". A page that listed only the current one would say
|
||||||
|
/// nothing about the case it exists for.
|
||||||
|
#[test]
|
||||||
|
fn the_other_monitor_is_named_on_the_page_read_from_this_one() {
|
||||||
|
let survey = two_monitors();
|
||||||
|
let shown = readouts(&survey, 0).expect("a display");
|
||||||
|
assert!(shown.others.contains("DP-2"), "{}", shown.others);
|
||||||
|
assert!(shown.others.contains("Display P3"), "{}", shown.others);
|
||||||
|
// Approximated, and saying so. An honest approximation the user can
|
||||||
|
// see is the whole trade made in `dr_plat::display`.
|
||||||
|
assert!(shown.others.contains("nearest to"), "{}", shown.others);
|
||||||
|
assert!(shown.others.contains("EIZO CG279X"), "{}", shown.others);
|
||||||
|
|
||||||
|
// And from the second monitor's point of view, the first is the other.
|
||||||
|
let shown = readouts(&survey, 1).expect("a display");
|
||||||
|
assert!(shown.colour.contains("Display P3"), "{}", shown.colour);
|
||||||
|
assert!(shown.others.contains("eDP-1"), "{}", shown.others);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single-monitor desktop leaves the row empty rather than repeating
|
||||||
|
/// itself, which is what makes the row conditional in `settings.slint`.
|
||||||
|
#[test]
|
||||||
|
fn one_display_has_no_others_to_list() {
|
||||||
|
let survey = DisplaySurvey::assumed_srgb(FallbackReason::NoWaylandProtocol);
|
||||||
|
let shown = readouts(&survey, 0).expect("a display");
|
||||||
|
assert!(shown.others.is_empty(), "{}", shown.others);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// Fractional scaling renders more pixels, not the same pixels stretched.
|
||||||
|
#[test]
|
||||||
|
fn a_fractionally_scaled_canvas_is_rendered_at_its_physical_size() {
|
||||||
|
// GNOME's fractional steps, against a canvas of a plausible size. The
|
||||||
|
// failure being guarded is silent: at 1.25 the compositor is handed
|
||||||
|
// 1600 samples for 2000 device pixels and the photograph goes soft.
|
||||||
|
assert_eq!(physical((1600, 1000), 1.25), (2000, 1250));
|
||||||
|
assert_eq!(physical((1600, 1000), 1.5), (2400, 1500));
|
||||||
|
assert_eq!(physical((1600, 1000), 1.75), (2800, 1750));
|
||||||
|
assert_eq!(physical((1600, 1000), 2.0), (3200, 2000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unscaled_display_renders_exactly_what_it_is_asked_for() {
|
||||||
|
// The other half of the same requirement: 1:1 must stay 1:1 rather
|
||||||
|
// than acquiring a rounding error that resamples every frame by a
|
||||||
|
// fraction of a pixel.
|
||||||
|
assert_eq!(physical((1923, 1081), 1.0), (1923, 1081));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_nonsense_scale_factor_does_not_blank_the_canvas() {
|
||||||
|
// It comes from the windowing system, and a zero would collapse the
|
||||||
|
// render target to a single pixel with no error anywhere.
|
||||||
|
assert_eq!(physical((800, 600), 0.0), (800, 600));
|
||||||
|
assert_eq!(physical((800, 600), f32::NAN), (800, 600));
|
||||||
|
assert_eq!(physical((800, 600), -2.0), (800, 600));
|
||||||
|
// And a canvas that has been collapsed to nothing by a dragged
|
||||||
|
// splitter still has to produce a renderable target.
|
||||||
|
assert_eq!(physical((0, 0), 2.0), (1, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-1
@@ -23,6 +23,7 @@ mod activity;
|
|||||||
mod collections_ui;
|
mod collections_ui;
|
||||||
mod derived_sync;
|
mod derived_sync;
|
||||||
mod develop;
|
mod develop;
|
||||||
|
mod display_ui;
|
||||||
mod export;
|
mod export;
|
||||||
pub mod faces;
|
pub mod faces;
|
||||||
mod gradient;
|
mod gradient;
|
||||||
@@ -1377,8 +1378,20 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
|||||||
let rows: Rc<slint::VecModel<ParamRow>> = Rc::new(slint::VecModel::default());
|
let rows: Rc<slint::VecModel<ParamRow>> = Rc::new(slint::VecModel::default());
|
||||||
window.set_adjust_rows(rows.clone().into());
|
window.set_adjust_rows(rows.clone().into());
|
||||||
// Viewport size, tracked so a re-render after a slider move matches it.
|
// Viewport size, tracked so a re-render after a slider move matches it.
|
||||||
|
//
|
||||||
|
// In *physical* pixels, which is what the render target wants and what
|
||||||
|
// FR-DSP-8 means by handling fractional scaling without resampling. The
|
||||||
|
// one caller that thinks in logical pixels is Slint's resize callback,
|
||||||
|
// and it converts on the way in.
|
||||||
let viewport = Rc::new(RefCell::new((1024u32, 768u32)));
|
let viewport = Rc::new(RefCell::new((1024u32, 768u32)));
|
||||||
|
|
||||||
|
// TRACES: FR-DSP-8
|
||||||
|
// What the displays are, and which one the canvas is on. Probed once here
|
||||||
|
// so that the About page can describe the session's colour path before any
|
||||||
|
// photograph is opened — the acquisition path is a property of the desktop
|
||||||
|
// and not of the image.
|
||||||
|
let display = display_ui::DisplayWatch::probe();
|
||||||
|
|
||||||
// Re-render the current session into the canvas.
|
// Re-render the current session into the canvas.
|
||||||
//
|
//
|
||||||
// Called on every slider change, so it must do no more than run the
|
// Called on every slider change, so it must do no more than run the
|
||||||
@@ -1393,10 +1406,26 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
|||||||
let session = session.clone();
|
let session = session.clone();
|
||||||
let viewport = viewport.clone();
|
let viewport = viewport.clone();
|
||||||
let drawn_history = drawn_history.clone();
|
let drawn_history = drawn_history.clone();
|
||||||
|
let display = display.clone();
|
||||||
Rc::new(move |window: &AppWindow, draft: bool| {
|
Rc::new(move |window: &AppWindow, draft: bool| {
|
||||||
let mut slot = session.borrow_mut();
|
let mut slot = session.borrow_mut();
|
||||||
let Some(s) = slot.as_mut() else { return };
|
let Some(s) = slot.as_mut() else { return };
|
||||||
|
|
||||||
|
// TRACES: FR-DSP-8 | FR-DSP-6
|
||||||
|
// **Every canvas render is encoded for the display showing it.**
|
||||||
|
//
|
||||||
|
// Set here rather than pushed from the display watch, and rather
|
||||||
|
// than set once when a photograph is opened, because this is the
|
||||||
|
// one path every frame takes. A session opened while the window
|
||||||
|
// sits on the second monitor is then correct on its *first* frame
|
||||||
|
// — where a push would leave it sRGB until the next poll, which is
|
||||||
|
// a visible flash of the wrong colour on every image opened.
|
||||||
|
//
|
||||||
|
// Free when nothing has changed: the field is compared before it
|
||||||
|
// is written, and an unchanged output space composes to the same
|
||||||
|
// structure hash and the same cached pipeline.
|
||||||
|
s.set_display_space(display.space());
|
||||||
|
|
||||||
// TRACES: FR-DEV-5
|
// TRACES: FR-DEV-5
|
||||||
// Whether undo has anywhere to go, pushed from here because every
|
// Whether undo has anywhere to go, pushed from here because every
|
||||||
// edit ends in a redraw and nothing else is on all of their paths:
|
// edit ends in a redraw and nothing else is on all of their paths:
|
||||||
@@ -2570,13 +2599,22 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
|||||||
|
|
||||||
// Track the canvas size so the adjust pass renders at viewport
|
// Track the canvas size so the adjust pass renders at viewport
|
||||||
// resolution rather than sensor resolution (FR-DSP-1).
|
// resolution rather than sensor resolution (FR-DSP-1).
|
||||||
|
//
|
||||||
|
// TRACES: FR-DSP-8
|
||||||
|
// Slint reports the canvas in *logical* pixels, which is the box the
|
||||||
|
// compositor will draw into and not the number of device pixels it will
|
||||||
|
// fill. `display_ui::physical` converts, so that a fractionally scaled
|
||||||
|
// desktop is presented 1:1 rather than resampled — see that function for
|
||||||
|
// why a soft canvas is the failure being avoided here.
|
||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let viewport = viewport.clone();
|
let viewport = viewport.clone();
|
||||||
let redraw = redraw.clone();
|
let redraw = redraw.clone();
|
||||||
|
let display = display.clone();
|
||||||
window.on_canvas_resized(move |w_px, h_px| {
|
window.on_canvas_resized(move |w_px, h_px| {
|
||||||
let Some(w) = weak.upgrade() else { return };
|
let Some(w) = weak.upgrade() else { return };
|
||||||
let size = (w_px.max(1) as u32, h_px.max(1) as u32);
|
let logical = (w_px.max(1) as u32, h_px.max(1) as u32);
|
||||||
|
let size = display.canvas_resized(&w, logical);
|
||||||
if *viewport.borrow() == size {
|
if *viewport.borrow() == size {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2585,6 +2623,10 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TRACES: FR-DSP-8 | FR-DSP-6
|
||||||
|
// And which display that canvas is on, from now until the window closes.
|
||||||
|
display_ui::attach(&window, &display, &viewport, redraw.clone());
|
||||||
|
|
||||||
// FR-UI-1: layout class from window width. Computed here rather than in
|
// FR-UI-1: layout class from window width. Computed here rather than in
|
||||||
// Slint because a property that both derives from and feeds the layout is
|
// Slint because a property that both derives from and feeds the layout is
|
||||||
// a binding loop.
|
// a binding loop.
|
||||||
|
|||||||
+18
-5
@@ -275,6 +275,13 @@ export component AppWindow inherits Window {
|
|||||||
in property <string> adapter: "detecting…";
|
in property <string> adapter: "detecting…";
|
||||||
in property <string> backend: "—";
|
in property <string> backend: "—";
|
||||||
in property <int> fps: 0;
|
in property <int> fps: 0;
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// The display showing the canvas and the colour transform it is getting.
|
||||||
|
/// Shown under ABOUT in Settings; see `settings.slint` for why the second
|
||||||
|
/// one is a sentence rather than a name.
|
||||||
|
in property <string> display-name: "detecting…";
|
||||||
|
in property <string> display-colour: "detecting…";
|
||||||
|
in property <string> display-others: "";
|
||||||
/// TRACES: FR-UI-2
|
/// TRACES: FR-UI-2
|
||||||
/// Set from `CARGO_PKG_VERSION`, so the About line cannot disagree with
|
/// Set from `CARGO_PKG_VERSION`, so the About line cannot disagree with
|
||||||
/// the binary it is part of.
|
/// the binary it is part of.
|
||||||
@@ -1267,6 +1274,9 @@ in property <bool> panel-visible: true;
|
|||||||
fps: root.fps;
|
fps: root.fps;
|
||||||
layout-class: root.layout-class;
|
layout-class: root.layout-class;
|
||||||
app-version: root.app-version;
|
app-version: root.app-version;
|
||||||
|
display-name: root.display-name;
|
||||||
|
display-colour: root.display-colour;
|
||||||
|
display-others: root.display-others;
|
||||||
|
|
||||||
original-budget: root.settings-original-budget;
|
original-budget: root.settings-original-budget;
|
||||||
original-unlimited: root.settings-original-unlimited;
|
original-unlimited: root.settings-original-unlimited;
|
||||||
@@ -1700,11 +1710,14 @@ in property <bool> panel-visible: true;
|
|||||||
// that far. Below 1:1 it stays smooth, where filtering is
|
// that far. Below 1:1 it stays smooth, where filtering is
|
||||||
// what keeps the image from aliasing.
|
// what keeps the image from aliasing.
|
||||||
//
|
//
|
||||||
// This matters even at moderate zoom on a HiDPI display:
|
// TRACES: FR-DSP-8
|
||||||
// the canvas is rendered at *logical* size and Slint scales
|
// The canvas is rendered at the *physical* pixel size of
|
||||||
// it up by the device pixel ratio, so the buffer is
|
// the box this image occupies, so `contain` presents it
|
||||||
// resampled on its way to the screen whatever the pipeline
|
// 1:1 and neither filter is reached at all until the
|
||||||
// did.
|
// photographer zooms. That is the point: on a fractionally
|
||||||
|
// scaled desktop the buffer used to be a logical-sized one
|
||||||
|
// that the compositor stretched, and no choice of filter
|
||||||
|
// recovers detail that was never rendered.
|
||||||
image-rendering: root.magnified
|
image-rendering: root.magnified
|
||||||
? ImageRendering.pixelated
|
? ImageRendering.pixelated
|
||||||
: ImageRendering.smooth;
|
: ImageRendering.smooth;
|
||||||
|
|||||||
@@ -117,6 +117,25 @@ export component SettingsPage inherits Rectangle {
|
|||||||
in property <string> layout-class;
|
in property <string> layout-class;
|
||||||
in property <string> app-version;
|
in property <string> app-version;
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-8
|
||||||
|
/// The display showing the canvas, and the colour it is being given.
|
||||||
|
///
|
||||||
|
/// These are the same kind of thing as the two above — a fact about the
|
||||||
|
/// session, not a setting — with one difference that earns them their own
|
||||||
|
/// row rather than a line in a log. FR-DSP-8's fallback is *defined* to be
|
||||||
|
/// sRGB where the display server will not say otherwise, and a photographer
|
||||||
|
/// being shown sRGB because their compositor has no colour-management
|
||||||
|
/// protocol has no other way to find that out. `display-colour` says which
|
||||||
|
/// of the acquisition paths this session is on, in words.
|
||||||
|
in property <string> display-name;
|
||||||
|
in property <string> display-colour;
|
||||||
|
/// The *other* monitors, where there are any. Empty on a single-display
|
||||||
|
/// desktop, which is why the row below is conditional: FR-DSP-8 is about
|
||||||
|
/// the second display, and the failure it names is invisible from the
|
||||||
|
/// first, so this page has to be readable about a monitor the reader is
|
||||||
|
/// not currently looking at.
|
||||||
|
in property <string> display-others;
|
||||||
|
|
||||||
callback original-budget-changed(string);
|
callback original-budget-changed(string);
|
||||||
callback original-unlimited-toggled(bool);
|
callback original-unlimited-toggled(bool);
|
||||||
callback thumbnail-budget-changed(string);
|
callback thumbnail-budget-changed(string);
|
||||||
@@ -708,10 +727,46 @@ export component SettingsPage inherits Rectangle {
|
|||||||
Value { text: root.layout-class; horizontal-stretch: 1; }
|
Value { text: root.layout-class; horizontal-stretch: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TRACES: FR-DSP-8
|
||||||
|
// Which display, and what colour it is being sent.
|
||||||
|
HorizontalLayout {
|
||||||
|
spacing: Theme.gap;
|
||||||
|
Label { text: "Display"; }
|
||||||
|
Value {
|
||||||
|
text: root.display-name;
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
spacing: Theme.gap;
|
||||||
|
Label { text: "Display colour"; }
|
||||||
|
Value {
|
||||||
|
text: root.display-colour;
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
// Wraps rather than elides: this is the one
|
||||||
|
// value on the page that is a sentence, and
|
||||||
|
// eliding it would cut off the half that says
|
||||||
|
// *why* — which is the half FR-DSP-8 asks for.
|
||||||
|
wrap: word-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.display-others != "": HorizontalLayout {
|
||||||
|
spacing: Theme.gap;
|
||||||
|
Label { text: "Other displays"; }
|
||||||
|
Value {
|
||||||
|
text: root.display-others;
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
wrap: word-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Caption {
|
Caption {
|
||||||
text: "Graphics and frame rate describe this session, "
|
text: "Graphics, frame rate and display colour "
|
||||||
+ "not a setting — they are here so a bug report "
|
+ "describe this session, not a setting — they "
|
||||||
+ "can quote them.";
|
+ "are here so a bug report can quote them.";
|
||||||
wrap: word-wrap;
|
wrap: word-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user