diff --git a/crates/ui-egui/src/panels/denoise.rs b/crates/ui-egui/src/panels/denoise.rs index 59a54364f..bcd77176a 100644 --- a/crates/ui-egui/src/panels/denoise.rs +++ b/crates/ui-egui/src/panels/denoise.rs @@ -142,12 +142,12 @@ pub fn detail_toggle(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, on } /// The bar under the Denoise slider while a photo's picture is made: at most 240 px wide, and -/// sweeping while the number of tiles is not known yet. The percentage is floored, as the bar's -/// accessible value is (and the SAM bar's text): 100 % only once done. +/// sweeping while the number of tiles is not known yet. The percentage is `progress::percent`, as the +/// bar's accessible value is: 100 % only once done. pub(crate) fn detail_bar(ui: &mut egui::Ui, done: u64, total: u64) -> egui::Response { let fraction = crate::progress::fraction_of(done, total); let text = match fraction { - Some(f) => tr_format!("Making the denoised picture… {}%", (f * 100.0).floor() as u32), + Some(f) => tr_format!("Making the denoised picture… {}%", crate::progress::percent(f)), None => tr("Starting…").to_string(), }; crate::progress::Progress::new(fraction).label(text).width(ui.available_width().min(240.0)).widget("denoise:progress").show(ui) diff --git a/crates/ui-egui/src/panels/masking.rs b/crates/ui-egui/src/panels/masking.rs index 5c1781e82..38d64b6c3 100644 --- a/crates/ui-egui/src/panels/masking.rs +++ b/crates/ui-egui/src/panels/masking.rs @@ -766,7 +766,7 @@ pub(crate) fn start_describe(app: &mut LightcraftApp, op: &str) { pub(crate) fn sam_download_bar(ui: &mut egui::Ui, done: u64, total: u64, width: f32) -> egui::Response { let fraction = crate::progress::fraction_of(done, total); let label = match fraction { - Some(f) => format!("SAM 3: {} %", (f64::from(f) * 100.0).floor()), + Some(f) => format!("SAM 3: {} %", crate::progress::percent(f)), None => "SAM 3".to_string(), }; crate::progress::Progress::new(fraction).label(label).width(width).widget("maskSamProgress").show(ui) diff --git a/crates/ui-egui/src/progress.rs b/crates/ui-egui/src/progress.rs index f80f81783..0f2dce012 100644 --- a/crates/ui-egui/src/progress.rs +++ b/crates/ui-egui/src/progress.rs @@ -45,6 +45,15 @@ pub fn fraction_of(done: u64, total: u64) -> Option { (total > 0).then(|| clamp_fraction(done as f32 / total as f32)) } +/// The whole percent a bar says for `fraction`: rounded down, so 100 comes only at exactly 1.0, but +/// with a little slack so a fraction that is exact in decimals (53 of 100, held as 0.52999997) +/// still says 53. A bar's text and its accessible value both come from here, so they agree. +pub fn percent(fraction: f32) -> u32 { + let f = clamp_fraction(fraction); + let whole = (f64::from(f) * 100.0 + 1e-3).floor() as u32; + if f < 1.0 { whole.min(99) } else { 100 } +} + /// A progress bar, built and then shown with [`Progress::show`]. #[derive(Clone, Debug)] pub struct Progress { @@ -103,7 +112,7 @@ impl Progress { Some(l) => egui::WidgetInfo::labeled(egui::WidgetType::ProgressIndicator, ui.is_enabled(), l), None => egui::WidgetInfo::new(egui::WidgetType::ProgressIndicator), }; - info.value = self.fraction.map(|f| f64::from(f * 100.0).floor()); + info.value = self.fraction.map(|f| f64::from(percent(f))); info }); if let Some(w) = &self.widget { diff --git a/crates/ui-egui/src/tests_progress.rs b/crates/ui-egui/src/tests_progress.rs index 41f779e0e..476b8a1a6 100644 --- a/crates/ui-egui/src/tests_progress.rs +++ b/crates/ui-egui/src/tests_progress.rs @@ -240,13 +240,27 @@ fn the_denoise_bars_keep_their_widths_in_a_wide_container() { #[test] fn the_denoise_bar_label_and_its_accessible_value_agree() { - // 3 of 7 is 42.86 %: the number drawn on the bar is the one announced - let (nodes, _, _) = frames(Vec::new(), |ui| { - crate::panels::denoise::detail_bar(ui, 3, 7); - }); - let bar = nodes.iter().find(|n| n.role == "ProgressIndicator").unwrap_or_else(|| panic!("{nodes:?}")); - let shown: f64 = bar.label.rsplit(' ').next().unwrap().trim_end_matches('%').parse().unwrap(); - assert_eq!(bar.value, Some(shown), "{bar:?}"); + // the number drawn on the bar is the one announced, and exact decimals stay exact + for (done, total, says) in + [(3, 7, 42.0), (53, 100, 53.0), (59, 100, 59.0), (29, 100, 29.0), (99, 100, 99.0), (100, 100, 100.0), (999, 1000, 99.0)] + { + let (nodes, _, _) = frames(Vec::new(), |ui| { + crate::panels::denoise::detail_bar(ui, done, total); + }); + let bar = nodes.iter().find(|n| n.role == "ProgressIndicator").unwrap_or_else(|| panic!("{nodes:?}")); + let shown: f64 = bar.label.rsplit(' ').next().unwrap().trim_end_matches('%').parse().unwrap(); + assert_eq!((shown, bar.value), (says, Some(says)), "{done}/{total}: {bar:?}"); + } +} + +#[test] +fn the_sam_bar_text_and_its_accessible_value_agree() { + for (done, total, says) in [(53, 100, 53.0), (59, 100, 59.0), (29, 100, 29.0), (999, 1000, 99.0), (100, 100, 100.0)] { + let (nodes, _, _) = frames(Vec::new(), |ui| crate::panels::masking::sam_download_bar(ui, done, total, 200.0).rect.width()); + let bar = nodes.iter().find(|n| n.role == "ProgressIndicator").unwrap_or_else(|| panic!("{nodes:?}")); + assert_eq!(bar.label, format!("SAM 3: {says} %"), "{done}/{total}"); + assert_eq!(bar.value, Some(says), "{done}/{total}"); + } } #[test]