import { Graphics } from 'pixi.js'; import { AddTilePictureCommand, MoveTilePictureCommand, RemoveTilePictureCommand, } from '../../core/commands/tileCommands'; import type { TileAsset } from '../../core/model/Asset'; import { pictureChoices, textureRefLabel } from '../../core/properties/textureRef'; import { Button, dropIndex, type DropSpot, HStack, Label, ListView, Menu, type MenuItem, ScrollContainer, Theme, VStack, } from '../../ui'; import type { EditorState } from '../state'; const WIDTH = 210; /** * The picture list beside the board (#118): one row per animation step, in the order they run. * Rows are addressed by their place rather than by the picture they name – the same block may be * used twice in one animation, and then two rows carry the same id. */ export class PictureStrip extends VStack { /** Public for the check scripts. */ readonly addButton: Button; readonly removeButton: Button; readonly list = new ListView(); private readonly title = new Label('Pictures', { style: 'muted' }); private readonly scroll = new ScrollContainer(); private readonly border = new Graphics(); private assetId = ''; private selected = -1; constructor(private readonly state: EditorState) { super({ gap: Theme.gap, padding: Theme.padding }); this.minWidth = WIDTH; this.addButton = new Button('+ Picture', { onClick: () => this.openPicturePicker() }); this.removeButton = new Button('Remove', { onClick: () => this.removeSelected() }); const header = new HStack({ gap: Theme.gap, align: 'center' }); header.add(this.title); this.add(header); this.list.emptyText = 'No pictures yet'; this.list.onSelect = (id) => this.select(id === null ? -1 : Number(id)); this.list.onDrop = (draggedId, spot) => this.drop(Number(draggedId), spot); this.list.onContextMenu = (id, x, y) => this.openRowMenu(Number(id), x, y); this.scroll.setContent(this.list); this.add(this.scroll, 1); const buttons = new HStack({ gap: Theme.gap }); buttons.add(this.addButton, 1); buttons.add(this.removeButton, 1); this.add(buttons); this.addChildAt(this.border, 0); state.on('project', () => this.refresh()); state.on('load', () => this.show('')); } show(assetId: string): void { if (this.assetId === assetId) { this.refresh(); return; } this.assetId = assetId; this.selected = -1; this.refresh(); } /** The rows as shown and which one is picked – the check scripts read this. */ debugInfo(): { pictures: string[]; selected: number } { return { pictures: this.asset()?.pictures.map((id) => this.labelOf(id)) ?? [], selected: this.selected }; } private asset(): TileAsset | undefined { const asset = this.state.project.assets.find((a) => a.id === this.assetId); return asset?.type === 'tile' ? asset : undefined; } private labelOf(id: string): string { return textureRefLabel(this.state.project, id); } private refresh(): void { const asset = this.asset(); const pictures = asset?.pictures ?? []; if (this.selected >= pictures.length) this.selected = pictures.length - 1; this.list.setItems( pictures.map((id, index) => ({ id: String(index), label: this.labelOf(id), hint: String(index + 1), })), ); this.list.select(this.selected >= 0 ? String(this.selected) : null, true); this.scroll.relayoutContent(); this.addButton.setEnabled(asset !== undefined); this.removeButton.setEnabled(this.selected >= 0); this.title.setText(pictures.length === 1 ? '1 picture' : `${pictures.length} pictures`); } private select(index: number): void { this.selected = index; this.removeButton.setEnabled(index >= 0); } private removeSelected(): void { const asset = this.asset(); if (!asset || this.selected < 0) return; this.state.execute(new RemoveTilePictureCommand(asset.id, this.selected)); } /** Every texture and every frame; a tile's block is one of them, whole or cut out. */ private openPicturePicker(): void { const asset = this.asset(); if (!asset) return; const items: MenuItem[] = pictureChoices(this.state.project).map((choice) => ({ label: choice.label, onSelect: () => this.state.execute(new AddTilePictureCommand(asset.id, choice.id)), })); if (items.length === 0) items.push({ label: 'No textures yet', onSelect: () => undefined }); new Menu(items, { searchable: true }).openBelow(this.addButton); } private openRowMenu(index: number, x: number, y: number): Menu | undefined { const asset = this.asset(); if (!asset) return undefined; this.select(index); this.list.select(String(index), true); const menu = new Menu([ { label: 'Remove', danger: true, onSelect: () => this.state.execute(new RemoveTilePictureCommand(asset.id, index)), }, ]); menu.open(x, y); return menu; } /** Dragging a row to another place is what reorders the animation. */ private drop(from: number, spot: DropSpot): void { const asset = this.asset(); if (!asset) return; const to = dropIndex(from, spot, asset.pictures.length); if (to === from || to < 0) return; if (this.state.execute(new MoveTilePictureCommand(asset.id, from, to))) this.select(to); } override redraw(): void { super.redraw(); this.border.clear().rect(0, 0, 1, this.h).fill(Theme.colors.border); } }