diff --git a/assets/locales/pending/open-progress-steps.json b/assets/locales/pending/open-progress-steps.json new file mode 100644 index 0000000000..5248a4418a --- /dev/null +++ b/assets/locales/pending/open-progress-steps.json @@ -0,0 +1,72 @@ +{ + "en": { + "file_load.step_build": "Building the model", + "file_load.step_feature": "Feature {done} of {total}", + "file_load.step_part": "Part {done} of {total}", + "file_load.step_percent": "{percent}%", + "file_load.step_read": "Reading the file", + "file_load.step_rebuild": "Rebuilding the features", + "file_load.step_repair": "Checking and repairing the parts", + "file_load.step_show": "Preparing the view" + }, + "ar": { + "file_load.step_build": "جارٍ بناء النموذج", + "file_load.step_feature": "الميزة {done} من {total}", + "file_load.step_part": "الجزء {done} من {total}", + "file_load.step_percent": "{percent}%", + "file_load.step_read": "جارٍ قراءة الملف", + "file_load.step_rebuild": "جارٍ إعادة بناء الميزات", + "file_load.step_repair": "جارٍ فحص الأجزاء وإصلاحها", + "file_load.step_show": "جارٍ تجهيز العرض" + }, + "de": { + "file_load.step_build": "Modell wird aufgebaut", + "file_load.step_feature": "Merkmal {done} von {total}", + "file_load.step_part": "Teil {done} von {total}", + "file_load.step_percent": "{percent} %", + "file_load.step_read": "Datei wird gelesen", + "file_load.step_rebuild": "Merkmale werden neu aufgebaut", + "file_load.step_repair": "Teile werden geprüft und repariert", + "file_load.step_show": "Ansicht wird vorbereitet" + }, + "he": { + "file_load.step_build": "בונה את המודל", + "file_load.step_feature": "תכונה {done} מתוך {total}", + "file_load.step_part": "חלק {done} מתוך {total}", + "file_load.step_percent": "{percent}%", + "file_load.step_read": "קורא את הקובץ", + "file_load.step_rebuild": "בונה מחדש את התכונות", + "file_load.step_repair": "בודק ומתקן את החלקים", + "file_load.step_show": "מכין את התצוגה" + }, + "ja": { + "file_load.step_build": "モデルを構築中", + "file_load.step_feature": "フィーチャー {done}/{total}", + "file_load.step_part": "パーツ {done}/{total}", + "file_load.step_percent": "{percent}%", + "file_load.step_read": "ファイルを読み込み中", + "file_load.step_rebuild": "フィーチャーを再構築中", + "file_load.step_repair": "パーツを確認・修復中", + "file_load.step_show": "ビューを準備中" + }, + "ko": { + "file_load.step_build": "모델 만드는 중", + "file_load.step_feature": "피처 {done}/{total}", + "file_load.step_part": "부품 {done}/{total}", + "file_load.step_percent": "{percent}%", + "file_load.step_read": "파일 읽는 중", + "file_load.step_rebuild": "피처 다시 빌드 중", + "file_load.step_repair": "부품 확인 및 복구 중", + "file_load.step_show": "뷰 준비 중" + }, + "zh": { + "file_load.step_build": "正在构建模型", + "file_load.step_feature": "特征 {done}/{total}", + "file_load.step_part": "零件 {done}/{total}", + "file_load.step_percent": "{percent}%", + "file_load.step_read": "正在读取文件", + "file_load.step_rebuild": "正在重建特征", + "file_load.step_repair": "正在检查和修复零件", + "file_load.step_show": "正在准备视图" + } +} diff --git a/crates/ui-egui/src/file_load.rs b/crates/ui-egui/src/file_load.rs index a1b1bb7363..0549ebdd9a 100644 --- a/crates/ui-egui/src/file_load.rs +++ b/crates/ui-egui/src/file_load.rs @@ -979,6 +979,39 @@ pub(crate) fn phase(app: &SolveApp) -> Option<&'static str> { }) } +/// The three steps the card lists for an open or an import (`cad`: a STEP or IGES file), the one +/// under way and how far it is. A STEP file is read, its parts checked and repaired one by one +/// (the kernel's `converting`), then the model is built and shown; a design is read, its +/// features rebuilt, then shown. `work` is the kernel's last report, which stays at its end +/// once the step is over; `features` is (built, total). +fn load_steps( + cad: bool, + phase: u8, + work: Option<(&'static str, usize, usize)>, + features: (usize, usize), +) -> ([&'static str; 3], usize, Option) { + let (done, total) = features; + let labels = if cad { + ["file_load.step_read", "file_load.step_repair", "file_load.step_build"] + } else { + ["file_load.step_read", "file_load.step_rebuild", "file_load.step_show"] + }; + let count = |key: &str, done: usize, total: usize| { + Some(crate::i18n::format(key, &[("done", &(done + 1).min(total).to_string()), ("total", &total.to_string())])) + }; + let percent = |d: usize, t: usize| Some(crate::i18n::format("file_load.step_percent", &[("percent", &(d * 100 / t.max(1)).to_string())])); + let (current, detail) = match (phase, work) { + (1.., _) => (2, None), + (_, Some(("reading", d, t))) if cad => (0, percent(d, t)), + (_, Some((_, d, t))) if cad && d < t => (1, count("file_load.step_part", d, t)), + (_, Some(_)) if cad => (2, None), + _ if total == 0 => (0, None), + _ if cad => (1, None), + _ => (1, count("file_load.step_feature", done, total)), + }; + (labels, current, detail) +} + pub fn show(app: &mut SolveApp, ctx: &egui::Context) { let Some(r) = &app.file_load.running else { return }; let cancelled = r.cancelled(); @@ -1061,29 +1094,40 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { }; ui_kit::text(ui, ¬e, ui_kit::Text::Note("preview")); } - let status = if cancelled && export { - crate::i18n::tr_label("dxf.cancel_wait").to_string() - } else if cancelled { - crate::i18n::tr("file_load.stopping").to_string() + // An open or an import goes through three steps the card lists; the other operations + // (and a cancelled one) say what they are doing in a line. + let stepped = !cancelled && matches!(r.request.kind, Kind::Open | Kind::Insert | Kind::Recover { .. }); + if stepped { + let cad = matches!(r.request.kind, Kind::Open | Kind::Insert) + && (solidcraft_engine::io::is_step_path(&r.request.path) || solidcraft_engine::io::is_iges_path(&r.request.path)); + let (labels, current, detail) = load_steps(cad, phase, r.progress.work(), (done, total)); + let labels = labels.map(crate::i18n::tr); + ui_kit::steps(ui, "file_load", &labels, current, detail.as_deref()); } else { - match (phase, work) { - (_, work) if r.request.kind == Kind::ExportDxf => crate::dialogs_view_dxf::progress_text(work), - (_, Some(("reading", d, t))) => { - crate::i18n::format("file_load.reading_percent", &[("percent", &(d * 100 / t.max(1)).to_string())]) + let status = if cancelled && export { + crate::i18n::tr_label("dxf.cancel_wait").to_string() + } else if cancelled { + crate::i18n::tr("file_load.stopping").to_string() + } else { + match (phase, work) { + (_, work) if r.request.kind == Kind::ExportDxf => crate::dialogs_view_dxf::progress_text(work), + (_, Some(("reading", d, t))) => { + crate::i18n::format("file_load.reading_percent", &[("percent", &(d * 100 / t.max(1)).to_string())]) + } + (_, Some((_, d, t))) => { + crate::i18n::format("file_load.converting", &[("done", &(d + 1).to_string()), ("total", &t.to_string())]) + } + (1, _) => crate::i18n::tr("file_load.preparing_view").to_string(), + (2, _) => crate::i18n::tr("file_load.preparing_thumbnail").to_string(), + _ if total > 0 => crate::i18n::format( + "file_load.rebuilding", + &[("done", &(done + 1).min(total).to_string()), ("total", &total.to_string())], + ), + _ => crate::i18n::tr("file_load.reading").to_string(), } - (_, Some((_, d, t))) => crate::i18n::format("file_load.converting", &[("done", &(d + 1).to_string()), ("total", &t.to_string())]), - (1, _) => crate::i18n::tr("file_load.preparing_view").to_string(), - (2, _) => crate::i18n::tr("file_load.preparing_thumbnail").to_string(), - _ if total > 0 => { - crate::i18n::format("file_load.rebuilding", &[("done", &(done + 1).min(total).to_string()), ("total", &total.to_string())]) - } - _ => crate::i18n::tr("file_load.reading").to_string(), - } - }; - ui.horizontal(|ui| { - ui.spinner(); - ui_kit::text(ui, &status, ui_kit::Text::Report("status", false)); - }); + }; + ui_kit::steps(ui, "file_load", &[status.as_str()], 0, None); + } // Always there, so the card keeps its height as the phases change. let fraction = if cancelled { 0.0 @@ -1120,6 +1164,23 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { mod tests { use super::*; + /// A STEP file goes through its three steps in order as the kernel reports them, and stays + /// on the last once the parts are done; a design's steps follow its features. + #[test] + fn the_card_follows_an_import_through_its_three_steps() { + let step = |phase, work, features| { + let (_, current, detail) = load_steps(true, phase, work, features); + (current, detail) + }; + assert_eq!(step(0, None, (0, 0)), (0, None)); + assert_eq!(step(0, Some(("reading", 40, 80)), (0, 0)), (0, Some("50%".into()))); + assert_eq!(step(0, Some(("converting", 11, 140)), (0, 1)), (1, Some("Part 12 of 140".into()))); + assert_eq!(step(0, Some(("converting", 140, 140)), (0, 1)), (2, None)); + assert_eq!(step(1, Some(("converting", 140, 140)), (1, 1)), (2, None)); + let (labels, current, detail) = load_steps(false, 0, None, (2, 5)); + assert_eq!((labels[1], current, detail.as_deref()), ("file_load.step_rebuild", 1, Some("Feature 3 of 5"))); + } + fn completed_open(app: &mut SolveApp, request: Request, session: Session) { let (tx, rx) = std::sync::mpsc::channel(); tx.send(Ok(Made { session, result: json!({"loaded": true}), thumbnail: false })).unwrap(); diff --git a/crates/ui-egui/src/ui_kit/status.rs b/crates/ui-egui/src/ui_kit/status.rs index b98851908e..8606aab32c 100644 --- a/crates/ui-egui/src/ui_kit/status.rs +++ b/crates/ui-egui/src/ui_kit/status.rs @@ -60,3 +60,49 @@ pub(crate) fn progress(ui: &mut egui::Ui, key: &str, fraction: f32) -> egui::Res crate::scenario::publish_rect(&format!("dialog:progress:{key}"), bar); response } + +/// Steps of a running operation, one row each: a tick for the ones done, a spinner for the one +/// under way and a ring for the ones to come, each the height of the text and centred on it. +/// `detail` (how far the current step is) goes at the end of its row. Pair it with `progress` +/// for the current step. Tokens: `font_label, control_height, form_ink/dim/primary, space_2/8`. +pub(crate) fn steps(ui: &mut egui::Ui, key: &str, labels: &[&str], current: usize, detail: Option<&str>) { + let t = Tokens::get(); + let mark = t.font_label; + for (i, label) in labels.iter().enumerate() { + let (row, response) = + ui.allocate_exact_size(vec2(super::metrics::dimension(ui.available_width()), super::row_height()), egui::Sense::hover()); + let slot = egui::Rect::from_center_size(egui::pos2(row.left() + mark / 2.0, row.center().y), vec2(mark, mark)); + let painter = ui.painter(); + let (font, ink) = match i.cmp(¤t) { + std::cmp::Ordering::Less => { + super::checks::paint_check_glyph(painter, slot, false, t.form_ink); + (egui::FontId::proportional(t.font_label), t.form_ink) + } + std::cmp::Ordering::Equal => { + egui::Spinner::new().size(mark).color(t.form_primary).paint_at(ui, slot); + (crate::fonts::semibold(t.font_label), t.form_ink) + } + std::cmp::Ordering::Greater => { + painter.circle_stroke(slot.center(), mark / 2.0 - t.space_2, egui::Stroke::new(1.0, t.form_dim)); + (egui::FontId::proportional(t.font_label), t.form_dim) + } + }; + let mut text = egui::Rect::from_min_max(egui::pos2(slot.right() + t.space_8, row.top()), row.max); + if let Some(detail) = detail.filter(|_| i == current) { + let galley = ui.painter().layout_no_wrap(detail.to_string(), egui::FontId::proportional(t.font_label), t.form_dim); + let at = egui::pos2(row.right() - galley.size().x, row.center().y - galley.size().y / 2.0); + if at.x > text.left() + t.space_8 { + ui.painter().galley(at, galley, t.form_dim); + text.max.x = at.x - t.space_8; + } + } + super::text::paint_text(ui, text, label, font, ink, false); + let state = match i.cmp(¤t) { + std::cmp::Ordering::Less => "done", + std::cmp::Ordering::Equal => "current", + std::cmp::Ordering::Greater => "next", + }; + response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, ui.is_enabled(), *label)); + crate::scenario::publish_rect(&format!("dialog:step:{key}:{i}:{state}"), row); + } +} diff --git a/docs/ui-kit.md b/docs/ui-kit.md index bb20665c9b..a5506665e8 100644 --- a/docs/ui-kit.md +++ b/docs/ui-kit.md @@ -138,6 +138,7 @@ table. Panel/Window/Confirm compositions also have dedicated geometry and intera | `label_actions` | Clipped row name beside compact actions | `control_height`, form font/spacing tokens | | status: `status_header`, `StatusHeader` | Supplied icon/name/status and constraint meter | `font_label`, `text/dim`, `accent`, sketch/header tokens | | `progress` | Clamped determinate progress | `space_3/6`, `dialog_rule`, `accent` | +| `steps` | A running operation's steps: ticked when done, a text-high spinner on the current one with how far it is, a ring for those to come | `font_label`, `control_height`, `form_ink/dim/primary`, `space_2/8` | History contents are selected from measured panel width. A 56 pt thin strip shows icons and four vertical transport controls: beginning, back, forward and end. The minimum named