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 Показувати системний курсор
Animation Effects Ефекти анімації
Animation effects Ефекти анімації
Interface scale Масштаб інтерфейсу
Import... Імпорт...
Export... Експорт...
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,
/// Whether egui was last told to animate (`None` until the first frame).
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 quit_requested: bool,
/// 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_follows_system: false,
shown_animations: None,
shown_interface_scale: None,
frame_ms: 0.0,
quit_requested: false,
quit_confirmed: false,
@@ -561,6 +564,13 @@ impl CadApp {
if !animate {
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.finish_opening();
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.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"));
color_row(ui, app.options.model_background, Tokens::DARK.canvas, "modelBackground", changes);
ui.end_row();
+23 -1
View File
@@ -21,6 +21,7 @@
//! | `modelBackground`, `paperBackground` | `"#rrggbb"`, or `null` for the default |
//! | `selectionColor` | the colour selected objects are highlighted in: `"#rrggbb"`, or `null` for the theme's |
//! | `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
//! 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.
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).
#[derive(Clone, Copy, Debug, PartialEq)]
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
/// (AutoCAD's time-sensitive right-click). Off: a right-click always opens the menu.
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 {
@@ -76,6 +82,7 @@ impl Default for UiOptions {
snap_marker_size: theme::SNAP_MARKER,
selection_color: None,
time_sensitive_right_click: true,
interface_scale: 1.0,
}
}
}
@@ -141,6 +148,7 @@ struct UiChanges {
savetime: Option<u32>,
savefilepath: Option<String>,
time_sensitive_right_click: Option<bool>,
interface_scale: Option<f32>,
}
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")?;
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" => {
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")?);
@@ -210,6 +222,7 @@ fn ui_json(app: &CadApp) -> Map<String, Value> {
m.insert("paperBackground".into(), color_json(o.paper_background));
m.insert("selectionColor".into(), color_json(o.selection_color));
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("SAVETIME".into(), json!(cadcraft_engine::autosave::savetime()));
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 {
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 {
app.ui.save_version = v.year().to_string();
}
@@ -314,6 +330,7 @@ impl CadApp {
"selectionColor": color_json(self.options.selection_color),
"snapMarkerSize": self.options.snap_marker_size,
"timeSensitiveRightClick": self.options.time_sensitive_right_click,
"interfaceScale": self.options.interface_scale,
})
.to_string()
}
@@ -329,7 +346,9 @@ impl CadApp {
cadcraft_engine::cmd::options::restore(&mut self.session, vars);
}
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) {
let _ = parse_ui(key, value, &mut c);
}
@@ -353,6 +372,9 @@ impl CadApp {
if let Some(b) = c.time_sensitive_right_click {
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!(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}");
}