Files
feuerwerk/src/editor/font/FontPicker.ts
T
adminandClaude Fable 5.1 cc6db5f350
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m46s
Three stores, three importers and three pickers that differ only in their type are one of each (#85)
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:38:54 +02:00

111 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<StoredFont>;
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<FontItem, FontRow> {
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<FontItem> {
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<FontItem[]> {
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);
}