import { Container, Graphics, Text, TextStyle } from 'pixi.js'; import type { StoredFont } from '../../core/store/FontStore'; import { formatBytes, Theme, textStyle, truncate } from '../../ui'; import { fontLibrary } from '../../viewport/FontLibrary'; import { currentPicker, FilePicker, openPicker, type PickerItem, type PickerRow, RowList, withAssetNames, } from '../FilePicker'; import { type FontImportDeps, importStoredFont, pickAndImportFonts } from '../fontImport'; const ROW_HEIGHT = 52; const ROW_PAD = 8; /** Big enough to tell two typefaces apart at a glance, small enough for a long line to fit. */ const SAMPLE_SIZE = 22; const SAMPLE_TEXT = 'Aa Bb Cc 0123 The quick brown fox'; type FontItem = PickerItem; interface FontRow extends PickerRow { sample: Text; } /** * A typeface has no thumbnail to show, so each row *is* the preview: the sample line is drawn in * the face itself, and reflows from the fallback once the library has loaded it. */ class FontList extends RowList { protected readonly rowHeight = ROW_HEIGHT; constructor() { super(); this.watch(fontLibrary, () => this.redraw()); } protected buildRow(): FontRow { const box = new Container(); const background = new Graphics(); // Its own style object, never `textStyle('body')`: those are shared across the whole editor, // so writing a family into one would restyle every label in the window. const sample = new Text({ style: new TextStyle({ fontSize: SAMPLE_SIZE, fill: Theme.colors.text }) }); const caption = new Text({ style: textStyle('small') }); box.addChild(background, sample, caption); return { box, background, sample, caption }; } protected redrawRow(row: FontRow, item: FontItem, selected: boolean): void { this.drawBox(row, this.w, ROW_HEIGHT, selected); // Empty family = not loaded yet; the row then reads in the editor's own typeface and the // library's change event brings it back here once the face has arrived. row.sample.style.fontFamily = fontLibrary.family(item.file) || Theme.font; row.sample.text = truncate(SAMPLE_TEXT, row.sample.style, this.w - ROW_PAD * 2); row.sample.position.set(ROW_PAD, ROW_PAD - 2); // The asset name wins where there is one – it is what the project calls this face. Otherwise // the name it was uploaded under, which is all a bare hash leaves to go on. row.caption.style = textStyle(item.assetName ? 'accent' : 'muted'); row.caption.text = truncate( item.assetName ?? (item.name || formatBytes(item.bytes)), row.caption.style, this.w - ROW_PAD * 2, ); row.caption.position.set(ROW_PAD, ROW_HEIGHT - 16); } } export class FontPicker extends FilePicker { declare protected readonly list: FontList; constructor(private readonly deps: FontImportDeps) { super( new FontList(), { title: 'Choose a font', loading: 'Loading fonts…', empty: 'No fonts on the server yet – upload one.', use: 'Use font', noun: 'font', }, () => pickAndImportFonts(deps), ); } rowGlobal(file: string): { x: number; y: number } | null { return this.list.rowGlobal(file); } protected async fetchItems(): Promise { return withAssetNames(await this.deps.fonts.list(), this.deps.state.project, 'font'); } protected contentHeight(count: number): { unit: number; total: number } { return { unit: ROW_HEIGHT, total: this.list.heightFor(count) }; } protected adopt(item: FontItem): void { importStoredFont(item, this.deps); } } export function openFontPicker(deps: FontImportDeps): FontPicker { return openPicker(new FontPicker(deps)); } export function currentFontPicker(): FontPicker | null { return currentPicker(FontPicker); }