From b57219f3480f5629960be409273e44842f8fa9d3 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Fri, 2 Oct 2026 21:33:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(fonts):=20=E5=90=8E=E7=AB=AF=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E5=91=BD=E4=BB=A4=20fonts=5Fget=5Fsystem=5Ffonts=20?= =?UTF-8?q?=E5=8F=8A=20ui=5Ffont/mono=5Ffont=20=E8=AE=BE=E7=BD=AE=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + apps/app/Cargo.toml | 1 + apps/app/build.rs | 8 ++ apps/app/src/api/fonts.rs | 91 +++++++++++++++++++ apps/app/src/api/mod.rs | 1 + apps/app/src/main.rs | 1 + .../20261002020000_add-font-settings.sql | 2 + packages/app-lib/src/state/settings.rs | 16 ++++ 8 files changed, 121 insertions(+) create mode 100644 apps/app/src/api/fonts.rs create mode 100644 packages/app-lib/migrations/20261002020000_add-font-settings.sql diff --git a/Cargo.toml b/Cargo.toml index 2293561..1338da0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ encoding_rs = "0.8.35" enumset = "1.1.10" eyre = "0.6.12" flate2 = "1.1.4" +fontdb = "0.24.0" fs4 = { version = "0.13.1", default-features = false } futures = "0.3.31" futures-lite = "2.6.1" diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml index fe01a4b..2b66379 100644 --- a/apps/app/Cargo.toml +++ b/apps/app/Cargo.toml @@ -16,6 +16,7 @@ dashmap = { workspace = true } either = { workspace = true } enumset = { workspace = true, features = ["serde"] } flate2 = { workspace = true } +fontdb = { workspace = true } futures = { workspace = true } hyper = { workspace = true, features = ["server"] } hyper-util = { workspace = true } diff --git a/apps/app/build.rs b/apps/app/build.rs index 1ab3c38..7bd7295 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -697,6 +697,14 @@ fn main() { DefaultPermissionRule::AllowAllCommands, ), ) + .plugin( + "fonts", + InlinedPlugin::new() + .commands(&["fonts_get_system_fonts"]) + .default_permission( + DefaultPermissionRule::AllowAllCommands, + ), + ) .plugin( "drop", InlinedPlugin::new() diff --git a/apps/app/src/api/fonts.rs b/apps/app/src/api/fonts.rs new file mode 100644 index 0000000..54bfe6f --- /dev/null +++ b/apps/app/src/api/fonts.rs @@ -0,0 +1,91 @@ +use serde::Serialize; +use std::collections::HashMap; +use std::sync::OnceLock; + +use crate::api::Result; + +#[derive(Debug, Clone, Serialize)] +pub struct SystemFontFamily { + pub family: String, + pub monospaced: bool, +} + +/// Scanning every installed face costs a few hundred milliseconds, and the +/// collection only changes between sessions, so it is read once per process. +static SYSTEM_FONTS: OnceLock> = OnceLock::new(); + +type FontFamilies = [(String, fontdb::Language)]; + +/// fontdb lists the English US family first when the font has one, but the +/// lookup is explicit so a face with an unusual name table still resolves to +/// the name the webview matches against. +fn english_family_name(families: &FontFamilies) -> Option<&str> { + families + .iter() + .find(|(_, language)| { + *language == fontdb::Language::English_UnitedStates + }) + .or_else(|| families.first()) + .map(|(name, _)| name.trim()) + .filter(|name| !name.is_empty()) +} + +#[derive(Default)] +struct FamilyAccumulator { + families: HashMap, +} + +impl FamilyAccumulator { + fn add(&mut self, families: &FontFamilies, monospaced: bool) { + let Some(name) = english_family_name(families) else { + return; + }; + + self.families + .entry(name.to_owned()) + .and_modify(|flag| *flag |= monospaced) + .or_insert(monospaced); + } + + fn into_sorted_families(self) -> Vec { + let mut families: Vec = self + .families + .into_iter() + .map(|(family, monospaced)| SystemFontFamily { family, monospaced }) + .collect(); + families.sort_by_cached_key(|entry| entry.family.to_lowercase()); + families + } +} + +fn collect_system_fonts() -> Vec { + let mut database = fontdb::Database::new(); + database.load_system_fonts(); + + let mut accumulator = FamilyAccumulator::default(); + for face in database.faces() { + accumulator.add(&face.families, face.monospaced); + } + + accumulator.into_sorted_families() +} + +#[tauri::command] +pub async fn fonts_get_system_fonts() -> Result> { + if let Some(fonts) = SYSTEM_FONTS.get() { + return Ok(fonts.clone()); + } + + let fonts = tauri::async_runtime::spawn_blocking(collect_system_fonts) + .await + .map_err(std::io::Error::other)?; + + // A concurrent caller may have won the race; the scan is deterministic. + Ok(SYSTEM_FONTS.get_or_init(|| fonts).clone()) +} + +pub fn init() -> tauri::plugin::TauriPlugin { + tauri::plugin::Builder::new("fonts") + .invoke_handler(tauri::generate_handler![fonts_get_system_fonts]) + .build() +} \ No newline at end of file diff --git a/apps/app/src/api/mod.rs b/apps/app/src/api/mod.rs index 544509a..6c46a21 100644 --- a/apps/app/src/api/mod.rs +++ b/apps/app/src/api/mod.rs @@ -36,6 +36,7 @@ pub mod curseforge; pub mod datapacks; pub mod drop; pub mod files; +pub mod fonts; pub mod friends; pub mod worlds; diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 2467aaa..cfd1486 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -947,6 +947,7 @@ fn main() { .plugin(api::datapacks::init()) .plugin(api::drop::init()) .plugin(api::files::init()) + .plugin(api::fonts::init()) .plugin(api::friends::init()) .plugin(api::worlds::init()) .plugin(api::terracotta::init()) diff --git a/packages/app-lib/migrations/20261002020000_add-font-settings.sql b/packages/app-lib/migrations/20261002020000_add-font-settings.sql new file mode 100644 index 0000000..f9ecabd --- /dev/null +++ b/packages/app-lib/migrations/20261002020000_add-font-settings.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings ADD COLUMN ui_font TEXT; +ALTER TABLE settings ADD COLUMN mono_font TEXT; \ No newline at end of file diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs index 019ddf2..053541e 100644 --- a/packages/app-lib/src/state/settings.rs +++ b/packages/app-lib/src/state/settings.rs @@ -145,6 +145,10 @@ pub struct Settings { #[serde(default = "default_ui_scale")] pub ui_scale: f64, #[serde(default)] + pub ui_font: Option, + #[serde(default)] + pub mono_font: Option, + #[serde(default)] pub close_behavior: String, #[serde(default)] pub auto_hide_downloads_button: bool, @@ -289,6 +293,8 @@ impl Settings { json(home_widgets) as \"home_widgets?: String\", auto_hide_downloads_button, json(terracotta_public_nodes) terracotta_public_nodes, ui_scale, + ui_font, + mono_font, version FROM settings " @@ -362,6 +368,8 @@ impl Settings { transparent_background_blur: res.transparent_background_blur == 1, sidebar_instance_count: res.sidebar_instance_count as u32, ui_scale: res.ui_scale, + ui_font: res.ui_font, + mono_font: res.mono_font, close_behavior, auto_hide_downloads_button: res.auto_hide_downloads_button == 1, home_layout: HomeLayout::from_string(&res.home_layout), @@ -665,6 +673,14 @@ impl Settings { .bind(self.ui_scale) .execute(exec) .await?; + sqlx::query("UPDATE settings SET ui_font = ? WHERE id = 0") + .bind(&self.ui_font) + .execute(exec) + .await?; + sqlx::query("UPDATE settings SET mono_font = ? WHERE id = 0") + .bind(&self.mono_font) + .execute(exec) + .await?; Ok(()) }