From 75ec4fea77ab7415c803e39d0ab7a0d5dc79210b Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 12:38:52 -0800 Subject: [PATCH] Preferences: Reset Preferences shares the row of Export and Import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export Preferences… and Import Preferences… had a row to themselves, and Reset Preferences started the next row, left of Previous, Next, Cancel and OK. Reset Preferences now follows Import Preferences… on the first row, which wraps when it is wider than the dialog, and the row below holds only the category buttons, Cancel and OK. Tests check that the three buttons share a row in English, and that in every language the dialog keeps its 760 pt content width with each button inside it and above the OK row. Hindi, Gujarati, Georgian, Japanese, Korean and Chinese are measured with fallback fonts, since the tests do not load the craft-fonts faces or installed fonts those languages are drawn with. --- crates/ui-egui/src/prefs_dialog.rs | 9 ++-- crates/ui-egui/src/prefs_dialog/tests.rs | 68 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index 9029459195..f9939177de 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -404,7 +404,8 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { }); }); ui.add_space(14.0); - ui.horizontal(|ui| { + // Export, Import and Reset share one row, which wraps when it is wider than the dialog. + ui.horizontal_wrapped(|ui| { if widgets::secondary_button(ui, tl!("Export Preferences…")).on_hover_text(tl!("Save the applied preferences to a JSON file")).clicked() { export_preferences(app, &mut d); @@ -417,15 +418,15 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { { import_preferences(app, &mut d); } - }); - ui.add_space(8.0); - ui.horizontal(|ui| { if widgets::secondary_button(ui, tl!("Reset Preferences")) .on_hover_text(tl!("Restore every preference to its default (applied on OK)")) .clicked() { reset = true; } + }); + ui.add_space(8.0); + ui.horizontal(|ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if widgets::primary_button(ui, tl!("OK")).clicked() { ok = true; diff --git a/crates/ui-egui/src/prefs_dialog/tests.rs b/crates/ui-egui/src/prefs_dialog/tests.rs index f45ba93410..5c9356edd9 100644 --- a/crates/ui-egui/src/prefs_dialog/tests.rs +++ b/crates/ui-egui/src/prefs_dialog/tests.rs @@ -335,6 +335,74 @@ fn reset_preferences(app: &mut VectorcraftApp) { crate::dialogs::confirm(app).unwrap(); } +/// The Preferences dialog drawn in `lang` with the open dialog available: the label and screen +/// rectangle of each control, from the accessibility tree, and the dialog's content rectangle +/// (inside its 20 pt margin and frame stroke). +fn dialog_controls(lang: crate::i18n::Lang) -> (Vec<(String, egui::Rect)>, egui::Rect) { + crate::i18n::set_current(lang); + let mut a = app(); + a.services.pick_open = Some(Box::new(|_: &crate::FilePick| None)); + open(&mut a, Some("General")); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let mut v = vec![]; + for time in [0.0, 0.25, 0.5] { + let screen_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 900.0))); + let raw = egui::RawInput { screen_rect, time: Some(time), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| show(&mut a, ui.ctx())); + out.textures_delta.clear(); + let nodes = out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default(); + v = nodes + .iter() + .filter_map(|(_, n)| { + let b = n.bounds()?; + Some((n.label()?.to_string(), egui::Rect::from_min_max(egui::pos2(b.x0 as f32, b.y0 as f32), egui::pos2(b.x1 as f32, b.y1 as f32)))) + }) + .collect(); + } + crate::i18n::set_current(crate::i18n::Lang::EN); + let window = ctx.memory(|m| m.area_rect(egui::Id::unique("dialog-preferences"))).expect("the dialog window is shown"); + let content = window.shrink(20.0 + ctx.global_style().visuals.window_stroke.width); + (v, content) +} + +/// Export Preferences…, Import Preferences… and Reset Preferences share one row above OK and +/// Cancel. +#[test] +fn export_import_and_reset_share_one_row() { + let (controls, _) = dialog_controls(crate::i18n::Lang::EN); + let top = |label: &str| controls.iter().find(|(t, _)| t == label).map(|(_, r)| r.top()).unwrap_or_else(|| panic!("no `{label}` in {controls:?}")); + let row = top("Export Preferences…"); + assert!((top("Import Preferences…") - row).abs() < 0.5, "Import is on Export's row"); + assert!((top("Reset Preferences") - row).abs() < 0.5, "Reset is on Export's row"); + assert!(top("OK") > row + 20.0, "OK is on the row below"); +} + +/// In every language the dialog keeps its 760 pt content width, and the three buttons above OK +/// lie inside it and above the row of OK and Cancel. +#[test] +fn export_import_and_reset_fit_in_every_language() { + for lang in crate::i18n::Lang::all() { + let (controls, content) = dialog_controls(lang); + assert!(content.width() <= 760.5, "{}: the dialog's content grew to {content:?}", lang.code()); + let find = |label: &str| { + let label = crate::i18n::tr(lang, label); + controls.iter().find(|(t, _)| t == label).map(|(_, r)| *r).unwrap_or_else(|| panic!("{}: no `{label}` in {controls:?}", lang.code())) + }; + let ok = find("OK"); + for label in ["Export Preferences…", "Import Preferences…", "Reset Preferences"] { + let r = find(label); + assert!( + r.right() <= content.right() + 0.5 && r.left() >= content.left() - 0.5, + "{}: `{label}` at {r:?} is outside the dialog's content {content:?}", + lang.code() + ); + assert!(r.bottom() <= ok.top(), "{}: `{label}` at {r:?} reaches the OK row at {ok:?}", lang.code()); + } + } +} + /// Reset Preferences then OK resets the preferences, General included, and leaves the open /// document's units, with no undo step in the document. #[test]