Open and import card: three plain steps instead of a status line

An open or an import lists its three steps (a STEP or IGES file: reading the file, checking
and repairing the parts, building the model; a design: reading, rebuilding the features,
preparing the view), the ones done ticked, the current one with a spinner the height of its
text and how far it is, and the bar for that step. The other file operations keep their one
line, led by the same small spinner. New kit control: ui_kit::steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 08:42:59 -04:00
committed by Irakli Kourtis
parent baa44fc64b
commit b7d813bd6c
4 changed files with 201 additions and 21 deletions
@@ -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": "正在准备视图"
}
}
+82 -21
View File
@@ -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<String>) {
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, &note, 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();
+46
View File
@@ -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(&current) {
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(&current) {
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);
}
}
+1
View File
@@ -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