diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index ae2155419..c57537a32 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -2,7 +2,7 @@ //! and sheet tabs until Esc or the back arrow returns to the sheet. //! //! A navigation column on the left picks a page (Home, New, Open, Info, Save As, Print, Export, -//! Options); Save and Close act at once. Every action runs an existing command through +//! Options, Account); Save and Close act at once. Every action runs an existing command through //! [`SheetApp::run`] or an existing dialog through [`SheetApp::open_dialog`]; the state here is //! UI-only and never saved. Recent workbooks are listed by path only: nothing on disk is touched //! until the user picks one. @@ -31,10 +31,11 @@ pub enum Page { Print, Export, Options, + Account, } impl Page { - pub const ALL: [Page; 8] = [Page::Home, Page::New, Page::Open, Page::Info, Page::SaveAs, Page::Print, Page::Export, Page::Options]; + pub const ALL: [Page; 9] = [Page::Home, Page::New, Page::Open, Page::Info, Page::SaveAs, Page::Print, Page::Export, Page::Options, Page::Account]; /// The id `ui.backstage` and `ui.dialog {"name": "backstage", "page": …}` take. pub fn id(self) -> &'static str { @@ -47,6 +48,7 @@ impl Page { Page::Print => "print", Page::Export => "export", Page::Options => "options", + Page::Account => "account", } } @@ -66,6 +68,7 @@ impl Page { Page::Print => "Print", Page::Export => "Export", Page::Options => "Options", + Page::Account => "Account", } } @@ -79,6 +82,7 @@ impl Page { Page::Print => Icon::Print, Page::Export => Icon::Share, Page::Options => Icon::Settings, + Page::Account => Icon::Info, } } } @@ -174,9 +178,12 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { out.close = true; } } + // Bottom up: Account sits just above Options, as in Excel. ui.with_layout(egui::Layout::bottom_up(egui::Align::Min), |ui| { - if nav_item(ui, Page::Options.icon(), lang.tr("Options"), bs.page == Page::Options, nav_text, nav_sel).clicked() { - out.page = Some(Page::Options); + for page in [Page::Options, Page::Account] { + if nav_item(ui, page.icon(), lang.tr(page.label()), bs.page == page, nav_text, nav_sel).clicked() { + out.page = Some(page); + } } }); }); @@ -195,6 +202,7 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { Page::Print => print(app, ui, &mut bs, &mut out), Page::Export => export(app, ui, &mut out), Page::Options => options(app, ui), + Page::Account => account(app, ui), } }); }); @@ -852,6 +860,51 @@ fn options(app: &mut SheetApp, ui: &mut Ui) { dim(ui, lang.tr("Calculation settings are saved with the workbook.")); } +/// The platform the app was built for, e.g. "macOS (aarch64)"; the web build reports "Web". +fn platform() -> String { + let os = match std::env::consts::OS { + "macos" => "macOS", + "windows" => "Windows", + "linux" => "Linux", + "freebsd" => "FreeBSD", + "openbsd" => "OpenBSD", + "netbsd" => "NetBSD", + _ if cfg!(target_arch = "wasm32") => return "Web".into(), + other => other, + }; + format!("{os} ({})", std::env::consts::ARCH) +} + +/// Excel's File › Account: product information and the About window. +fn account(app: &mut SheetApp, ui: &mut Ui) { + let t = Tokens::get(ui.ctx()); + let lang = app.ui.language; + section(ui, lang, "Product Information"); + ui.label(RichText::new("GridCraft").font(theme::ui_bold(16.0)).color(t.text)); + ui.add_space(8.0); + egui::Grid::new("backstage_account").num_columns(2).spacing(vec2(18.0, 8.0)).show(ui, |ui| { + for (label, value) in + [("Version", env!("CARGO_PKG_VERSION").to_string()), ("Platform", platform()), ("Licence", env!("CARGO_PKG_LICENSE").to_string())] + { + ui.label(RichText::new(lang.tr(label)).color(t.text_dim)); + ui.label(value); + ui.end_row(); + } + }); + ui.add_space(18.0); + ui.horizontal(|ui| { + if square_button(ui, Icon::Info, lang.tr("About GridCraft")).clicked() { + app.open_dialog("about", json!({})); + } + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(14.0); + ui.label(RichText::new(lang.tr("About GridCraft")).font(theme::ui_bold(13.5)).color(t.text)); + dim(ui, lang.tr("Version, contributors and the AI models that helped")); + }); + }); +} + #[cfg(test)] mod tests { use super::*; @@ -862,7 +915,7 @@ mod tests { assert_eq!(Page::parse(page.id()), Some(page)); } assert_eq!(Page::parse(" SAVEAS "), Some(Page::SaveAs)); - assert_eq!(Page::parse("account"), None); + assert_eq!(Page::parse("feedback"), None); assert_eq!(Page::parse(""), None); } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index d27ea052e..54dcadaeb 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -183,7 +183,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - ("view.animations", "Animations", "{on?: bool} (toggles without it) → {on}"), ("view.interfaceScale", "Interface Size", "{percent: 75–200 | \"auto\"} (100 = auto; clamped)"), ("ui.dialog", "Open Dialog", ""), - ("ui.backstage", "File (Backstage view)", "{page?: home|new|open|info|saveAs|print|export|options|close}"), + ("ui.backstage", "File (Backstage view)", "{page?: home|new|open|info|saveAs|print|export|options|account|close}"), ( "ui.printSettings", "Print Settings", diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 4cc6a3e37..f8c318bc2 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -519,6 +519,13 @@ const JAPANESE: &[(&str, &str)] = &[ ("Export", "エクスポート"), ("Close", "閉じる"), ("Options", "オプション"), + ("Account", "アカウント"), + ("Product Information", "製品情報"), + ("About GridCraft", "GridCraft について"), + ("Version", "バージョン"), + ("Platform", "プラットフォーム"), + ("Licence", "ライセンス"), + ("Version, contributors and the AI models that helped", "バージョン、貢献者、支援した AI モデル"), ("Blank workbook", "空白のブック"), ("Recent", "最近使ったアイテム"), ("No recent workbooks yet.", "最近使ったブックはまだありません。"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index bf8c35f9b..81848f599 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -258,6 +258,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportovat", "Close" => "Zavřít", "Options" => "Možnosti", + "Account" => "Účet", + "Product Information" => "Informace o produktu", + "About GridCraft" => "O aplikaci GridCraft", + "Version" => "Verze", + "Platform" => "Platforma", + "Licence" => "Licence", "Blank workbook" => "Prázdný sešit", "Recent" => "Naposledy použité", "No recent workbooks yet." => "Zatím žádné naposledy použité sešity.", diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 75653e583..246c63572 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -256,6 +256,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportieren", "Close" => "Schließen", "Options" => "Optionen", + "Account" => "Konto", + "Product Information" => "Produktinformationen", + "About GridCraft" => "Info zu GridCraft", + "Version" => "Version", + "Platform" => "Plattform", + "Licence" => "Lizenz", + "Version, contributors and the AI models that helped" => "Version, Mitwirkende und die KI-Modelle, die geholfen haben", "Blank workbook" => "Leere Arbeitsmappe", "Recent" => "Zuletzt verwendet", "No recent workbooks yet." => "Noch keine zuletzt verwendeten Arbeitsmappen.", diff --git a/crates/ui-egui/src/i18n/es.rs b/crates/ui-egui/src/i18n/es.rs index 0582110c7..948dc7dd1 100644 --- a/crates/ui-egui/src/i18n/es.rs +++ b/crates/ui-egui/src/i18n/es.rs @@ -257,6 +257,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportar", "Close" => "Cerrar", "Options" => "Opciones", + "Account" => "Cuenta", + "Product Information" => "Información del producto", + "About GridCraft" => "Acerca de GridCraft", + "Version" => "Versión", + "Platform" => "Plataforma", + "Licence" => "Licencia", + "Version, contributors and the AI models that helped" => "Versión, colaboradores y los modelos de IA que ayudaron", "Blank workbook" => "Libro en blanco", "Recent" => "Recientes", "No recent workbooks yet." => "Todavía no hay libros recientes.", diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index c70b25cf5..780065501 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -342,6 +342,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exporter", "Close" => "Fermer", "Options" => "Options", + "Account" => "Compte", + "Product Information" => "Informations sur le produit", + "About GridCraft" => "À propos de GridCraft", + "Version" => "Version", + "Platform" => "Plateforme", + "Licence" => "Licence", + "Version, contributors and the AI models that helped" => "Version, contributeurs et les modèles d'IA qui ont aidé", "Blank workbook" => "Classeur vide", "Recent" => "Récent", "No recent workbooks yet." => "Aucun classeur récent pour le moment.", diff --git a/crates/ui-egui/src/i18n/ko.rs b/crates/ui-egui/src/i18n/ko.rs index f3e1688d0..9849d451c 100644 --- a/crates/ui-egui/src/i18n/ko.rs +++ b/crates/ui-egui/src/i18n/ko.rs @@ -344,6 +344,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "내보내기", "Close" => "닫기", "Options" => "옵션", + "Account" => "계정", + "Product Information" => "제품 정보", + "About GridCraft" => "GridCraft 정보", + "Version" => "버전", + "Platform" => "플랫폼", + "Licence" => "라이선스", + "Version, contributors and the AI models that helped" => "버전, 기여자 및 도움을 준 AI 모델", "Blank workbook" => "새 통합 문서", "Recent" => "최근 항목", "No recent workbooks yet." => "최근 통합 문서가 아직 없습니다.", diff --git a/crates/ui-egui/src/i18n/pt.rs b/crates/ui-egui/src/i18n/pt.rs index 9ab5b7baf..27f504eee 100644 --- a/crates/ui-egui/src/i18n/pt.rs +++ b/crates/ui-egui/src/i18n/pt.rs @@ -345,6 +345,11 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportar", "Close" => "Fechar", "Options" => "Opções", + "Account" => "Conta", + "Product Information" => "Informações do produto", + "Version" => "Versão", + "Platform" => "Plataforma", + "Licence" => "Licença", "Blank workbook" => "Pasta de trabalho em branco", "Recent" => "Recentes", "No recent workbooks yet." => "Ainda não há pastas de trabalho recentes.", diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 868f653b8..2bfa63bb4 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -344,6 +344,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Экспорт", "Close" => "Закрыть", "Options" => "Параметры", + "Account" => "Учетная запись", + "Product Information" => "Сведения о продукте", + "About GridCraft" => "О программе GridCraft", + "Version" => "Версия", + "Platform" => "Платформа", + "Licence" => "Лицензия", + "Version, contributors and the AI models that helped" => "Версия, участники и модели ИИ, которые помогали", "Blank workbook" => "Пустая книга", "Recent" => "Последние", "No recent workbooks yet." => "Недавних книг пока нет.", diff --git a/crates/ui-egui/src/i18n/sr.rs b/crates/ui-egui/src/i18n/sr.rs index 152bddee8..9621d7d3a 100644 --- a/crates/ui-egui/src/i18n/sr.rs +++ b/crates/ui-egui/src/i18n/sr.rs @@ -336,6 +336,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Izvezi", "Close" => "Zatvori", "Options" => "Opcije", + "Account" => "Nalog", + "Product Information" => "Informacije o proizvodu", + "About GridCraft" => "O programu GridCraft", + "Version" => "Verzija", + "Platform" => "Platforma", + "Licence" => "Licenca", + "Version, contributors and the AI models that helped" => "Verzija, saradnici i AI modeli koji su pomogli", "Blank workbook" => "Prazna radna sveska", "Recent" => "Nedavno", "No recent workbooks yet." => "Još nema nedavnih radnih svezaka.", diff --git a/crates/ui-egui/src/i18n/zh.rs b/crates/ui-egui/src/i18n/zh.rs index a9989f2e4..1af3d4563 100644 --- a/crates/ui-egui/src/i18n/zh.rs +++ b/crates/ui-egui/src/i18n/zh.rs @@ -342,6 +342,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "导出", "Close" => "关闭", "Options" => "选项", + "Account" => "账户", + "Product Information" => "产品信息", + "About GridCraft" => "关于 GridCraft", + "Version" => "版本", + "Platform" => "平台", + "Licence" => "许可证", + "Version, contributors and the AI models that helped" => "版本、贡献者以及参与协助的 AI 模型", "Blank workbook" => "空白工作簿", "Recent" => "最近", "No recent workbooks yet." => "还没有最近使用的工作簿。", diff --git a/crates/ui-egui/tests/file_menu.rs b/crates/ui-egui/tests/file_menu.rs index 691eef367..abdc11592 100644 --- a/crates/ui-egui/tests/file_menu.rs +++ b/crates/ui-egui/tests/file_menu.rs @@ -122,7 +122,7 @@ fn file_options_opens_the_options_page_and_pages_are_reachable_by_command() { h.run_steps(2); assert_eq!(page(&h), Some(id)); } - assert!(h.state_mut().run("ui.backstage", serde_json::json!({"page": "account"})).is_err()); + assert!(h.state_mut().run("ui.backstage", serde_json::json!({"page": "feedback"})).is_err()); h.state_mut().run("ui.backstage", serde_json::json!({"page": "close"})).unwrap(); assert!(h.state().backstage.is_none()); // The back arrow returns to the sheet too. @@ -132,6 +132,20 @@ fn file_options_opens_the_options_page_and_pages_are_reachable_by_command() { assert!(h.state().backstage.is_none()); } +#[test] +fn file_account_shows_the_version_and_opens_about() { + let mut h = harness(Services::default()); + choose(&mut h, "Account"); + assert_eq!(page(&h), Some("account")); + assert!(h.state().dialog.is_none()); + // The About dialog also shows the version; it must not be open yet. + h.get_by_label(env!("CARGO_PKG_VERSION")); + h.get_by_label(env!("CARGO_PKG_LICENSE")); + click(&mut h, "About GridCraft"); + assert_eq!(h.state().dialog.as_ref().map(|d| d.name()), Some("about")); + assert_eq!(page(&h), Some("account"), "About opens over the File view, like Protect Workbook on Info"); +} + #[test] fn export_as_pdf_is_delivered_as_a_download() { let files = Arc::new(Mutex::new(Vec::<(String, Vec)>::new())); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 7cc6ec230..bd1c43262 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -32,7 +32,7 @@ Errors come back as `{"id": …, "ok": false, "error": "…"}`. | `ui.edit.begin` / `ui.edit.text` / `ui.edit.commit` / `ui.edit.cancel` | `{text?}` / `{move?: down\|right\|up\|left\|none}` | Drive the cell editor | | `ui.ribbon` | `{tab}` | Switch ribbon tab | | `ui.dialog` / `ui.dialog.set` / `ui.dialog.confirm` / `ui.dialog.cancel` | `{name}` / `{field, value}` | Open, fill in and confirm dialogs | -| `ui.backstage` (via `engine.execute`) | `{page?: home\|new\|open\|info\|saveAs\|print\|export\|options\|close}` | Open the File tab's Backstage view on a page, or close it (`ui.inspect` reports the open page as `backstage`) | +| `ui.backstage` (via `engine.execute`) | `{page?: home\|new\|open\|info\|saveAs\|print\|export\|options\|account\|close}` | Open the File tab's Backstage view on a page, or close it (`ui.inspect` reports the open page as `backstage`) | | `ui.screenshot` | `{path?}` | PNG of the window (base64 when no path). Animations are settled first: the picture shows the final state | | `ui.set` | `{dark?, formulaBar?, ribbonCollapsed?, animations?}` | UI preferences (`formulaBar` runs `view.formulaBar`) | | `ui.resize` / `ui.focus` | `{width, height}` | Window control |