Skip to content

Commit

Permalink
Query local fonts to show a font list
Browse files Browse the repository at this point in the history
This uses the [Local Font Access
API](https://developer.mozilla.org/en-US/docs/Web/API/Local_Font_Access_API)
to query the list of fonts installed on the user's system. If we
successfully query it we show a combobox with the list of fonts.
Otherwise it's still a textbox. We also get information about the styles
that are available for each font family. Unfortunately this comes as a
string rather than individual fields, so we have to parse it. We do this
by looking for specific style names in there. Since this is mostly
heuristic based, instead of only showing the styles that we found, we
still show all of them, but grey out those that we didn't find, but you
can still select them.
  • Loading branch information
CryZe committed Apr 29, 2024
1 parent 0f87db7 commit ff2f22c
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 32 deletions.
94 changes: 62 additions & 32 deletions src/ui/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import HotkeyButton from "./HotkeyButton";
import ToggleCheckbox from "./ToggleCheckbox";
import { UrlCache } from "../util/UrlCache";
import { openFileAsArrayBuffer } from "../util/FileUtil";
import * as FontList from "../util/FontList";

export interface Props<T> {
context: string,
Expand Down Expand Up @@ -175,6 +176,7 @@ export class JsonSettingValueFactory implements SettingValueFactory<ExtendedSett
}
}


export class SettingsComponent<T> extends React.Component<Props<T>> {
public render() {
const settingsRows: JSX.Element[] = [];
Expand Down Expand Up @@ -908,21 +910,55 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
];

if (value.Font !== null) {
FontList.load(() => this.setState({}));

const { family, style, weight, stretch } = value.Font;

const styles = FontList.knownStyles.get(family);

if (FontList.knownFamilies.length > 0) {
children.push(
<select
style={{
fontFamily: family,
}}
value={family}
onChange={(e) => {
this.props.setValue(
valueIndex,
expect(
factory.fromFont(e.target.value, style, weight, stretch),
"Unexpected Font",
),
);
}}
>
<option value=""></option>
{
FontList.knownFamilies.map((n) =>
<option value={n} style={{ fontFamily: n }}>{n}</option>
)
}
</select>
);
} else {
children.push(
<input
value={family}
onChange={(e) => {
this.props.setValue(
valueIndex,
expect(
factory.fromFont(e.target.value, style, weight, stretch),
"Unexpected Font",
),
);
}}
/>
);
}

children.push(
<input
value={family}
onChange={(e) => {
this.props.setValue(
valueIndex,
expect(
factory.fromFont(e.target.value, style, weight, stretch),
"Unexpected Font",
),
);
}}
/>,
<>Style</>,
<select
value={style}
Expand Down Expand Up @@ -952,17 +988,13 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
);
}}
>
<option value="thin">Thin</option>
<option value="extra-light">Extra Light</option>
<option value="light">Light</option>
<option value="semi-light">Semi Light</option>
<option value="normal">Normal</option>
<option value="medium">Medium</option>
<option value="semi-bold">Semi Bold</option>
<option value="bold">Bold</option>
<option value="extra-bold">Extra Bold</option>
<option value="black">Black</option>
<option value="extra-black">Extra Black</option>
{
FontList.FONT_WEIGHTS.map(([_, value, name]) => (
<option style={{
color: styles?.has(value) ? "white" : "grey"
}} value={value}>{name}</option>
))
}
</select>,
<>Stretch</>,
<select
Expand All @@ -977,15 +1009,13 @@ export class SettingsComponent<T> extends React.Component<Props<T>> {
);
}}
>
<option value="ultra-condensed">Ultra Condensed</option>
<option value="extra-condensed">Extra Condensed</option>
<option value="condensed">Condensed</option>
<option value="semi-condensed">Semi Condensed</option>
<option value="normal">Normal</option>
<option value="semi-expanded">Semi Expanded</option>
<option value="expanded">Expanded</option>
<option value="extra-expanded">Extra Expanded</option>
<option value="ultra-expanded">Ultra Expanded</option>
{
FontList.FONT_STRETCHES.map(([_, value, name]) => (
<option style={{
color: styles?.has(value) ? "white" : "grey"
}} value={value}>{name}</option>
))
}
</select>,
);
}
Expand Down
74 changes: 74 additions & 0 deletions src/util/FontList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
let queriedKnownFonts = false;
export let knownFamilies: string[] = [];
export const knownStyles = new Map<string, Set<string>>();

// There's three values per row:
// 1. What to look for in the style specifier of the loaded font list.
// 2. The value to tell livesplit-core.
// 3. The label to display in the UI.
export const FONT_WEIGHTS = [
["thin", "thin", "Thin"],
["extralight", "extra-light", "Extra Light"],
["light", "light", "Light"],
["semilight", "semi-light", "Semi Light"],
["normal", "normal", "Normal"],
["medium", "medium", "Medium"],
["semibold", "semi-bold", "Semi Bold"],
["bold", "bold", "Bold"],
["extrabold", "extra-bold", "Extra Bold"],
["black", "black", "Black"],
["extrablack", "extra-black", "Extra Black"],
];

export const FONT_STRETCHES = [
["ultracondensed", "ultra-condensed", "Ultra Condensed"],
["extracondensed", "extra-condensed", "Extra Condensed"],
["condensed", "condensed", "Condensed"],
["semicondensed", "semi-condensed", "Semi Condensed"],
["normal", "normal", "Normal"],
["semiexpanded", "semi-expanded", "Semi Expanded"],
["expanded", "expanded", "Expanded"],
["extraexpanded", "extra-expanded", "Extra Expanded"],
["ultraexpanded", "ultra-expanded", "Ultra Expanded"],
];

// Italics are always supported.

export function load(loadedCallback: () => void) {
if (!queriedKnownFonts) {
queriedKnownFonts = true;
if ("queryLocalFonts" in window) {
(window as any).queryLocalFonts().then((availableFonts: any) => {
try {
for (const font of availableFonts) {
if (!knownStyles.has(font.family)) {
knownStyles.set(font.family, new Set(["normal", "bold"]));
}

const set = knownStyles.get(font.family)!;
const styles = (font.style as string).toLowerCase().split(" ");

for (const [keyword, value] of FONT_STRETCHES) {
if (styles.includes(keyword)) {
set.add(value);
}
}

for (const [keyword, value] of FONT_WEIGHTS) {
if (styles.includes(keyword)) {
set.add(value);
}
}
}

knownFamilies = Array.from(knownStyles.keys());

loadedCallback();
} catch {
// It's fine if it fails, it's an experimental web API, and
// the user may reject it.
}
});
}
}
}

0 comments on commit ff2f22c

Please sign in to comment.