From e1ff15ee911fb55392630cd95d227dee98d6440b Mon Sep 17 00:00:00 2001 From: Rayyan34 Date: Mon, 2 Nov 2026 01:31:59 -0700 Subject: [PATCH] feat(settings): preview each font in its own typeface in font pickers (#33254) * feat(settings): preview each font in its own typeface in font pickers * perf(settings): skip offscreen font rows so opening the picker loads only visible fonts * fix(settings): keep symbol font names readable in font pickers * fix(settings): size skipped font rows by their content box --------- Co-authored-by: Neil --- .../components/settings/FontAutocomplete.tsx | 28 +++++++++++++++++-- ...ngsFormControls.font-autocomplete.test.tsx | 27 ++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/settings/FontAutocomplete.tsx b/src/renderer/src/components/settings/FontAutocomplete.tsx index 10aadb07022..6e39e9dadeb 100644 --- a/src/renderer/src/components/settings/FontAutocomplete.tsx +++ b/src/renderer/src/components/settings/FontAutocomplete.tsx @@ -7,6 +7,27 @@ import { ScrollArea } from '../ui/scroll-area' import { filterFontSuggestions, getRenderedFontSuggestions } from './settings-form-option-filter' import { useInstalledFontsUnavailable } from './installed-font-list' import { translate } from '@/i18n/i18n' +import { buildAppFontFamily } from '@/lib/app-font-family' + +// Why: these OS-shipped fonts draw pictographs or Greek for Latin letters, so their own name would be unreadable. +const SYMBOL_FONT_FAMILIES = new Set([ + 'webdings', + 'wingdings', + 'wingdings 2', + 'wingdings 3', + 'marlett', + 'symbol', + 'mt extra', + 'bookshelf symbol 7', + 'd050000l', + 'standard symbols ps' +]) + +function fontPreviewStyle(font: string): React.CSSProperties | undefined { + return font && !SYMBOL_FONT_FAMILIES.has(font.toLowerCase()) + ? { fontFamily: buildAppFontFamily(font) } + : undefined +} type FontAutocompleteProps = { value: string @@ -354,13 +375,16 @@ export function FontAutocomplete({ onMouseDown={(e) => e.preventDefault()} onMouseEnter={() => setHighlightedIndex(sourceIndex)} onClick={() => commitValue(font)} - className={`flex w-full items-center justify-between rounded-sm px-3 py-2 text-left text-sm transition-colors ${ + // Why content-visibility: each row loads its own font, so skip offscreen rows until scrolled to; 20px is the line height inside py-2. + className={`flex w-full items-center justify-between rounded-sm px-3 py-2 text-left text-sm transition-colors [contain-intrinsic-size:auto_20px] [content-visibility:auto] ${ sourceIndex === highlightedIndex ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/60' }`} > - {font || defaultOptionLabel} + + {font || defaultOptionLabel} + {font === value ? : null} )) diff --git a/src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx b/src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx index 7e45aaf7853..dfdbb68e419 100644 --- a/src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx +++ b/src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx @@ -148,6 +148,33 @@ describe('FontAutocomplete', () => { expectOptionsToBePortaled() }) + it('renders each option name in its own typeface and leaves the input in the UI font', async () => { + const fonts = ['Arial', 'Fira Code', 'Georgia'] + + function Harness(): ReactNode { + const [value, setValue] = useState('Arial') + return + } + + await act(async () => { + root.render() + }) + await act(async () => { + getInput().focus() + }) + + const optionFamilies = Array.from( + document.querySelectorAll('[role="option"]') + ).map((option) => option.querySelector('span')?.style.fontFamily ?? '') + + const primaryFamilies = optionFamilies.map((family) => + family.split(',')[0]?.replaceAll('"', '') + ) + + expect(primaryFamilies).toEqual(fonts) + expect(getInput().style.fontFamily).toBe('') + }) + it('bounds the portaled list to the available popover height', async () => { function Harness(): ReactNode { const [value, setValue] = useState('Cascadia Mono')