Skip to content

Commit

Permalink
feat: add picker monospace filter
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardoperra committed Nov 26, 2023
1 parent 78ffb4d commit 2867c92
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 124 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {textFieldStyles, themeVars} from '@codeimage/ui';
import {responsiveStyle} from '@codeui/kit';
import {responsiveStyle, themeTokens} from '@codeui/kit';
import {createVar, style} from '@vanilla-extract/css';

export const input = style([
textFieldStyles.baseField,
{
padding: themeVars.spacing['1'],
paddingLeft: themeVars.spacing['3'],
paddingRight: themeVars.spacing['3'],
flex: 1,
Expand All @@ -17,6 +16,16 @@ export const input = style([
},
]);

export const inputValue = style({
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
});

export const inputIcon = style({
flexShrink: 0,
});

export const fontListboxHeight = createVar();

export const fontPickerPopover = style([
Expand All @@ -41,7 +50,7 @@ export const aspectRatioCardDetails = style({

export const centeredContent = style({
width: '100%',
height: '300px',
height: fontListboxHeight,
display: 'flex',
flexDirection: 'column',
gap: '1rem',
Expand All @@ -59,3 +68,16 @@ export const virtualizedFontListbox = style({
overflow: 'auto',
height: '100%',
});

export const virtualizedFontListboxSearch = style({
flex: 1,
});

export const virtualizedFontListboxToolbar = style({
display: 'flex',
justifyContent: 'space-between',
gap: themeTokens.spacing['2'],
':first-child': {
flex: 1,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import {Box, FlexField, HStack, Text, VStack} from '@codeimage/ui';
import {
As,
IconButton,
icons,
Listbox,
Popover,
PopoverContent,
PopoverTrigger,
icons,
} from '@codeui/kit';
import {useModality} from '@core/hooks/isMobile';
import {DynamicSizedContainer} from '@ui/DynamicSizedContainer/DynamicSizedContainer';
Expand Down Expand Up @@ -41,7 +41,7 @@ export function FontPicker(props: FontPickerProps) {

const webListboxItems = () =>
configState.get.fonts
.filter(font => !font.custom)
.filter(font => font.type === 'web')
.map(font => ({
label: font.name,
value: font.id,
Expand Down Expand Up @@ -71,10 +71,10 @@ export function FontPicker(props: FontPickerProps) {
>
<PopoverTrigger asChild>
<As component={'div'} class={styles.input}>
<Text weight={'normal'}>
<span class={styles.inputValue}>
{selectedFont()?.name ?? 'No font selected'}
</Text>
<icons.SelectorIcon />
</span>
<icons.SelectorIcon class={styles.inputIcon} />
</As>
</PopoverTrigger>
<PopoverContent variant={'bordered'} class={styles.fontPickerPopover}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {EditorConfigStore} from '@codeimage/store/editor/config.store';
import {Box, FieldLabelHint, Link, LoadingCircle, Text} from '@codeimage/ui';
import {Button, VirtualizedListbox} from '@codeui/kit';
import {Button, Checkbox, TextField, VirtualizedListbox} from '@codeui/kit';
import {pipe} from 'rxjs';
import {
createMemo,
createSignal,
Match,
onCleanup,
onMount,
Expand All @@ -24,20 +26,43 @@ interface FontSystemPickerProps {

export function FontSystemPicker(props: FontSystemPickerProps) {
const configStore = provideState(EditorConfigStore);
const [fontTerm, setFontTerm] = createSignal('');
const [showOnlyMonospaced, setShowOnlyMonospaced] = createSignal(true);

const {localFontsApi} = configStore;

onMount(() => {
const subscription = localFontsApi.accessSystemFonts().subscribe();
const subscription = localFontsApi.accessSystemFonts(true).subscribe();
onCleanup(() => subscription.unsubscribe());
});

const fonts = createMemo(() => {
return Object.entries(unwrap(localFontsApi.state().fonts)).map(
([fontId, fontDataList]) => ({
label: fontDataList[0].postscriptName,
value: fontId,
}),
);
const onlyMonospaced = showOnlyMonospaced();
const term = fontTerm();
const fonts = unwrap(configStore.get.systemFonts);

return pipe(
(_: typeof fonts) => _,
// Monospaced filter
fonts =>
onlyMonospaced ? fonts.filter(font => font.fontData.monospaced) : fonts,
// Term filter
fonts =>
term.length > 2
? fonts.filter(font => font.name.toLowerCase().includes(term))
: fonts,
// Sorting alphabetically
fonts =>
fonts.sort((a, b) =>
a.fontData.family.localeCompare(b.fontData.family),
),
// Map to select items
fonts =>
fonts.map(font => ({
label: font.name,
value: font.id,
})),
)(fonts);
});

const listboxProps = createFontPickerListboxProps({
Expand All @@ -55,13 +80,22 @@ export function FontSystemPicker(props: FontSystemPickerProps) {
<Suspense>
<Switch>
<Match when={localFontsApi.state().permissionState === 'granted'}>
<Button
size={'xs'}
theme={'secondary'}
onClick={localFontsApi.loadFonts}
>
Reload fonts
</Button>
<div class={styles.virtualizedFontListboxToolbar}>
<TextField
placeholder={'Search font...'}
value={fontTerm()}
onChange={setFontTerm}
size={'xs'}
slotClasses={{root: styles.virtualizedFontListboxSearch}}
/>
<Button
size={'xs'}
theme={'secondary'}
onClick={localFontsApi.loadFonts}
>
Reload fonts
</Button>
</div>

<Show
fallback={<LoadingContent />}
Expand All @@ -76,6 +110,15 @@ export function FontSystemPicker(props: FontSystemPickerProps) {
/>
</div>
</Show>
<Box marginTop={'2'}>
<Checkbox
disabled={localFontsApi.state().loading}
checked={showOnlyMonospaced()}
onChange={setShowOnlyMonospaced}
size={'md'}
label={'Show only monospaced fonts'}
/>
</Box>
</Match>
<Match when={localFontsApi.state().permissionState === 'prompt'}>
<LoadingContent message={'Waiting for permission.'} />
Expand Down
29 changes: 21 additions & 8 deletions apps/codeimage/src/core/configuration/font.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {LoadedFont} from '../../hooks/use-local-fonts';
import {mapToDictionary} from '../helpers/mapToDictionary';

interface CustomFontType {
Expand All @@ -6,13 +7,25 @@ interface CustomFontType {
fontData?: FontData;
}

export interface CustomFontConfiguration {
interface WebFontConfiguration {
id: string;
name: string;
custom: boolean;
type: 'web';
types: readonly CustomFontType[];
}

interface SystemFontConfiguration {
id: string;
name: string;
type: 'system';
fontData: LoadedFont;
types: readonly CustomFontType[];
}

export type CustomFontConfiguration =
| WebFontConfiguration
| SystemFontConfiguration;

function createCustomFonts<T extends ReadonlyArray<CustomFontConfiguration>>(
fonts: T,
) {
Expand All @@ -25,7 +38,7 @@ export const [SUPPORTED_FONTS, SUPPORTED_FONTS_DICTIONARY] = createCustomFonts([
{
id: 'jetbrains-mono',
name: 'Jetbrains Mono',
custom: false,
type: 'web',
types: [
{name: 'Regular', weight: 400},
{name: 'Medium', weight: 500},
Expand All @@ -35,7 +48,7 @@ export const [SUPPORTED_FONTS, SUPPORTED_FONTS_DICTIONARY] = createCustomFonts([
{
id: 'fira-code',
name: 'Fira Code',
custom: false,
type: 'web',
types: [
{name: 'Regular', weight: 400},
{name: 'Medium', weight: 500},
Expand All @@ -45,7 +58,7 @@ export const [SUPPORTED_FONTS, SUPPORTED_FONTS_DICTIONARY] = createCustomFonts([
{
id: 'source-code-pro',
name: 'Source Code pro',
custom: false,
type: 'web',
types: [
{name: 'Regular', weight: 400},
{name: 'Medium', weight: 500},
Expand All @@ -55,7 +68,7 @@ export const [SUPPORTED_FONTS, SUPPORTED_FONTS_DICTIONARY] = createCustomFonts([
{
id: 'overpass-mono',
name: 'Overpass Mono',
custom: false,
type: 'web',
types: [
{name: 'Regular', weight: 400},
{name: 'Medium', weight: 500},
Expand All @@ -65,7 +78,7 @@ export const [SUPPORTED_FONTS, SUPPORTED_FONTS_DICTIONARY] = createCustomFonts([
{
id: 'space-mono',
name: 'Space Mono',
custom: false,
type: 'web',
types: [
{name: 'Regular', weight: 400},
{name: 'Bold', weight: 700},
Expand All @@ -74,7 +87,7 @@ export const [SUPPORTED_FONTS, SUPPORTED_FONTS_DICTIONARY] = createCustomFonts([
{
id: 'cascadia-code',
name: 'Cascadia Code',
custom: false,
type: 'web',
types: [
{name: 'Regular', weight: 400},
{name: 'Bold', weight: 700},
Expand Down
63 changes: 63 additions & 0 deletions apps/codeimage/src/core/modules/localFontAccessApi/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {defer, merge, Observable, of, switchMap} from 'rxjs';

declare global {
interface Window {
/**
* query local fonts
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/queryLocalFonts MDN Reference}
*/
queryLocalFonts?: (options?: {
postscriptNames: string[];
}) => Promise<FontData[]>;
}

interface FontData {
/**
* the family of the font face
*/
readonly family: string;
/**
* the full name of the font face
*/
readonly fullName: string;
/**
* the PostScript name of the font face
*/
readonly postscriptName: string;
/**
* the style of the font face
*/
readonly style: string;
/**
* get a Promise that fulfills with a Blob containing the raw bytes of the underlying font file
*/
readonly blob: () => Promise<Blob>;
}
}

export function isLocalFontsFeatureSupported() {
const {queryLocalFonts} = window;
return !!queryLocalFonts;
}

export async function checkLocalFontPermission() {
const {navigator} = window;
return navigator.permissions.query({
name: 'local-fonts',
// TODO: extend dom interfaces?
} as unknown as PermissionDescriptor);
}

export const checkLocalFontPermission$ = defer(() =>
checkLocalFontPermission(),
).pipe(
switchMap(permission => {
const permissionStateChange$ = new Observable<PermissionState>(observer => {
permission.onchange = function (this) {
observer.next(this.state);
};
});
return merge(of(permission.state), permissionStateChange$);
}),
);
Loading

0 comments on commit 2867c92

Please sign in to comment.