//! TRACES: FR-CULL-3 //! The focus-peaking vocabulary, as the indices a chip row can carry. //! //! `dr_gpu` decides what peaking *is* — the measure, the thresholds, the //! marks. This decides how a menu of three sensitivities and four colours //! crosses the boundary into Slint, which has no notion of a Rust enum and //! carries the choice as an `int` into an array of labels. //! //! That translation is small and it is the kind of small that goes wrong //! silently. An index the interface sends that Rust reads as a different //! variant produces a control that changes something other than what it says, //! which nobody notices as a bug — they notice it as peaking behaving oddly. //! So the order lives in one place here, both directions are asserted to round //! trip, and a test checks that the labels in `ui/peaking.slint` still number //! the same as the vocabularies they claim to name. //! //! Free-standing functions over plain integers, deliberately, for the reason //! `crate::histogram` gives: none of this needs a GPU, a window or a //! photograph to be checked, and all of it is invisible when wrong. use dr_gpu::{PeakColour, PeakSensitivity}; /// The sensitivities, in the order the chip row shows them. /// /// Least sensitive first, so the row reads left to right as "mark less" to /// "mark more" — the axis the photographer is actually moving along. pub(crate) const SENSITIVITIES: [PeakSensitivity; 3] = [ PeakSensitivity::Low, PeakSensitivity::Medium, PeakSensitivity::High, ]; /// The mark colours, in the order the chip row shows them. pub(crate) const COLOURS: [PeakColour; 4] = [ PeakColour::Red, PeakColour::Yellow, PeakColour::Cyan, PeakColour::Magenta, ]; /// The sensitivity an index names. /// /// Out of range falls back to the default rather than panicking. The index /// arrives from the interface, and the interface is the half of this that can /// be recompiled without recompiling the other — a chip row that grew an entry /// should degrade to a sane setting, not take the application down mid-cull. pub(crate) fn sensitivity(index: i32) -> PeakSensitivity { usize::try_from(index) .ok() .and_then(|i| SENSITIVITIES.get(i).copied()) .unwrap_or_default() } /// The colour an index names, on the same terms. pub(crate) fn colour(index: i32) -> PeakColour { usize::try_from(index) .ok() .and_then(|i| COLOURS.get(i).copied()) .unwrap_or_default() } /// Which chip is lit for this sensitivity. pub(crate) fn sensitivity_index(value: PeakSensitivity) -> i32 { SENSITIVITIES.iter().position(|s| *s == value).unwrap_or(0) as i32 } /// Which chip is lit for this colour. pub(crate) fn colour_index(value: PeakColour) -> i32 { COLOURS.iter().position(|c| *c == value).unwrap_or(0) as i32 } #[cfg(test)] mod tests { use super::*; #[test] fn every_variant_appears_exactly_once_in_its_row() { // A variant missing from the row is a setting the photographer cannot // reach; one listed twice is two chips that do the same thing, of // which only the first can ever look selected. Both are invisible in // the running application until somebody presses the wrong chip. for s in SENSITIVITIES { assert_eq!( SENSITIVITIES.iter().filter(|x| **x == s).count(), 1, "{s:?} is listed more than once" ); } for c in COLOURS { assert_eq!(COLOURS.iter().filter(|x| **x == c).count(), 1); } // Named rather than counted, so adding a variant to `dr_gpu` without // adding it here fails to compile instead of passing quietly. assert!(SENSITIVITIES.contains(&PeakSensitivity::Low)); assert!(SENSITIVITIES.contains(&PeakSensitivity::Medium)); assert!(SENSITIVITIES.contains(&PeakSensitivity::High)); assert!(COLOURS.contains(&PeakColour::Red)); assert!(COLOURS.contains(&PeakColour::Yellow)); assert!(COLOURS.contains(&PeakColour::Cyan)); assert!(COLOURS.contains(&PeakColour::Magenta)); } #[test] fn an_index_and_its_variant_agree_in_both_directions() { // The failure this catches is a chip that lights up under the pointer // while a different setting takes effect — the two directions drifting // apart is exactly what one shared array is here to prevent, and the // only way to see it is to go round. for (i, s) in SENSITIVITIES.iter().enumerate() { assert_eq!(sensitivity(i as i32), *s); assert_eq!(sensitivity_index(*s), i as i32); } for (i, c) in COLOURS.iter().enumerate() { assert_eq!(colour(i as i32), *c); assert_eq!(colour_index(*c), i as i32); } } #[test] fn an_index_from_nowhere_lands_on_the_default_rather_than_panicking() { // Slint has no bound on the `int` it sends and Rust has no way to // refuse one. A panic here would be an application that closes because // a chip row was edited. assert_eq!(sensitivity(-1), PeakSensitivity::default()); assert_eq!(sensitivity(99), PeakSensitivity::default()); assert_eq!(colour(-1), PeakColour::default()); assert_eq!(colour(99), PeakColour::default()); } #[test] fn the_panel_offers_exactly_the_choices_this_module_knows_about() { // **The one seam neither compiler checks.** The labels live in // `ui/peaking.slint` and the meanings live here, joined only by an // integer; a fifth colour added to the chip row would send index 4 to // `colour`, which would quietly answer Red. Reading the file is // clumsier than a derive, and it is what there is. let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/ui/peaking.slint")) .expect("the panel this module serves"); let listed = |line_start: &str| -> usize { let line = src .lines() .map(str::trim) .find(|l| l.starts_with(line_start)) .unwrap_or_else(|| panic!("no `{line_start}` row in peaking.slint")); line.matches('"').count() / 2 }; assert_eq!( listed("options: [\"Low\""), SENSITIVITIES.len(), "the sensitivity chips and `SENSITIVITIES` disagree" ); assert_eq!( listed("options: [\"Red\""), COLOURS.len(), "the colour chips and `COLOURS` disagree" ); } }