Options: an interface scale preference (#1508)

interfaceScale (0.5-3, default 1) in the OPTIONS JSON form and the Display
tab scales every menu, palette, dialog and the command line through egui's
zoom factor, and is kept in the profile.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mattlafy
2026-10-19 02:33:04 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 94aad92f1d
commit 64e55f903c
5 changed files with 93 additions and 1 deletions
+1
View File
@@ -1219,6 +1219,7 @@
Show System Cursor Показувати системний курсор Show System Cursor Показувати системний курсор
Animation Effects Ефекти анімації Animation Effects Ефекти анімації
Animation effects Ефекти анімації Animation effects Ефекти анімації
Interface scale Масштаб інтерфейсу
Import... Імпорт... Import... Імпорт...
Export... Експорт... Export... Експорт...
Batch Publish... Пакетна публікація... Batch Publish... Пакетна публікація...
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
1219
1220
1221
1222
1223
1224
1225
+10
View File
@@ -224,6 +224,8 @@ pub struct CadApp {
shown_follows_system: bool, shown_follows_system: bool,
/// Whether egui was last told to animate (`None` until the first frame). /// Whether egui was last told to animate (`None` until the first frame).
shown_animations: Option<bool>, shown_animations: Option<bool>,
/// The interface scale last handed to egui (`None` until the first frame).
shown_interface_scale: Option<f32>,
pub frame_ms: f64, pub frame_ms: f64,
pub quit_requested: bool, pub quit_requested: bool,
/// The app may close although drawings have unsaved changes (they were dealt with, or the /// The app may close although drawings have unsaved changes (they were dealt with, or the
@@ -271,6 +273,7 @@ impl CadApp {
shown_theme: None, shown_theme: None,
shown_follows_system: false, shown_follows_system: false,
shown_animations: None, shown_animations: None,
shown_interface_scale: None,
frame_ms: 0.0, frame_ms: 0.0,
quit_requested: false, quit_requested: false,
quit_confirmed: false, quit_confirmed: false,
@@ -561,6 +564,13 @@ impl CadApp {
if !animate { if !animate {
animation::unsmooth_scroll(ctx); animation::unsmooth_scroll(ctx);
} }
// Applied only when the saved scale changes, so egui's own keyboard zoom keeps working
// within a session (and is never saved).
let scale = self.options.interface_scale;
if self.shown_interface_scale != Some(scale) {
ctx.set_zoom_factor(scale);
self.shown_interface_scale = Some(scale);
}
self.drain_control(ctx); self.drain_control(ctx);
self.finish_opening(); self.finish_opening();
self.opening.show(ctx); self.opening.show(ctx);
+13
View File
@@ -161,6 +161,19 @@ fn display(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
} }
ui.end_row(); ui.end_row();
ui.label(crate::tl!("Interface scale"));
let scale = app.options.interface_scale;
let percent = |s: f32| format!("{} %", (s * 100.0).round());
egui::ComboBox::from_id_salt("options_interface_scale").selected_text(percent(scale)).show_ui(ui, |ui| {
for n in [75u16, 100, 125, 150, 175, 200, 250, 300] {
let s = f32::from(n) / 100.0;
if ui.selectable_label((scale - s).abs() < 1e-4, format!("{n} %")).clicked() && (scale - s).abs() >= 1e-4 {
changes.insert("interfaceScale".into(), json!(s));
}
}
});
ui.end_row();
ui.label(crate::tl!("Model background")); ui.label(crate::tl!("Model background"));
color_row(ui, app.options.model_background, Tokens::DARK.canvas, "modelBackground", changes); color_row(ui, app.options.model_background, Tokens::DARK.canvas, "modelBackground", changes);
ui.end_row(); ui.end_row();
+23 -1
View File
@@ -21,6 +21,7 @@
//! | `modelBackground`, `paperBackground` | `"#rrggbb"`, or `null` for the default | //! | `modelBackground`, `paperBackground` | `"#rrggbb"`, or `null` for the default |
//! | `selectionColor` | the colour selected objects are highlighted in: `"#rrggbb"`, or `null` for the theme's | //! | `selectionColor` | the colour selected objects are highlighted in: `"#rrggbb"`, or `null` for the theme's |
//! | `snapMarkerSize` | object snap marker size in points, 5–31 | //! | `snapMarkerSize` | object snap marker size in points, 5–31 |
//! | `interfaceScale` | the interface's size, 0.5–3 (1: the system's own) |
//! //!
//! SAVETIME and SAVEFILEPATH are the engine's autosave settings, kept with the autosave state //! SAVETIME and SAVEFILEPATH are the engine's autosave settings, kept with the autosave state
//! rather than in the profile. //! rather than in the profile.
@@ -52,6 +53,9 @@ const MAX_PATH_CHARS: usize = 1024;
/// Object snap marker sizes the Options dialog offers, in points. /// Object snap marker sizes the Options dialog offers, in points.
pub const SNAP_MARKER_SIZES: std::ops::RangeInclusive<f32> = 5.0..=31.0; pub const SNAP_MARKER_SIZES: std::ops::RangeInclusive<f32> = 5.0..=31.0;
/// Interface scales the JSON form accepts (1: the system's own size).
pub const INTERFACE_SCALES: std::ops::RangeInclusive<f32> = 0.5..=3.0;
/// The interface's own preferences (the drafting ones are engine settings). /// The interface's own preferences (the drafting ones are engine settings).
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct UiOptions { pub struct UiOptions {
@@ -66,6 +70,8 @@ pub struct UiOptions {
/// While a command runs: a quick right-click is Enter and a held one opens the shortcut menu /// While a command runs: a quick right-click is Enter and a held one opens the shortcut menu
/// (AutoCAD's time-sensitive right-click). Off: a right-click always opens the menu. /// (AutoCAD's time-sensitive right-click). Off: a right-click always opens the menu.
pub time_sensitive_right_click: bool, pub time_sensitive_right_click: bool,
/// The size of every menu, palette, dialog and the command line (1: the system's own).
pub interface_scale: f32,
} }
impl Default for UiOptions { impl Default for UiOptions {
@@ -76,6 +82,7 @@ impl Default for UiOptions {
snap_marker_size: theme::SNAP_MARKER, snap_marker_size: theme::SNAP_MARKER,
selection_color: None, selection_color: None,
time_sensitive_right_click: true, time_sensitive_right_click: true,
interface_scale: 1.0,
} }
} }
} }
@@ -141,6 +148,7 @@ struct UiChanges {
savetime: Option<u32>, savetime: Option<u32>,
savefilepath: Option<String>, savefilepath: Option<String>,
time_sensitive_right_click: Option<bool>, time_sensitive_right_click: Option<bool>,
interface_scale: Option<f32>,
} }
fn color_param(key: &str, v: &Value) -> Result<Option<Color32>, String> { fn color_param(key: &str, v: &Value) -> Result<Option<Color32>, String> {
@@ -174,6 +182,10 @@ fn parse_ui(key: &str, v: &Value, c: &mut UiChanges) -> Result<bool, String> {
let n = v.as_f64().filter(|n| SNAP_MARKER_SIZES.contains(&(*n as f32)) && n.is_finite()).ok_or("snapMarkerSize must be from 5 to 31")?; let n = v.as_f64().filter(|n| SNAP_MARKER_SIZES.contains(&(*n as f32)) && n.is_finite()).ok_or("snapMarkerSize must be from 5 to 31")?;
c.snap_marker_size = Some(n as f32); c.snap_marker_size = Some(n as f32);
} }
"interfaceScale" => {
let n = v.as_f64().filter(|n| n.is_finite() && INTERFACE_SCALES.contains(&(*n as f32))).ok_or("interfaceScale must be from 0.5 to 3")?;
c.interface_scale = Some(n as f32);
}
"saveVersion" => { "saveVersion" => {
let name = v.as_str().map_or_else(|| v.to_string(), str::to_string); let name = v.as_str().map_or_else(|| v.to_string(), str::to_string);
c.save_version = Some(crate::SaveVersion::parse(name.trim()).ok_or("saveVersion must be 2000, 2004, 2007, 2010, 2013 or 2018")?); c.save_version = Some(crate::SaveVersion::parse(name.trim()).ok_or("saveVersion must be 2000, 2004, 2007, 2010, 2013 or 2018")?);
@@ -210,6 +222,7 @@ fn ui_json(app: &CadApp) -> Map<String, Value> {
m.insert("paperBackground".into(), color_json(o.paper_background)); m.insert("paperBackground".into(), color_json(o.paper_background));
m.insert("selectionColor".into(), color_json(o.selection_color)); m.insert("selectionColor".into(), color_json(o.selection_color));
m.insert("snapMarkerSize".into(), json!(o.snap_marker_size)); m.insert("snapMarkerSize".into(), json!(o.snap_marker_size));
m.insert("interfaceScale".into(), json!(o.interface_scale));
m.insert("saveVersion".into(), json!(app.ui.save_version)); m.insert("saveVersion".into(), json!(app.ui.save_version));
m.insert("SAVETIME".into(), json!(cadcraft_engine::autosave::savetime())); m.insert("SAVETIME".into(), json!(cadcraft_engine::autosave::savetime()));
m.insert("SAVEFILEPATH".into(), json!(cadcraft_engine::autosave::savefilepath())); m.insert("SAVEFILEPATH".into(), json!(cadcraft_engine::autosave::savefilepath()));
@@ -272,6 +285,9 @@ fn run_json(app: &mut CadApp, params: &Value) -> Result<Value, String> {
if let Some(n) = ui.snap_marker_size { if let Some(n) = ui.snap_marker_size {
app.options.snap_marker_size = n; app.options.snap_marker_size = n;
} }
if let Some(n) = ui.interface_scale {
app.options.interface_scale = n;
}
if let Some(v) = ui.save_version { if let Some(v) = ui.save_version {
app.ui.save_version = v.year().to_string(); app.ui.save_version = v.year().to_string();
} }
@@ -314,6 +330,7 @@ impl CadApp {
"selectionColor": color_json(self.options.selection_color), "selectionColor": color_json(self.options.selection_color),
"snapMarkerSize": self.options.snap_marker_size, "snapMarkerSize": self.options.snap_marker_size,
"timeSensitiveRightClick": self.options.time_sensitive_right_click, "timeSensitiveRightClick": self.options.time_sensitive_right_click,
"interfaceScale": self.options.interface_scale,
}) })
.to_string() .to_string()
} }
@@ -329,7 +346,9 @@ impl CadApp {
cadcraft_engine::cmd::options::restore(&mut self.session, vars); cadcraft_engine::cmd::options::restore(&mut self.session, vars);
} }
let mut c = UiChanges::default(); let mut c = UiChanges::default();
for key in ["systemCursor", "modelBackground", "paperBackground", "selectionColor", "snapMarkerSize", "timeSensitiveRightClick"] { for key in
["systemCursor", "modelBackground", "paperBackground", "selectionColor", "snapMarkerSize", "timeSensitiveRightClick", "interfaceScale"]
{
if let Some(value) = v.get(key) { if let Some(value) = v.get(key) {
let _ = parse_ui(key, value, &mut c); let _ = parse_ui(key, value, &mut c);
} }
@@ -353,6 +372,9 @@ impl CadApp {
if let Some(b) = c.time_sensitive_right_click { if let Some(b) = c.time_sensitive_right_click {
self.options.time_sensitive_right_click = b; self.options.time_sensitive_right_click = b;
} }
if let Some(n) = c.interface_scale {
self.options.interface_scale = n;
}
} }
} }
+46
View File
@@ -107,3 +107,49 @@ fn autosave_save_version_and_right_click_in_options() {
assert!(text.contains("Cancel") && text.contains("Snap Overrides"), "the menu opens:\n{text}"); assert!(text.contains("Cancel") && text.contains("Snap Overrides"), "the menu opens:\n{text}");
assert!(a.session.prompt_text().contains("next point"), "no Enter: {}", a.session.prompt_text()); assert!(a.session.prompt_text().contains("next point"), "no Enter: {}", a.session.prompt_text());
} }
#[test]
fn interface_scale_in_options() {
let mut a = app();
assert_eq!(a.run("options", json!({})).unwrap()["interfaceScale"], json!(1.0));
let r = a.run("options", json!({"interfaceScale": 1.5})).unwrap();
assert_eq!(r["interfaceScale"], json!(1.5));
let ctx = egui::Context::default();
frame(&mut a, &ctx, Vec::new(), 0.0);
assert_eq!(ctx.zoom_factor(), 1.5, "applied as egui's zoom factor");
// Refused all or nothing.
let before = a.run("options", json!({})).unwrap();
for bad in [
json!({"interfaceScale": 0.4, "PICKBOX": 9}),
json!({"interfaceScale": 3.1}),
json!({"interfaceScale": "big", "snapMarkerSize": 20}),
json!({"interfaceScale": null}),
json!({"interfaceScale": f64::NAN}),
] {
let err = a.run("options", bad.clone()).expect_err(&bad.to_string());
assert!(err.to_string().contains("interfaceScale must be from 0.5 to 3"), "{bad}: {err}");
assert_eq!(a.run("options", json!({})).unwrap(), before, "{bad} changed nothing");
}
// It survives a restart; an out-of-range saved value is skipped.
let mut b = app();
b.load_profile(&a.profile_json());
assert_eq!(b.options.interface_scale, 1.5);
let mut c = app();
c.load_profile(&a.profile_json().replace("\"interfaceScale\":1.5", "\"interfaceScale\":9.0"));
assert_eq!(c.options.interface_scale, 1.0);
// Reset brings back the system's own size.
assert_eq!(a.run("options", json!({"reset": true})).unwrap()["interfaceScale"], json!(1.0));
frame(&mut a, &ctx, Vec::new(), 0.1);
assert_eq!(ctx.zoom_factor(), 1.0);
// The Display tab shows it.
a.run("options", json!({"interfaceScale": 1.25})).unwrap();
a.ui.dialog = Some("options:display".into());
frame(&mut a, &ctx, Vec::new(), 0.2);
let text = frame(&mut a, &ctx, Vec::new(), 0.3);
assert!(text.contains("Interface scale") && text.contains("125 %"), "{text}");
}