Файли
openidea/src/platform/systemFonts.ts
T
Walter Wangтаbghtyu 00693334f7 feat(settings): add editable font search and refresh (#251)
The font pickers (editor, fallback, UI, terminal) become editable comboboxes: type to search installed families or enter any family name, and Refresh re-reads the system fonts. ide-fonts' cached family list is now refreshable (DirectWrite re-queries the system collection with checkForUpdates; CoreText and fontconfig are read anew), exposed through system_font_families(refresh). Before the first native result the pickers fall back to Canvas detection of common families; saved names and the last catalog are kept when a refresh fails. Fonts are enumerated on the local desktop in remote-development windows too.

Co-authored-by: bghtyu <[email protected]>
2026-10-29 23:35:33 +08:00

73 рядки
2.7 KiB
TypeScript

/** Shared suggestions only: editor, UI and terminal preferences remain independent. */
import { useEffect } from "react";
import { detectCandidateFonts } from "./candidateFonts";
import { BUNDLED_MONO_FAMILY, BUNDLED_UI_FAMILY } from "./fonts";
import { create } from "zustand";
import { isTauri } from "@tauri-apps/api/core";
import { call, commands, type FontFamily } from "@/platform/ipc";
export type FontStatus = "idle" | "loading" | "ready" | "empty" | "error" | "browser";
export const bundledFonts: FontFamily[] = [
{ name: BUNDLED_MONO_FAMILY, monospaced: true },
{ name: BUNDLED_UI_FAMILY, monospaced: false },
];
export function mergeFontFamilies(fonts: FontFamily[]): FontFamily[] {
const unique = new Map<string, FontFamily>();
for (const font of [...fonts, ...bundledFonts]) {
if (font.name.trim()) unique.set(font.name.toLocaleLowerCase(), font);
}
return [...unique.values()].sort((a, b) => a.name.localeCompare(b.name));
}
export type FontSource = "bundled" | "limited" | "native";
export const useSystemFontStore = create<{ fonts: FontFamily[]; status: FontStatus; source: FontSource }>(() => ({
fonts: bundledFonts,
status: "idle",
source: "bundled",
}));
let pending: Promise<void> | null = null;
function fallback(status: "error" | "browser") {
// Even an empty successful native catalog is authoritative. A failed refresh
// must never replace it with the old, necessarily incomplete candidate list.
if (useSystemFontStore.getState().source === "native") {
useSystemFontStore.setState({ status });
return;
}
let candidates: FontFamily[] = [];
try {
candidates = detectCandidateFonts();
} catch {
// A failing detector must not break settings or discard manual preferences.
}
useSystemFontStore.setState({ fonts: mergeFontFamilies(candidates), source: "limited", status });
}
export function loadSystemFonts(refresh = false): Promise<void> {
if (pending) return pending;
if (!refresh && useSystemFontStore.getState().status !== "idle") return Promise.resolve();
if (!isTauri()) {
fallback("browser");
return Promise.resolve();
}
useSystemFontStore.setState({ status: "loading" });
pending = Promise.resolve()
.then(() => call(commands.systemFontFamilies(refresh)))
.then((fonts) =>
useSystemFontStore.setState({
fonts: mergeFontFamilies(fonts),
status: fonts.length ? "ready" : "empty",
source: "native",
}),
)
.catch(() => fallback("error"))
.finally(() => {
pending = null;
});
return pending;
}
export function useSystemFonts() {
const state = useSystemFontStore();
useEffect(() => {
void loadSystemFonts();
}, []);
return state;
}