feat(settings): an app-wide typeface, picked from the fonts already shipped
Settings > Appearance > Typography & Language gains a Font row. The catalogue is the SAME five the lyrics panel offers and the same slugs, because they are the same files: everything is bundled in qml/assets/fonts/, so a choice cannot fail on a machine that lacks the face and looks identical everywhere. "System" resolves to the window default, which is Inter. It applies LIVE, with no restart: the family sits on the ApplicationWindow, which every Text that does not name its own family inherits. The index rides its own qproperty rather than the settings document, because the consumer is the root window and making it parse the whole settingsJson on every republish to read one integer is the wrong trade. Same shape as theme_filter beside it. The loaders are lazy, exactly like the lyrics picker: a family nobody selected is never loaded, and one that has not finished loading falls back to Inter rather than rendering the app in a wrong face for a frame. Only the first label is translated — a typeface name is a proper noun. cc
This commit is contained in:
3 files changed
+97
-1
No files matched your search
@@ -178,7 +178,38 @@ ApplicationWindow {
|
||||
// the family app-wide, so QualityBadge can name it without carrying its
|
||||
// own loader into every row it renders in.
|
||||
FontLoader { id: lineSeed; source: "assets/fonts/LINESeedJP-Regular.ttf" }
|
||||
font.family: interRegular.status === FontLoader.Ready ? interRegular.name : "Sans Serif"
|
||||
|
||||
// ── Settings > Appearance > Typography & Language > Font ───────────────
|
||||
// The app-wide typeface. Every Text in the tree that does not name a
|
||||
// family of its own inherits THIS one, which is why the setting lives on
|
||||
// the ApplicationWindow and applies live with no restart.
|
||||
//
|
||||
// The catalog is settings_qt::APP_FONT_VALUES and the index rides
|
||||
// `QbzShell.appFontIndex` — a property of its own rather than a read out
|
||||
// of settingsJson, so the root window does not parse the whole settings
|
||||
// document to find one integer.
|
||||
//
|
||||
// LAZY, exactly like the lyrics panel's own picker
|
||||
// (shell/LyricsLinesView.qml:66-80): a family nobody selected is never
|
||||
// loaded, and a family that has not FINISHED loading falls back to Inter
|
||||
// rather than rendering the app in a wrong face for a frame.
|
||||
FontLoader { id: appMontserrat; source: QbzShell.appFontIndex === 2 ? "assets/fonts/Montserrat-VariableFont_wght.ttf" : "" }
|
||||
FontLoader { id: appNoto; source: QbzShell.appFontIndex === 3 ? "assets/fonts/NotoSans-VariableFont_wdth,wght.ttf" : "" }
|
||||
FontLoader { id: appSource; source: QbzShell.appFontIndex === 4 ? "assets/fonts/SourceSans3-VariableFont_wght.ttf" : "" }
|
||||
readonly property string appFontFamily: {
|
||||
if (QbzShell.appFontIndex === 1)
|
||||
return lineSeed.status === FontLoader.Ready ? lineSeed.name : ""
|
||||
if (QbzShell.appFontIndex === 2)
|
||||
return appMontserrat.status === FontLoader.Ready ? appMontserrat.name : ""
|
||||
if (QbzShell.appFontIndex === 3)
|
||||
return appNoto.status === FontLoader.Ready ? appNoto.name : ""
|
||||
if (QbzShell.appFontIndex === 4)
|
||||
return appSource.status === FontLoader.Ready ? appSource.name : ""
|
||||
return ""
|
||||
}
|
||||
font.family: window.appFontFamily !== ""
|
||||
? window.appFontFamily
|
||||
: (interRegular.status === FontLoader.Ready ? interRegular.name : "Sans Serif")
|
||||
|
||||
// Phase 23: every domain singleton boots (registers its Qt-thread
|
||||
// hop; QbzSession.boot additionally fires the app boot sequence).
|
||||
|
||||
@@ -1301,6 +1301,36 @@ const LANGUAGE_LABELS: &[&str] = &[
|
||||
const LANGUAGE_VALUES: &[&str] = &["auto", "en", "es", "fr", "de", "pt", "ru", "ja", "nl"];
|
||||
const UI_SCALE_LABELS: &[&str] = &["Extra small", "Small", "Default", "Large", "Extra large"];
|
||||
const UI_SCALE_VALUES: &[&str] = &["xs", "small", "default", "large", "xl"];
|
||||
/// The app-wide typeface (Settings > Appearance > Typography & Language).
|
||||
///
|
||||
/// The SAME five the lyrics panel offers (`lyrics_qt::FONTS`) and the same
|
||||
/// slugs, because they are the same files: everything here is BUNDLED in
|
||||
/// `qml/assets/fonts/`, so a choice cannot fail on a machine that does not
|
||||
/// have the face installed, and it looks identical everywhere. That is the
|
||||
/// Tauri build's mechanism, which is what the owner asked for.
|
||||
///
|
||||
/// Only "System" is translated — a typeface name is a proper noun.
|
||||
/// "System" resolves to the window default, which is Inter (Main.qml).
|
||||
const APP_FONT_LABELS: &[&str] = &[
|
||||
"System",
|
||||
"LINE Seed JP",
|
||||
"Montserrat",
|
||||
"Noto Sans",
|
||||
"Source Sans 3",
|
||||
];
|
||||
const APP_FONT_VALUES: &[&str] = &[
|
||||
"system",
|
||||
"line-seed-jp",
|
||||
"montserrat",
|
||||
"noto-sans",
|
||||
"source-sans-3",
|
||||
];
|
||||
|
||||
/// The persisted app-font choice as an index into [`APP_FONT_VALUES`].
|
||||
/// `pub` because shell_bridge seeds `app_font_index` from it at boot.
|
||||
pub fn app_font_index() -> i32 {
|
||||
index_of(APP_FONT_VALUES, &pref_str("app_font", "system"), 0)
|
||||
}
|
||||
const IMMERSIVE_SEARCH_LABELS: &[&str] =
|
||||
&["Disabled", "Replace current queue", "Play next", "Add to queue"];
|
||||
const IMMERSIVE_SEARCH_VALUES: &[&str] = &["disabled", "replace", "next", "queue"];
|
||||
@@ -1483,6 +1513,10 @@ pub struct SettingsDoc {
|
||||
pub ui_scales: Vec<String>,
|
||||
#[serde(rename = "uiScaleIndex")]
|
||||
pub ui_scale_index: i32,
|
||||
#[serde(rename = "appFonts")]
|
||||
pub app_fonts: Vec<String>,
|
||||
#[serde(rename = "appFontIndex")]
|
||||
pub app_font_index: i32,
|
||||
#[serde(rename = "immersiveSearchActions")]
|
||||
pub immersive_search_actions: Vec<String>,
|
||||
#[serde(rename = "immersiveSearchActionIndex")]
|
||||
@@ -1930,6 +1964,14 @@ pub async fn publish_snapshot() {
|
||||
language_index: index_of(LANGUAGE_VALUES, &pref_str("language", "auto"), 0),
|
||||
ui_scales: UI_SCALE_LABELS.iter().map(|l| qbz_i18n::t(l)).collect(),
|
||||
ui_scale_index: index_of(UI_SCALE_VALUES, &pref_str("ui_scale", "default"), 2),
|
||||
// Only the first label is a word; the rest are typeface names and
|
||||
// are passed through untranslated.
|
||||
app_fonts: APP_FONT_LABELS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| if i == 0 { qbz_i18n::t(l) } else { (*l).to_string() })
|
||||
.collect(),
|
||||
app_font_index: app_font_index(),
|
||||
immersive_search_actions: IMMERSIVE_SEARCH_LABELS
|
||||
.iter()
|
||||
.map(|l| qbz_i18n::t(l))
|
||||
@@ -2577,6 +2619,18 @@ pub async fn settings_select(runtime: &Arc<AppRuntime<LoggingAdapter>>, key: &st
|
||||
// Live switch (phase 20): trRev bump + doc republish.
|
||||
crate::apply_language(lang.to_string());
|
||||
}
|
||||
"app-font" => {
|
||||
let Some(font) = APP_FONT_VALUES.get(index) else {
|
||||
return;
|
||||
};
|
||||
save_pref("app_font", serde_json::json!(font));
|
||||
// LIVE, no restart: Main.qml rebinds the ApplicationWindow's
|
||||
// `font.family` off this property and every unstyled Text in the
|
||||
// tree inherits it.
|
||||
let idx = index as i32;
|
||||
crate::shell_bridge::ui(move |mut b| b.as_mut().set_app_font_index(idx));
|
||||
log::info!("[qbz-qt] app_font -> {font}");
|
||||
}
|
||||
"ui-scale" => {
|
||||
let Some(scale) = UI_SCALE_VALUES.get(index) else {
|
||||
return;
|
||||
|
||||
@@ -108,6 +108,15 @@ pub mod qbz_shell {
|
||||
#[qproperty(QString, theme_list_json)]
|
||||
// Dropdown filter: 0 All / 1 Dark / 2 Light (ui_prefs theme_filter).
|
||||
#[qproperty(i32, theme_filter)]
|
||||
// Settings > Appearance > Typography & Language > Font: an index into
|
||||
// settings_qt::APP_FONT_VALUES (0 = System = the window default).
|
||||
//
|
||||
// It rides its OWN property rather than the settings document because
|
||||
// the consumer is Main.qml — the ApplicationWindow whose `font.family`
|
||||
// every unstyled Text inherits — and making the root window parse the
|
||||
// whole settingsJson on every republish to read one integer is the
|
||||
// wrong trade. Same shape as `theme_filter` above.
|
||||
#[qproperty(i32, app_font_index)]
|
||||
// The custom-theme EDITOR state (custom_theme_qt.rs):
|
||||
// {"isDark":bool,"tokens":{"<kebab-key>":"#aarrggbb"}} — the eleven
|
||||
// editable base tokens plus the polarity, mirroring the Slint
|
||||
@@ -749,6 +758,7 @@ pub struct QbzShellRust {
|
||||
theme_slug: QString,
|
||||
theme_list_json: QString,
|
||||
theme_filter: i32,
|
||||
app_font_index: i32,
|
||||
custom_theme_json: QString,
|
||||
diagnostics_json: QString,
|
||||
log_viewer_json: QString,
|
||||
@@ -841,6 +851,7 @@ impl Default for QbzShellRust {
|
||||
theme_slug: QString::from(crate::theme_qt::current_slug().as_str()),
|
||||
theme_list_json: QString::from(crate::theme_qt::theme_list_json().as_str()),
|
||||
theme_filter: crate::theme_qt::theme_filter(),
|
||||
app_font_index: crate::settings_qt::app_font_index(),
|
||||
custom_theme_json: QString::from(crate::custom_theme_qt::state_json().as_str()),
|
||||
diagnostics_json: QString::from(
|
||||
crate::diagnostics_qt::empty_doc_json().as_str(),
|
||||
|
||||
Reference in new issue
Block a user