diff --git a/crates/ui-egui/src/colors.rs b/crates/ui-egui/src/colors.rs index 70542f3d90..2a9d5f25f4 100644 --- a/crates/ui-egui/src/colors.rs +++ b/crates/ui-egui/src/colors.rs @@ -490,14 +490,11 @@ fn body(ui: &mut Ui, d: &mut ColorsDialog) -> Option { ui.set_width(430.0); ui.label(egui::RichText::new(tl!("Colors")).font(crate::theme::semibold(14.0))); ui.add_space(6.0); - ui.horizontal(|ui| { - for (tab, label) in [(ColorsTab::Standard, "Standard"), (ColorsTab::Custom, "Custom")] { - if ui.selectable_label(d.tab == tab, tl!(label)).clicked() { - d.tab = tab; - } - } - }); - ui.separator(); + let names = [tl!("Standard").to_string(), tl!("Custom").to_string()]; + if let Some(k) = crate::widgets::dialog_tabs(ui, &names, d.tab as usize) { + d.tab = if k == 0 { ColorsTab::Standard } else { ColorsTab::Custom }; + } + ui.add_space(6.0); ui.horizontal(|ui| { crate::rtl::vertical(ui, |ui| { ui.set_width(330.0); diff --git a/crates/ui-egui/src/dialogs/dialogs_insert/symbol.rs b/crates/ui-egui/src/dialogs/dialogs_insert/symbol.rs index 984b4e1306..797d6aef72 100644 --- a/crates/ui-egui/src/dialogs/dialogs_insert/symbol.rs +++ b/crates/ui-egui/src/dialogs/dialogs_insert/symbol.rs @@ -269,14 +269,11 @@ fn symbol_cell(app: &mut WordApp, ui: &mut Ui, font: &str, c: char, selected: bo pub(super) fn symbol_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SymbolForm) -> bool { let t = Tokens::get(ui.ctx()); ui.set_width(GRID_COLS as f32 * CELL + 24.0); - ui.horizontal(|ui| { - for (i, l) in ["Symbols", "Special Characters"].into_iter().enumerate() { - if ui.selectable_label(f.tab == i as u8, tl!(l)).clicked() { - f.tab = i as u8; - } - } - }); - ui.separator(); + let names = [tl!("Symbols").to_string(), tl!("Special Characters").to_string()]; + if let Some(k) = crate::widgets::dialog_tabs(ui, &names, f.tab as usize) { + f.tab = k as u8; + } + ui.add_space(6.0); let mut insert: Option<(Value, String, String)> = None; if f.tab == 1 { if special_ui(app, ui, f, &mut insert, &t) { diff --git a/crates/ui-egui/src/dialogs/dialogs_para.rs b/crates/ui-egui/src/dialogs/dialogs_para.rs index 59a348ada0..0fd462903e 100644 --- a/crates/ui-egui/src/dialogs/dialogs_para.rs +++ b/crates/ui-egui/src/dialogs/dialogs_para.rs @@ -557,14 +557,11 @@ impl BordersForm { /// The Borders and Shading dialog body. Returns true to close. pub fn borders(app: &mut WordApp, ui: &mut Ui, f: &mut BordersForm) -> bool { - ui.horizontal(|ui| { - for (i, l) in ["Borders", "Page Border", "Shading"].into_iter().enumerate() { - if ui.selectable_label(f.tab == i as u8, tl!(l)).clicked() { - f.tab = i as u8; - } - } - }); - ui.separator(); + let names = [tl!("Borders").to_string(), tl!("Page Border").to_string(), tl!("Shading").to_string()]; + if let Some(k) = crate::widgets::dialog_tabs(ui, &names, f.tab as usize) { + f.tab = k as u8; + } + ui.add_space(6.0); let theme = app.session.doc.settings.theme_colors.clone(); match f.tab { 0 => { diff --git a/crates/ui-egui/src/dialogs/dialogs_review.rs b/crates/ui-egui/src/dialogs/dialogs_review.rs index b790d71990..d5977dbadb 100644 --- a/crates/ui-egui/src/dialogs/dialogs_review.rs +++ b/crates/ui-egui/src/dialogs/dialogs_review.rs @@ -396,14 +396,11 @@ fn list_change(app: &mut WordApp, f: &mut AutoCorrectForm, params: Value) -> boo } fn autocorrect_ui(app: &mut WordApp, ui: &mut Ui, f: &mut AutoCorrectForm) -> bool { - ui.horizontal(|ui| { - for (i, l) in ["AutoCorrect", "AutoFormat As You Type", "Math AutoCorrect"].into_iter().enumerate() { - if ui.selectable_label(f.tab == i as u8, tl!(l)).clicked() { - f.tab = i as u8; - } - } - }); - ui.separator(); + let names = [tl!("AutoCorrect").to_string(), tl!("AutoFormat As You Type").to_string(), tl!("Math AutoCorrect").to_string()]; + if let Some(k) = crate::widgets::dialog_tabs(ui, &names, f.tab as usize) { + f.tab = k as u8; + } + ui.add_space(6.0); match f.tab { 0 => autocorrect_tab(app, ui, f), 1 => { diff --git a/crates/ui-egui/src/dialogs/dialogs_ribbon.rs b/crates/ui-egui/src/dialogs/dialogs_ribbon.rs index 7223b93485..8105569077 100644 --- a/crates/ui-egui/src/dialogs/dialogs_ribbon.rs +++ b/crates/ui-egui/src/dialogs/dialogs_ribbon.rs @@ -88,17 +88,14 @@ fn label_of(app: &WordApp, id: &str) -> String { /// The dialog body; true closes it. pub fn customize_ribbon(app: &mut WordApp, ui: &mut Ui, f: &mut RibbonForm) -> bool { - ui.horizontal(|ui| { - for (page, name) in [(Page::Ribbon, "Ribbon"), (Page::QuickAccess, "Quick Access Toolbar")] { - if ui.selectable_label(f.page == page, tl!(name)).clicked() && f.page != page { - f.page = page; - f.renaming = None; - f.confirm_reset = false; - f.message.clear(); - } - } - }); - ui.separator(); + let names = [tl!("Ribbon").to_string(), tl!("Quick Access Toolbar").to_string()]; + if let Some(k) = crate::widgets::dialog_tabs(ui, &names, (f.page == Page::QuickAccess) as usize) { + f.page = if k == 0 { Page::Ribbon } else { Page::QuickAccess }; + f.renaming = None; + f.confirm_reset = false; + f.message.clear(); + } + ui.add_space(6.0); let (left, right, h) = (150.0, 230.0, 300.0); ui.add(egui::TextEdit::singleline(&mut f.filter).hint_text(tl!("Search")).desired_width(left + right + 8.0)); let mut cats: Vec<(String, String)> = categories(app).into_iter().map(|c| (c.clone(), tl!(&c).to_string())).collect(); diff --git a/crates/ui-egui/src/dialogs/dispatch.rs b/crates/ui-egui/src/dialogs/dispatch.rs index d6f1edf593..ae4fca44e2 100644 --- a/crates/ui-egui/src/dialogs/dispatch.rs +++ b/crates/ui-egui/src/dialogs/dispatch.rs @@ -274,7 +274,7 @@ fn paragraph_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { else { return false; }; - let names = ["Indents and Spacing", "Line and Page Breaks", "Asian Typography"].map(|n| tl!(n).to_string()); + let names = [tl!("Indents and Spacing").to_string(), tl!("Line and Page Breaks").to_string(), tl!("Asian Typography").to_string()]; if let Some(k) = crate::widgets::dialog_tabs(ui, &names, *tab as usize) { *tab = k as u8; } @@ -1161,7 +1161,7 @@ fn about_dialog(ui: &mut Ui, d: &mut Dialog) -> bool { return false; }; ui.set_width(820.0); - let names = ["About", "Contributors", "Models", "Language data"].map(|n| tl!(n).to_string()); + let names = [tl!("About").to_string(), tl!("Contributors").to_string(), tl!("Models").to_string(), tl!("Language data").to_string()]; if let Some(i) = crate::widgets::dialog_tabs(ui, &names, *tab as usize) { *tab = i as u8; } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3c11561440..516852fd6c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -813,5 +813,8 @@ pub(crate) fn rust_sources(dir: &std::path::Path) -> Vec { files } +#[cfg(test)] +mod tests_dialog_tabs; + #[cfg(test)] mod tests; diff --git a/crates/ui-egui/src/tests_dialog_tabs.rs b/crates/ui-egui/src/tests_dialog_tabs.rs new file mode 100644 index 0000000000..476cfe302a --- /dev/null +++ b/crates/ui-egui/src/tests_dialog_tabs.rs @@ -0,0 +1,95 @@ +//! Dialogs with real tabs: a click on a tab shows its page. + +use egui_kittest::kittest::Queryable; +use serde_json::json; +use wordcraft_engine::Session; + +use crate::WordApp; +use crate::dialogs::Dialog; + +fn harness(name: &str) -> egui_kittest::Harness<'static, WordApp> { + let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), crate::Services::default()); + let _ = app.run("ui.language", json!({"value": "en"})); + let _ = app.run("document.setText", json!({"text": "Some text"})); + let _ = app.run("select.all", json!({})); + let _ = app.run("ui.dialog", json!({"name": name})); + if app.dialog.is_none() && app.colors.is_none() { + app.dialog = Dialog::open(name, &mut app); + } + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_accessibility_check(false).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.ctx.set_fonts(crate::theme::font_definitions(false, false)); + h.ctx.set_missing_glyph_policy(egui::MissingGlyphPolicy::Tofu); + settle(&mut h); + h +} + +fn settle(h: &mut egui_kittest::Harness<'static, WordApp>) { + for _ in 0..4 { + h.step(); + } +} + +fn tab_button<'a>(h: &'a egui_kittest::Harness<'static, WordApp>, label: &'a str) -> egui_kittest::Node<'a> { + h.query_all_by_role_and_label(egui::accesskit::Role::Button, label).last().unwrap_or_else(|| panic!("no {label} button")) +} + +/// Click each tab in turn. After every click `page` must report its index. +fn check(name: &str, tabs: &[&str], page: impl Fn(&WordApp) -> usize) { + let mut h = harness(name); + assert_eq!(page(h.state()), 0, "{name}: opens on the first tab"); + for (i, label) in tabs.iter().enumerate() { + tab_button(&h, label).click(); + settle(&mut h); + assert_eq!(page(h.state()), i, "{name}: the {label} tab shows its page"); + } +} + +#[test] +fn colors_tabs_switch_pages() { + check("colors", &["Standard", "Custom"], |app| app.colors.as_ref().map_or(9, |d| d.tab as usize)); +} + +#[test] +fn symbol_tabs_switch_pages() { + check("symbol", &["Symbols", "Special Characters"], |app| match &app.dialog { + Some(Dialog::Insert(d)) => match &**d { + crate::dialogs_insert::InsertDialog::Symbol(f) => f.tab as usize, + _ => 9, + }, + _ => 9, + }); +} + +#[test] +fn borders_tabs_switch_pages() { + check("borders", &["Borders", "Page Border", "Shading"], |app| match &app.dialog { + Some(Dialog::Borders { form }) => form.tab as usize, + _ => 9, + }); +} + +#[test] +fn autocorrect_tabs_switch_pages() { + check("autoCorrect", &["AutoCorrect", "AutoFormat As You Type", "Math AutoCorrect"], |app| match &app.dialog { + Some(Dialog::Review(d)) => match &**d { + crate::dialogs_review::ReviewDialog::AutoCorrect(f) => f.tab as usize, + _ => 9, + }, + _ => 9, + }); +} + +#[test] +fn customize_ribbon_tabs_switch_pages() { + check("customizeRibbon", &["Ribbon", "Quick Access Toolbar"], |app| match &app.dialog { + Some(Dialog::CustomizeRibbon { form }) => (form.page == crate::dialogs_ribbon::Page::QuickAccess) as usize, + _ => 9, + }); +} diff --git a/crates/ui-egui/src/widgets/tests.rs b/crates/ui-egui/src/widgets/tests.rs index f97cb4e7df..b1896f34c1 100644 --- a/crates/ui-egui/src/widgets/tests.rs +++ b/crates/ui-egui/src/widgets/tests.rs @@ -530,3 +530,24 @@ fn a_long_combo_list_builds_only_the_rows_in_view() { }) .drop_without_applying_deltas(); } + +/// The tab strip exposes each tab as a button with its selected state, and a click on an +/// unselected tab reports its index. +#[test] +fn dialog_tabs_expose_the_selected_state_and_report_clicks() { + use egui_kittest::kittest::{NodeT, Queryable}; + let mut h = egui_kittest::Harness::builder().with_size(vec2(300.0, 60.0)).build_ui_state( + |ui, st: &mut (usize, Option)| { + if let Some(i) = dialog_tabs(ui, &["One".to_string(), "Two".to_string()], st.0) { + st.1 = Some(i); + } + }, + (0usize, None), + ); + h.run_steps(2); + assert_eq!(h.get_by_label("One").accesskit_node().toggled(), Some(egui::accesskit::Toggled::True)); + assert_eq!(h.get_by_label("Two").accesskit_node().toggled(), Some(egui::accesskit::Toggled::False)); + h.get_by_label("Two").click(); + h.run_steps(2); + assert_eq!(h.state().1, Some(1)); +}