дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(appearance): fallback font for the custom UI font (#660)
Adds a UI fallback font under Settings › Appearance › Use custom font, so glyphs missing from the chosen UI font (e.g. CJK in Menlo) render in a font the user picks instead of the system default. The fallback joins the UI font stack used by menus, the project tree and dialogs, and resolves macOS user fonts through the shared font loader. Also keeps the custom font controls wrapping together at narrow widths and covers save/clear/disable, stack composition and settings round-trip in tests. Fixes #654 Co-authored-by: a-rookie-of-C-language <[email protected]>
Цей коміт міститься в:
1 джерело
f8d6f9df77
коміт
5c0a5239fa
11 files changed
+142
-38
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatPattern, formatTimestamp } from "@/platform/dateFormat";
|
||||
import { resolveThemeId } from "@/workbench/theme";
|
||||
import { nextZoom } from "../appearanceRuntime";
|
||||
import { applyAppearanceOptions, nextZoom } from "../appearanceRuntime";
|
||||
import { validateSettings } from "../model/schema";
|
||||
import { validateAppearanceExtras, validateNotifications } from "../model/behaviorSchema";
|
||||
|
||||
describe("appearance & behavior settings", () => {
|
||||
@@ -27,6 +28,27 @@ describe("appearance & behavior settings", () => {
|
||||
expect(validateAppearanceExtras({ showToolWindowBadges: 1 }).showToolWindowBadges).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the UI fallback font empty by default and trims a stored one", () => {
|
||||
expect(validateAppearanceExtras({}).uiFontFallback).toBe("");
|
||||
expect(validateAppearanceExtras({ uiFontFallback: " 思源黑体 " }).uiFontFallback).toBe("思源黑体");
|
||||
expect(validateAppearanceExtras({ uiFontFallback: 5 }).uiFontFallback).toBe("");
|
||||
});
|
||||
|
||||
it("applies and clears the UI fallback, and removes custom fonts when disabled", () => {
|
||||
const root = document.createElement("div");
|
||||
const settings = validateSettings({
|
||||
appearance: { useCustomFont: true, uiFontFamily: "Menlo", uiFontFallback: "Songti SC" },
|
||||
});
|
||||
applyAppearanceOptions(settings, root);
|
||||
expect(root.style.getPropertyValue("--font-ui")).toBe('"Menlo", "Songti SC", system-ui, sans-serif');
|
||||
settings.appearance.uiFontFallback = "";
|
||||
applyAppearanceOptions(settings, root);
|
||||
expect(root.style.getPropertyValue("--font-ui")).toBe('"Menlo", system-ui, sans-serif');
|
||||
settings.appearance.useCustomFont = false;
|
||||
applyAppearanceOptions(settings, root);
|
||||
expect(root.style.getPropertyValue("--font-ui")).toBe("");
|
||||
});
|
||||
|
||||
it("formats dates with an override pattern and pretty dates", () => {
|
||||
const d = new Date(2026, 0, 5, 14, 7);
|
||||
expect(formatPattern(d, "dd.MM.yyyy")).toBe("05.01.2026");
|
||||
|
||||
@@ -222,6 +222,24 @@ describe("Settings dialog", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("enables, saves and clears the custom UI fallback font", async () => {
|
||||
const dialog = await openDialog("appearance");
|
||||
const fallback = within(dialog).getByRole("combobox", { name: "UI fallback font" });
|
||||
const custom = within(dialog).getByRole("checkbox", { name: "Use custom font:" });
|
||||
expect(fallback).toBeDisabled();
|
||||
fireEvent.click(custom);
|
||||
expect(fallback).toBeEnabled();
|
||||
fireEvent.change(fallback, { target: { value: "Songti SC" } });
|
||||
expect(settingsSet).not.toHaveBeenCalled();
|
||||
await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })));
|
||||
expect(settingsSet).toHaveBeenCalledWith("global", "appearance.uiFontFallback", JSON.stringify("Songti SC"));
|
||||
fireEvent.change(fallback, { target: { value: "" } });
|
||||
await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })));
|
||||
expect(settingsSet).toHaveBeenCalledWith("global", "appearance.uiFontFallback", JSON.stringify(""));
|
||||
fireEvent.click(custom);
|
||||
expect(fallback).toBeDisabled();
|
||||
});
|
||||
|
||||
it("edits and saves independent custom font weights", async () => {
|
||||
const dialog = await openDialog("editor.font");
|
||||
for (const [label, value] of [
|
||||
|
||||
@@ -33,7 +33,7 @@ export function applyAppearanceOptions(s: ValidSettings, root: HTMLElement = doc
|
||||
if (size === DEFAULT_SETTINGS.appearance.uiFontSize) root.style.removeProperty(name);
|
||||
else root.style.setProperty(name, `${size + delta}px`);
|
||||
}
|
||||
if (a.useCustomFont) root.style.setProperty("--font-ui", uiFontFamily(a.uiFontFamily));
|
||||
if (a.useCustomFont) root.style.setProperty("--font-ui", uiFontFamily(a.uiFontFamily, a.uiFontFallback));
|
||||
else root.style.removeProperty("--font-ui");
|
||||
|
||||
flag(root, "compact", a.compactMode);
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface AppearanceExtras {
|
||||
zoom: number;
|
||||
useCustomFont: boolean;
|
||||
uiFontFamily: string;
|
||||
uiFontFallback: string;
|
||||
contrastScrollbars: boolean;
|
||||
redGreenVision: boolean;
|
||||
differentToolWindowBackground: boolean;
|
||||
@@ -72,6 +73,7 @@ export const DEFAULT_APPEARANCE_EXTRAS: AppearanceExtras = {
|
||||
zoom: 100,
|
||||
useCustomFont: false,
|
||||
uiFontFamily: "Inter",
|
||||
uiFontFallback: "",
|
||||
contrastScrollbars: false,
|
||||
redGreenVision: false,
|
||||
differentToolWindowBackground: false,
|
||||
@@ -123,6 +125,7 @@ export function validateAppearanceExtras(raw: unknown): AppearanceExtras {
|
||||
zoom: int(a.zoom, d.zoom, ZOOM_LIMITS),
|
||||
useCustomFont: b("useCustomFont"),
|
||||
uiFontFamily: typeof a.uiFontFamily === "string" && a.uiFontFamily.trim() ? a.uiFontFamily.trim() : d.uiFontFamily,
|
||||
uiFontFallback: typeof a.uiFontFallback === "string" ? a.uiFontFallback.trim() : d.uiFontFallback,
|
||||
contrastScrollbars: b("contrastScrollbars"),
|
||||
redGreenVision: b("redGreenVision"),
|
||||
differentToolWindowBackground: b("differentToolWindowBackground"),
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
/* "[x] Use custom font: [Inter] Size: [13]" */
|
||||
/* Inline option groups, with custom font controls wrapping together. */
|
||||
.fontRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -37,8 +37,17 @@
|
||||
.fontInput {
|
||||
width: 220px;
|
||||
}
|
||||
.sizeLabel {
|
||||
margin-left: 8px;
|
||||
.customFontRow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.fontOption {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.fontOption > label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sizeInput {
|
||||
width: 64px;
|
||||
|
||||
@@ -181,35 +181,53 @@ function ZoomRow() {
|
||||
function CustomFontRow() {
|
||||
const [on, setOn] = useSetting<boolean>("appearance.useCustomFont");
|
||||
const [family, setFamily] = useSetting<string>("appearance.uiFontFamily");
|
||||
const [fallback, setFallback] = useSetting<string>("appearance.uiFontFallback");
|
||||
const [size, setSize] = useSetting<number>("appearance.uiFontSize");
|
||||
const id = useId();
|
||||
const matched = useLabelMatch("Use custom font");
|
||||
return (
|
||||
<div className={clsx(styles.fontRow, matched && styles.matched)}>
|
||||
<Checkbox checked={on} onChange={setOn} label={<SearchLabel text="Use custom font:" />} />
|
||||
<FontPicker label="UI font" disabled={!on} value={family} onChange={setFamily} />
|
||||
<label className={clsx(styles.sizeLabel, !on && styles.disabled)} htmlFor={`${id}-size`}>
|
||||
<SearchLabel text="Size:" />
|
||||
</label>
|
||||
{on ? (
|
||||
<NumberInput
|
||||
id={`${id}-size`}
|
||||
value={size}
|
||||
onChange={setSize}
|
||||
{...LIMITS.uiFontSize}
|
||||
integer
|
||||
aria-label="UI font size"
|
||||
<div className={clsx(styles.fontRow, styles.customFontRow, matched && styles.matched)}>
|
||||
<span className={styles.fontOption}>
|
||||
<Checkbox checked={on} onChange={setOn} label={<SearchLabel text="Use custom font:" />} />
|
||||
<FontPicker label="UI font" disabled={!on} value={family} onChange={setFamily} />
|
||||
</span>
|
||||
<span className={styles.fontOption}>
|
||||
<label className={clsx(!on && styles.disabled)} htmlFor={`${id}-fallback`}>
|
||||
<SearchLabel text="Fallback font:" />
|
||||
</label>
|
||||
<FontPicker
|
||||
id={`${id}-fallback`}
|
||||
label="UI fallback font"
|
||||
disabled={!on}
|
||||
value={fallback}
|
||||
onChange={setFallback}
|
||||
allowEmpty
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={`${id}-size`}
|
||||
className={styles.sizeInput}
|
||||
value={String(size)}
|
||||
disabled
|
||||
aria-label="UI font size"
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.fontOption}>
|
||||
<label className={clsx(!on && styles.disabled)} htmlFor={`${id}-size`}>
|
||||
<SearchLabel text="Size:" />
|
||||
</label>
|
||||
{on ? (
|
||||
<NumberInput
|
||||
id={`${id}-size`}
|
||||
value={size}
|
||||
onChange={setSize}
|
||||
{...LIMITS.uiFontSize}
|
||||
integer
|
||||
aria-label="UI font size"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={`${id}-size`}
|
||||
className={styles.sizeInput}
|
||||
value={String(size)}
|
||||
disabled
|
||||
aria-label="UI font size"
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -531,7 +549,7 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
|
||||
{
|
||||
kind: "custom",
|
||||
label: "Use custom font",
|
||||
keywords: ["UI font size", "Size", "font family"],
|
||||
keywords: ["UI font size", "Size", "font family", "fallback font"],
|
||||
component: CustomFontRow,
|
||||
},
|
||||
{ kind: "toggle", key: "appearance.contrastScrollbars", label: "Use contrast scrollbars" },
|
||||
|
||||
Reference in new issue
Заблокувати користувача