diff --git a/docs/traceability.md b/docs/traceability.md index be39ca3..4944373 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| | Source files scanned | 265 | -| TRACES tags found | 762 | +| TRACES tags found | 765 | | Requirements defined | 177 | | Requirements covered | 102 | | **Coverage** | **57.6%** (102/177) | @@ -73,7 +73,7 @@ _None._ | 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:3694`](../ui/dr-ui/src/develop.rs#L3694), [`ui/dr-ui/src/develop.rs:4177`](../ui/dr-ui/src/develop.rs#L4177), [`ui/dr-ui/src/develop.rs:4211`](../ui/dr-ui/src/develop.rs#L4211), [`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), [`ui/dr-ui/src/develop.rs:2698`](../ui/dr-ui/src/develop.rs#L2698), [`ui/dr-ui/src/develop.rs:5469`](../ui/dr-ui/src/develop.rs#L5469), [`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:2747`](../ui/dr-ui/src/develop.rs#L2747), [`ui/dr-ui/src/develop.rs:5279`](../ui/dr-ui/src/develop.rs#L5279), [`ui/dr-ui/src/develop.rs:5311`](../ui/dr-ui/src/develop.rs#L5311), [`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:5469`](../ui/dr-ui/src/develop.rs#L5469), [`ui/dr-ui/src/develop.rs:5515`](../ui/dr-ui/src/develop.rs#L5515), [`ui/dr-ui/src/develop.rs:5534`](../ui/dr-ui/src/develop.rs#L5534), [`ui/dr-ui/src/develop.rs:689`](../ui/dr-ui/src/develop.rs#L689), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:258`](../ui/dr-ui/src/display_ui.rs#L258), [`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:1708`](../ui/dr-ui/ui/app.slint#L1708), [`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-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:5469`](../ui/dr-ui/src/develop.rs#L5469), [`ui/dr-ui/src/develop.rs:5515`](../ui/dr-ui/src/develop.rs#L5515), [`ui/dr-ui/src/develop.rs:5534`](../ui/dr-ui/src/develop.rs#L5534), [`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:1708`](../ui/dr-ui/ui/app.slint#L1708), [`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-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:5534`](../ui/dr-ui/src/develop.rs#L5534), [`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) | diff --git a/ui/dr-ui/src/display_ui.rs b/ui/dr-ui/src/display_ui.rs index 91eb1a8..012f3f6 100644 --- a/ui/dr-ui/src/display_ui.rs +++ b/ui/dr-ui/src/display_ui.rs @@ -142,32 +142,14 @@ impl DisplayWatch { physical(self.logical_canvas.get(), window.window().scale_factor()) } - /// Push the three About-page readouts. + /// Push the About-page readouts. fn publish(&self, window: &AppWindow) { - let survey = self.survey.borrow(); - let index = self.current.get(); - let here = survey - .displays - .get(index) - .or_else(|| survey.displays.first()); - let Some(here) = here else { return }; - - window.set_display_name(format!("{} ({})", here.name, survey.server.label()).into()); - window.set_display_colour(here.profile.describe().into()); - - // 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 = survey - .displays - .iter() - .enumerate() - .filter(|(i, _)| *i != index) - .map(|(_, d)| format!("{}: {}", d.name, d.profile.describe())) - .collect(); - window.set_display_others(others.join(" · ").into()); + 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. @@ -200,6 +182,55 @@ impl DisplayWatch { } } +/// 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 { + // 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 = 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 @@ -254,6 +285,96 @@ pub(crate) fn attach( #[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.