LR-BEHAV-BG-TASKS: unknown totals sweep, denoise bars keep their widths, Enter waits for the segmenter

This commit is contained in:
Nathaniel Ledford
2026-10-22 02:02:48 -04:00
parent 316f82dbd2
commit ee2559c60f
6 changed files with 67 additions and 19 deletions
+19 -11
View File
@@ -141,6 +141,22 @@ 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.
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).round() as u32),
None => tr("Starting…").to_string(),
};
crate::progress::Progress::new(fraction).label(text).width(ui.available_width().min(240.0)).widget("denoise:progress").show(ui)
}
/// The bar in Settings, AI Denoise: 380 px wide however wide the tab is.
pub(crate) fn settings_bar(ui: &mut egui::Ui, done: u64, total: u64, text: String) -> egui::Response {
crate::progress::Progress::new(crate::progress::fraction_of(done, total)).label(text).width(380.0).widget("denoise:settingsProgress").show(ui)
}
/// The line under the Denoise slider for the photo `id`, whose Amount is `amount`.
pub fn detail_status(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, amount: f64) {
let t = Tokens::get(ui.ctx());
@@ -198,13 +214,7 @@ pub fn detail_status(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, am
}
PhotoState::Running { done, total } => {
egui::Frame::NONE.inner_margin(pad).show(ui, |ui| {
let frac = if total > 0 { (done as f32 / total as f32).clamp(0.0, 1.0) } else { 0.0 };
let text = if total > 0 {
tr_format!("Making the denoised picture… {}%", (frac * 100.0).round() as u32)
} else {
tr("Starting…").to_string()
};
crate::progress::Progress::new(if total > 0 { Some(frac) } else { None }).label(text).widget("denoise:progress").show(ui);
detail_bar(ui, done as u64, total as u64);
});
}
PhotoState::Failed { why, unsupported } => {
@@ -329,7 +339,6 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
let (running, queued) = (app.caches.denoise.running, app.caches.denoise.queued);
match running {
Some((done, total)) => {
let frac = if total > 0 { (done as f32 / total as f32).clamp(0.0, 1.0) } else { 0.0 };
let name = status["running"]["photo"]
.as_u64()
.and_then(|p| app.session.catalog.photo(PhotoId(p)).map(|p| p.file_name.clone()))
@@ -341,7 +350,7 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
done = done,
total = total
);
crate::progress::Progress::new(Some(frac)).label(text).widget("denoise:settingsProgress").show(ui);
settings_bar(ui, done, total, text);
}
None if queued > 0 => hint(ui, t, &count(queued, "photo waiting", "photos waiting")),
None => {
@@ -445,13 +454,12 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
match (dl_state, dl) {
(Some("running"), Some(d)) => {
let (got, total) = (d["bytes"].as_u64().unwrap_or(0), d["total"].as_u64().unwrap_or(0));
let frac = if total > 0 { (got as f32 / total as f32).clamp(0.0, 1.0) } else { 0.0 };
let text = if total > 0 && got >= total {
tr("Checking…").to_string()
} else {
tr_format!("{} of {} from {host}", mb(Some(got)), mb(Some(total)), host = host)
};
crate::progress::Progress::new(Some(frac))
crate::progress::Progress::new(crate::progress::fraction_of(got, total))
.label(text)
.width(crate::progress::ROW_WIDTH.min(ui.available_width()))
.widget(format!("denoise:progress:{id}"))
+1 -2
View File
@@ -1674,9 +1674,8 @@ fn sam_model_body(app: &mut LightcraftApp, ui: &mut egui::Ui, error: Option<&str
let seg = &app.session.segmenter;
if d.running {
ui.add_space(4.0);
let frac = if d.total > 0 { d.done as f64 / d.total as f64 } else { 0.0 };
let text = format!("{:.2} / {:.2} GB · {}", gb(d.done), gb(d.total), d.file);
crate::progress::Progress::new(Some(frac as f32)).label(text).widget("progress:samDownload").show(ui);
crate::progress::Progress::new(crate::progress::fraction_of(d.done, d.total)).label(text).widget("progress:samDownload").show(ui);
let r = ui.button(crate::i18n::tr("Cancel Download"));
crate::widgets::register(ui.ctx(), "button:samCancel", r.rect);
if r.clicked() {
+1 -2
View File
@@ -361,13 +361,12 @@ fn model_row(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, m: &Value,
match (dl_state, dl) {
(Some("running"), Some(d)) => {
let (got, total) = (d["bytes"].as_u64().unwrap_or(0), d["total"].as_u64().unwrap_or(0));
let frac = if total > 0 { (got as f32 / total as f32).clamp(0.0, 1.0) } else { 0.0 };
let text = if total > 0 && got >= total {
"Checking…".to_string()
} else {
format!("{} of {} from {host}", mb(Some(got)), mb(Some(total)))
};
crate::progress::Progress::new(Some(frac))
crate::progress::Progress::new(crate::progress::fraction_of(got, total))
.label(text)
.width(crate::progress::ROW_WIDTH.min(ui.available_width()))
.widget(format!("faces:progress:{id}"))
+18 -3
View File
@@ -183,8 +183,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
let pct = if download.total > 0 { download.done as f64 / download.total as f64 } else { 0.0 };
// the bar takes what the Details button leaves
let details = ui.spacing().interact_size.x + 30.0;
crate::progress::Progress::new(Some(pct as f32))
.label(format!("SAM 3: {} %", (pct * 100.0).floor()))
crate::progress::Progress::new(crate::progress::fraction_of(download.done, download.total))
// no percentage to say while the size is unknown
.label(if download.total > 0 { format!("SAM 3: {} %", (pct * 100.0).floor()) } else { String::new() })
.width((ui.available_width() - details).max(96.0))
.widget("maskSamProgress")
.show(ui);
@@ -767,6 +768,11 @@ pub(crate) fn start_describe(app: &mut LightcraftApp, op: &str) {
}
/// The Describe field: type what to select ("sky", "the red car") and press Return.
/// Enter in the Describe field is the Select button: it starts nothing while the segmenter works.
fn enter_submits(working: bool, lost_focus: bool, enter_pressed: bool) -> bool {
!working && lost_focus && enter_pressed
}
fn describe_field(app: &mut LightcraftApp, ui: &mut egui::Ui, new_mask: bool) {
let Some((op, mut text)) = app.ui.describe.clone() else { return };
// a new mask's field sits under the tiles; one that combines, under the selected mask
@@ -796,7 +802,8 @@ fn describe_field(app: &mut LightcraftApp, ui: &mut egui::Ui, new_mask: bool) {
if !r.has_focus() && !r.lost_focus() && text.is_empty() {
r.request_focus();
}
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
// Enter is the Select button: nothing starts while the segmenter is working
if enter_submits(working, r.lost_focus(), ui.input(|i| i.key_pressed(egui::Key::Enter))) {
submit = true;
}
});
@@ -904,6 +911,14 @@ fn brush_settings(app: &mut LightcraftApp, ui: &mut egui::Ui) {
mod tests {
use super::*;
#[test]
fn enter_in_the_describe_field_starts_nothing_while_the_segmenter_works() {
assert!(enter_submits(false, true, true));
assert!(!enter_submits(true, true, true));
assert!(!enter_submits(false, false, true));
assert!(!enter_submits(false, true, false));
}
#[test]
fn masking_context_actions_use_catalog_translations() {
let saved = crate::i18n::language();
+6
View File
@@ -39,6 +39,12 @@ pub fn clamp_fraction(fraction: f32) -> f32 {
if fraction.is_nan() { 0.0 } else { fraction.clamp(0.0, 1.0) }
}
/// `done` of `total` as a fraction, or `None` while the total is not known (0): an unknown total
/// is a bar that sweeps, not one stuck at 0 %.
pub fn fraction_of(done: u64, total: u64) -> Option<f32> {
(total > 0).then(|| clamp_fraction(done as f32 / total as f32))
}
/// A progress bar, built and then shown with [`Progress::show`].
#[derive(Clone, Debug)]
pub struct Progress {
+22 -1
View File
@@ -7,7 +7,7 @@ use egui::{Pos2, Rect, pos2, vec2};
use crate::button::{Button, contrast};
use crate::icons::Icon;
use crate::progress::{Progress, clamp_fraction, spinner};
use crate::progress::{Progress, clamp_fraction, fraction_of, spinner};
use crate::theme::{ThemeKind, Tokens};
/// What a screen reader gets for one node.
@@ -65,6 +65,15 @@ fn the_fraction_is_always_between_nothing_and_all() {
assert_eq!(Progress::new(None).fraction(), None);
}
#[test]
fn an_unknown_total_is_an_indeterminate_bar_not_an_empty_one() {
assert_eq!(fraction_of(0, 0), None);
assert_eq!(fraction_of(5, 0), None);
assert_eq!(fraction_of(0, 10), Some(0.0));
assert_eq!(fraction_of(5, 10), Some(0.5));
assert_eq!(fraction_of(20, 10), Some(1.0));
}
#[test]
fn a_bar_is_progress_with_a_value_and_reachable_by_its_id() {
let (nodes, ids, _) = frames(Vec::new(), |ui| {
@@ -197,3 +206,15 @@ fn a_failed_filmstrip_thumbnail_is_announced_with_its_reason() {
assert_eq!(spoken(false).as_deref(), Some("IMG_0001.jpg"));
assert_eq!(spoken(true).as_deref(), Some("IMG_0001.jpg, This photo can't be opened"));
}
#[test]
fn the_denoise_bars_keep_their_widths_in_a_wide_container() {
let (_, ids, (detail, settings, unknown)) = frames(Vec::new(), |ui| {
let detail = crate::panels::denoise::detail_bar(ui, 3, 10).rect.width();
let settings = crate::panels::denoise::settings_bar(ui, 3, 10, "Denoising a.jpg: 3 of 10 tiles".into()).rect.width();
let unknown = crate::panels::denoise::detail_bar(ui, 0, 0).rect.width();
(detail, settings, unknown)
});
assert_eq!((detail, settings, unknown), (240.0, 380.0, 240.0));
assert!(ids.iter().any(|(id, _)| id == "denoise:settingsProgress"));
}