From 95a3e00b03d20935491859bceec785cd49ed6507 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Fri, 2 Oct 2026 23:04:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(fonts):=20=E5=A4=96=E8=A7=82=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=96=B0=E5=A2=9E=E7=95=8C=E9=9D=A2/=E7=AD=89?= =?UTF-8?q?=E5=AE=BD=E5=AD=97=E4=BD=93=E9=80=89=E6=8B=A9=E5=99=A8=EF=BC=8C?= =?UTF-8?q?=E5=90=AB=E4=B8=AD=E8=8B=B1=E7=B9=81=E4=B8=AD=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 47 +++ .../ui/settings/AppearanceSettings.vue | 269 +++++++++++++++++- .../app-frontend/src/locales/en-US/index.json | 51 ++++ .../app-frontend/src/locales/zh-CN/index.json | 51 ++++ .../app-frontend/src/locales/zh-TW/index.json | 51 ++++ 5 files changed, 468 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5354349..9782bb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1889,6 +1889,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2660c5e9157bf76d2db1294e4a9feba604ef610819a3b591088d0d8392a3290f" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -3477,6 +3499,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -5102,6 +5133,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rsa" version = "0.9.10" @@ -5758,6 +5795,15 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.2" @@ -6912,6 +6958,7 @@ dependencies = [ "either", "enumset", "flate2", + "fontdb", "futures", "hyper", "hyper-util", diff --git a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue b/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue index 0d4089e..279ed0e 100644 --- a/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/AppearanceSettings.vue @@ -22,7 +22,7 @@ import { convertFileSrc } from '@tauri-apps/api/core' import { appDataDir, join } from '@tauri-apps/api/path' import { open } from '@tauri-apps/plugin-dialog' import { exists, mkdir, readFile, remove, writeFile } from '@tauri-apps/plugin-fs' -import { computed, ref, watch } from 'vue' +import { computed, onMounted, ref, watch } from 'vue' import { get, set } from '@/helpers/settings.ts' import { getOS } from '@/helpers/utils' @@ -41,6 +41,19 @@ import { parseCustomAccentColor, } from '@/store/theme.ts' +import { + buildFontFamilyOptions, + canonicalFontFamily, + DEFAULT_MONO_FONT_STACK, + DEFAULT_UI_FONT_STACK, + type FontFamilyOption, + type FontFamilyOptionOrDivider, + optionValueToFontSetting, + resolveFontFamily, + type SystemFontFamily, +} from '@/helpers/font-family.ts' +import { getSystemFontFamilies } from '@/helpers/system-fonts.ts' + import SettingsRow from './SettingsRow.vue' import SettingsSection from './SettingsSection.vue' @@ -363,6 +376,63 @@ const messages = defineMessages({ defaultMessage: 'Hide the downloads button in the sidebar when there are no active download tasks.', }, + fontsTitle: { id: 'app.appearance-settings.fonts.title', defaultMessage: 'Fonts' }, + fontsDescription: { + id: 'app.appearance-settings.fonts.description', + defaultMessage: 'Choose the fonts the launcher interface and its logs are rendered in.', + }, + uiFontTitle: { id: 'app.appearance-settings.fonts.ui-font', defaultMessage: 'Interface font' }, + uiFontDescription: { + id: 'app.appearance-settings.fonts.ui-font-description', + defaultMessage: + 'Used across the launcher interface. Only fonts installed on this system are listed.', + }, + monoFontTitle: { + id: 'app.appearance-settings.fonts.mono-font', + defaultMessage: 'Monospace font', + }, + monoFontDescription: { + id: 'app.appearance-settings.fonts.mono-font-description', + defaultMessage: 'Used for logs, the server console, and other monospace content.', + }, + fontDefault: { + id: 'app.appearance-settings.fonts.default', + defaultMessage: 'Launcher default', + }, + fontMonospace: { + id: 'app.appearance-settings.fonts.monospace', + defaultMessage: 'Monospace', + }, + fontMissing: { + id: 'app.appearance-settings.fonts.missing', + defaultMessage: 'Not installed', + }, + fontSearchPlaceholder: { + id: 'app.appearance-settings.fonts.search-placeholder', + defaultMessage: 'Search installed fonts', + }, + fontNoResults: { + id: 'app.appearance-settings.fonts.no-results', + defaultMessage: 'No matching fonts', + }, + fontLoadFailed: { + id: 'app.appearance-settings.fonts.load-failed', + defaultMessage: "Couldn't read the list of installed fonts.", + }, + fontRetry: { id: 'app.appearance-settings.fonts.retry', defaultMessage: 'Retry' }, + fontReset: { + id: 'app.appearance-settings.fonts.reset', + defaultMessage: 'Reset', + }, + fontPreviewLabel: { id: 'app.appearance-settings.fonts.preview', defaultMessage: 'Preview' }, + fontUiPreviewSample: { + id: 'app.appearance-settings.fonts.ui-preview-sample', + defaultMessage: 'The quick brown fox jumps over the lazy dog.', + }, + fontMonoPreviewSample: { + id: 'app.appearance-settings.fonts.mono-preview-sample', + defaultMessage: 'const x = () => 0; // monospace', + }, }) const os = ref(await getOS()) @@ -373,6 +443,78 @@ const customBackgroundPreview = computed(() => : null, ) +const systemFonts = ref([]) +const systemFontsLoading = ref(true) +const systemFontsFailed = ref(false) + +async function loadSystemFonts() { + systemFontsLoading.value = true + systemFontsFailed.value = false + + try { + systemFonts.value = await getSystemFontFamilies() + } catch (error) { + systemFontsFailed.value = true + console.warn('Failed to read the list of installed fonts', error) + } finally { + systemFontsLoading.value = false + } +} + +function selectedOptionLabel(options: FontFamilyOptionOrDivider[], value: string): string { + const option = options.find( + (entry): entry is FontFamilyOption => 'value' in entry && entry.value === value, + ) + + return option?.label ?? '' +} + +const uiFontOptions = computed(() => + buildFontFamilyOptions(systemFonts.value, { + defaultLabel: formatMessage(messages.fontDefault), + missingLabel: formatMessage(messages.fontMissing), + selected: settings.value.ui_font, + }), +) + +const monoFontOptions = computed(() => + buildFontFamilyOptions(systemFonts.value, { + defaultLabel: formatMessage(messages.fontDefault), + missingLabel: formatMessage(messages.fontMissing), + monospaceLabel: formatMessage(messages.fontMonospace), + selected: settings.value.mono_font, + groupMonospaced: true, + }), +) + +const uiFontSelection = computed({ + get: () => canonicalFontFamily(systemFonts.value, settings.value.ui_font), + set: (value: string) => { + settings.value.ui_font = optionValueToFontSetting(value) + }, +}) + +const monoFontSelection = computed({ + get: () => canonicalFontFamily(systemFonts.value, settings.value.mono_font), + set: (value: string) => { + settings.value.mono_font = optionValueToFontSetting(value) + }, +}) + +const uiFontSearchValue = computed(() => + selectedOptionLabel(uiFontOptions.value, uiFontSelection.value), +) +const monoFontSearchValue = computed(() => + selectedOptionLabel(monoFontOptions.value, monoFontSelection.value), +) + +const uiFontPreview = computed(() => + resolveFontFamily(settings.value.ui_font, DEFAULT_UI_FONT_STACK), +) +const monoFontPreview = computed(() => + resolveFontFamily(settings.value.mono_font, DEFAULT_MONO_FONT_STACK), +) + function updateUiScale(value: number) { const clamped = Math.min(200, Math.max(50, Math.round(value))) setUiScale(clamped) @@ -489,6 +631,8 @@ watch( settings.value.transparent_background_blur, settings.value.sidebar_instance_count, settings.value.close_behavior, + settings.value.ui_font, + settings.value.mono_font, ] as const, ([ path, @@ -499,6 +643,8 @@ watch( transparentBlur, sidebarInstanceCount, closeBehavior, + uiFont, + monoFont, ]) => { themeStore.customBackgroundPath = path themeStore.customBackgroundBlur = blur @@ -509,10 +655,19 @@ watch( themeStore.setTransparentBackgroundClass() themeStore.sidebarInstanceCount = sidebarInstanceCount themeStore.closeBehavior = closeBehavior as CloseBehavior + themeStore.uiFont = uiFont + themeStore.monoFont = monoFont + themeStore.setUiFont() + themeStore.setMonoFont() }, { immediate: true }, ) +onMounted(() => { + if (props.scope === 'interface') { + void loadSystemFonts() + } +}) watch( settings, async () => { @@ -1148,6 +1303,118 @@ watch( + + + + + + + + + + + + +
+ {{ formatMessage(messages.fontLoadFailed) }} + +
+
+