Preferences: choose the interface font (#1408)
Adds an Interface font setting that leads the proportional family, with the default face and CJK/Hangul fallbacks kept behind it (#1290). Co-authored-by: DuBearte <DuBearte@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
5 files changed
+148
-9
No files matched your search
@@ -292,6 +292,9 @@ pub struct Prefs {
|
||||
pub language: String,
|
||||
/// Interface scale (1 = 100 %).
|
||||
pub ui_scale: f32,
|
||||
/// Interface font: an installed font family (or a font file), or empty for the built-in one
|
||||
/// (#1290). Menus, dialogs and labels use it; glyphs it lacks fall back as before.
|
||||
pub ui_font: String,
|
||||
/// Put the feature history above the parts browser in the left sidebar instead of along the
|
||||
/// bottom of the viewport.
|
||||
pub timeline_left: bool,
|
||||
@@ -398,6 +401,7 @@ impl Default for Prefs {
|
||||
theme: "dark".into(),
|
||||
language: "en".into(),
|
||||
ui_scale: 1.0,
|
||||
ui_font: String::new(),
|
||||
timeline_left: false,
|
||||
timeline_left_height: 360.0,
|
||||
timeline_left_browser_collapsed: false,
|
||||
@@ -441,6 +445,8 @@ pub struct PrefsWindow {
|
||||
snapshot: Option<Snapshot>,
|
||||
/// The up axis preference the view follows now (applied when it changes, and at startup).
|
||||
applied_up: Option<String>,
|
||||
/// The search text of the interface font list.
|
||||
font_filter: String,
|
||||
}
|
||||
|
||||
/// Open the dialog at a page.
|
||||
@@ -504,6 +510,54 @@ impl Snapshot {
|
||||
}
|
||||
}
|
||||
|
||||
/// The interface font (#1290): the built-in font first, then the installed families, with a
|
||||
/// search box. The choice applies at once, like the other preferences.
|
||||
fn ui_font_row(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let builtin = app.tr_label("Built-in").to_string();
|
||||
let current = if app.preferences.ui_font.trim().is_empty() { builtin.clone() } else { app.preferences.ui_font.clone() };
|
||||
let families = crate::sketch_text::families(ui.ctx());
|
||||
let mut chosen: Option<String> = None;
|
||||
let combo = egui::ComboBox::from_id_salt("pref_ui_font")
|
||||
.width(220.0)
|
||||
.height(340.0)
|
||||
.selected_text(current)
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||
.show_ui(ui, |ui| {
|
||||
let search = egui::TextEdit::singleline(&mut app.prefs_window.font_filter).hint_text(crate::i18n::tr("sketch_text.font_search"));
|
||||
ui.add(search.desired_width(f32::INFINITY));
|
||||
let filter = app.prefs_window.font_filter.trim().to_lowercase();
|
||||
egui::ScrollArea::vertical().max_height(280.0).min_scrolled_height(280.0).auto_shrink([false, false]).show(ui, |ui| {
|
||||
if builtin.to_lowercase().contains(&filter) && ui.selectable_label(app.preferences.ui_font.trim().is_empty(), &builtin).clicked() {
|
||||
chosen = Some(String::new());
|
||||
}
|
||||
match &families {
|
||||
Some(list) => {
|
||||
for family in list.iter().filter(|f| f.to_lowercase().contains(&filter)) {
|
||||
let item = ui.selectable_label(app.preferences.ui_font == *family, family);
|
||||
crate::scenario::publish_handle(&format!("prefs:font:{family}"), item.rect.center());
|
||||
if item.clicked() {
|
||||
chosen = Some(family.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ui.label(crate::i18n::tr_label("Listing the installed fonts…"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
crate::scenario::publish_handle("prefs:ui_font", combo.response.rect.center());
|
||||
if let Some(name) = chosen {
|
||||
if name.is_empty() || crate::fonts::ui_font(&name).is_some() {
|
||||
app.preferences.ui_font = name;
|
||||
} else {
|
||||
app.set_status(format!("The font `{name}` could not be read"), true);
|
||||
}
|
||||
app.prefs_window.font_filter.clear();
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore Defaults for one page.
|
||||
fn defaults(app: &mut SolveApp, page: usize) {
|
||||
let d = Prefs::default();
|
||||
@@ -513,6 +567,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
p.theme = d.theme;
|
||||
p.language = d.language;
|
||||
p.ui_scale = d.ui_scale;
|
||||
p.ui_font = d.ui_font;
|
||||
p.timeline_left = d.timeline_left;
|
||||
p.timeline_left_height = d.timeline_left_height;
|
||||
p.timeline_left_browser_collapsed = d.timeline_left_browser_collapsed;
|
||||
@@ -1004,6 +1059,11 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
);
|
||||
crate::scenario::publish_rect("prefs:scale", scale.rect);
|
||||
ui.end_row();
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
ui.label(app.tr_label("Interface font"));
|
||||
ui_font_row(app, ui);
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label(app.tr("ui.feature_history"));
|
||||
let timeline_bottom = app.tr("ui.timeline_bottom").to_string();
|
||||
let timeline_left = app.tr("ui.timeline_left_sidebar").to_string();
|
||||
@@ -1429,6 +1489,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
if !crate::i18n::is_supported(&new.language) {
|
||||
return Err(format!("`language` must be one of: {}", crate::i18n::LOCALES.iter().map(|locale| locale.id).collect::<Vec<_>>().join(", ")));
|
||||
}
|
||||
if new.ui_font != app.preferences.ui_font && !new.ui_font.trim().is_empty() && crate::fonts::ui_font(&new.ui_font).is_none() {
|
||||
return Err(format!("`ui_font`: `{}` is not an installed font family or a readable font file", new.ui_font.trim()));
|
||||
}
|
||||
if !(0.5..=2.5).contains(&new.ui_scale) || !(0.25..=4.0).contains(&new.look_at_speed) {
|
||||
return Err("`ui_scale` must be 0.5…2.5 and `look_at_speed` 0.25…4".into());
|
||||
}
|
||||
@@ -1458,6 +1521,26 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// The interface font (#1290): a font that can be read, or empty for the built-in one; saved
|
||||
/// with the others and reset by Restore Defaults.
|
||||
#[test]
|
||||
fn ui_font_is_checked_saved_and_restored() {
|
||||
use serde_json::json;
|
||||
let mut app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
assert_eq!(app.preferences.ui_font, "");
|
||||
let family = solidcraft_engine::sketch::DEFAULT_FONT_FAMILY;
|
||||
let out = super::set(&mut app, &json!({ "ui_font": family })).unwrap();
|
||||
assert_eq!(out["ui_font"], family);
|
||||
let e = super::set(&mut app, &json!({"ui_font": "No Such Font 1290"})).unwrap_err();
|
||||
assert!(e.contains("ui_font"), "{e}");
|
||||
assert_eq!(app.preferences.ui_font, family, "a refused font leaves the choice as it was");
|
||||
let mut other = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
other.load_prefs(&app.prefs());
|
||||
assert_eq!(other.preferences.ui_font, family);
|
||||
super::defaults(&mut app, 0);
|
||||
assert_eq!(app.preferences.ui_font, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_fields_take_a_decimal_point_or_a_decimal_comma() {
|
||||
// #1347
|
||||
|
||||
Reference in new issue
Block a user