Preferences: one fixed size, buttons flush right, a real gear icon (#290)

- The dialog draws every page into a fixed, clipped 744x400 area, so no page
  (or translation) changes the window width; descriptions wrap and tall pages
  scroll inside it.
- OK / Cancel / Apply sit flush right in the bottom bar (right-to-left layout
  within the fixed width); Restore Defaults stays left.
- The "settings" icon is a gear: a solid ring with six square-ish teeth and a
  see-through centre hole, instead of the sun-like rays.
- Scenario s46 visits every page and checks the window keeps its width and
  that Apply ends at the bottom bar's right edge (new `same_width` and
  `right_edge` scenario checks).

Fixes #290

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-12 10:07:23 -04:00
1 parent 87bb2f37f8
commit df09fdbc32
4 files changed
+130 -35

No files matched your search

+49 -30
View File
@@ -396,52 +396,64 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let mut open = true;
let mut button: Option<&str> = None;
let before = app.preferences.clone();
crate::frame::window(ctx, app.tr("ui.preferences"), crate::frame::Width::Wide)
let shown = crate::frame::window(ctx, app.tr("ui.preferences"), crate::frame::Width::Wide)
.id(egui::Id::new("sc_prefs"))
.open(&mut open)
.collapsible(false)
.resizable(false)
.show(ctx, |ui| {
// A fixed page area: the vertical separator fills the height it is given, so an
// auto-sized one would grow every frame.
ui.allocate_ui_with_layout(egui::vec2(700.0, 400.0), egui::Layout::left_to_right(egui::Align::Min), |ui| {
ui.set_min_size(egui::vec2(700.0, 400.0));
ui.vertical(|ui| {
ui.set_width(140.0);
for (i, s) in PAGES.iter().enumerate() {
if ui.selectable_label(app.prefs_window.section == i, app.tr_label(s)).clicked() {
app.prefs_window.section = i;
app.keymap.read_only = false;
}
// One fixed size for every page (#290): the page is drawn into a clipped child of
// that size and only that size is allocated, so no page (or translation) can widen
// the window; descriptions wrap and tall pages scroll inside it.
ui.set_max_width(PREFS_W);
let (body, _) = ui.allocate_exact_size(egui::vec2(PREFS_W, PREFS_H), egui::Sense::hover());
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(body).layout(egui::Layout::left_to_right(egui::Align::Min)));
child.shrink_clip_rect(body);
let ui_body = &mut child;
ui_body.vertical(|ui| {
ui.set_width(PREFS_NAV_W);
for (i, s) in PAGES.iter().enumerate() {
if ui.selectable_label(app.prefs_window.section == i, app.tr_label(s)).clicked() {
app.prefs_window.section = i;
app.keymap.read_only = false;
}
});
ui.separator();
ui.vertical(|ui| {
// A fixed width, so the window keeps its size from page to page.
ui.set_width(540.0);
ui.label(RichText::new(app.tr_label(PAGES.get(app.prefs_window.section).copied().unwrap_or_default())).strong().size(14.0));
ui.add_space(6.0);
}
});
ui_body.separator();
ui_body.vertical(|ui| {
ui.set_width(ui.available_width());
ui.label(RichText::new(app.tr_label(PAGES.get(app.prefs_window.section).copied().unwrap_or_default())).strong().size(14.0));
ui.add_space(6.0);
if app.prefs_window.section == PAGE_SHORTCUTS {
// The shortcuts table scrolls by itself.
page(app, ui, &t);
});
} else {
egui::ScrollArea::vertical().id_salt("sc_prefs_page").auto_shrink([false, false]).show(ui, |ui| page(app, ui, &t));
}
});
ui.separator();
// Restore Defaults at the left; OK, Cancel and Apply flush right, as in every dialog.
ui.horizontal(|ui| {
ui.set_width(PREFS_W);
if ui.button(app.tr("ui.restore_defaults")).on_hover_text(app.tr("ui.reset_the_settings_on_this_page")).clicked() {
button = Some("defaults");
}
crate::frame::right_aligned(ui, 700.0, 170.0, |ui| {
if ui.button(app.tr("ui.heading_ok")).clicked() {
button = Some("ok");
}
if ui.button(app.tr("ui.cancel")).clicked() {
button = Some("cancel");
}
if ui.button(app.tr("ui.apply")).clicked() {
button = Some("apply");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// Right to left: Apply is the rightmost.
for (key, id) in [("ui.apply", "apply"), ("ui.cancel", "cancel"), ("ui.heading_ok", "ok")] {
let r = ui.button(app.tr(key));
crate::scenario::publish_rect(&format!("prefs:{id}"), r.rect);
if r.clicked() {
button = Some(id);
}
}
});
crate::scenario::publish_rect("prefs:footer", ui.max_rect());
});
});
if let Some(w) = &shown {
crate::scenario::publish_rect("prefs:window", w.response.rect);
}
if app.preferences.msaa != before.msaa {
save_graphics(app);
}
@@ -485,12 +497,19 @@ fn up_axis_changed(app: &mut SolveApp) {
}
}
/// The Preferences window's content size: the same on every page.
const PREFS_W: f32 = 744.0;
const PREFS_H: f32 = 400.0;
/// The page list at its left.
const PREFS_NAV_W: f32 = 128.0;
/// One page of the dialog.
fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
let _language = crate::i18n::scope(&app.preferences.language);
let note = |ui: &mut egui::Ui, s: &str| {
ui.label("");
ui.label(RichText::new(s).size(11.0).color(t.text_dim));
// Descriptions wrap within the page instead of widening it.
ui.add(egui::Label::new(RichText::new(s).size(11.0).color(t.text_dim)).wrap());
ui.end_row();
};
if app.prefs_window.section == PAGE_SHORTCUTS {