73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
import { Graphics } from 'pixi.js';
|
||
import { Button, drawHeaderBar, HStack, Label, Theme, type Widget } from '../../ui';
|
||
|
||
/** Past this a crumb cuts its name short, so a deep trail of long names still fits the tab. */
|
||
const CRUMB_MAX_WIDTH = 160;
|
||
|
||
/** "Game › Move › Other": every crumb but the last one leads back; empty hides the bar. */
|
||
export class Breadcrumb extends HStack {
|
||
private readonly bg = new Graphics();
|
||
private crumbs: Widget[] = [];
|
||
onPick: (index: number) => void = () => undefined;
|
||
|
||
constructor() {
|
||
super({ gap: 2, padding: 3, align: 'center' });
|
||
this.addChild(this.bg);
|
||
this.visible = false;
|
||
}
|
||
|
||
/** Returns whether the bar changed, so the caller relayouts only then. */
|
||
show(names: readonly string[]): boolean {
|
||
if (names.length === this.crumbs.length && names.every((n, i) => crumbText(this.crumbs[i]!) === n))
|
||
return false;
|
||
for (const widget of this.items) widget.destroy({ children: true });
|
||
this.crumbs = names.map((name, i) => {
|
||
if (i > 0) this.add(new Label('›', { style: 'muted' }));
|
||
const last = i === names.length - 1;
|
||
return this.add(
|
||
capped((width) =>
|
||
last
|
||
? new Label(name, { width, padding: Theme.padding })
|
||
: new Button(name, { variant: 'ghost', width, onClick: () => this.onPick(i) }),
|
||
),
|
||
);
|
||
});
|
||
this.visible = names.length > 0;
|
||
// The bar keeps its size across a rebuild, and `resize` lays out only on a new size.
|
||
this.relayout();
|
||
return true;
|
||
}
|
||
|
||
/** For the check scripts: what each crumb says, where it sits, and whether it leads anywhere. */
|
||
crumbsDrawn(): { name: string; x: number; y: number; left: number; right: number; clickable: boolean }[] {
|
||
if (!this.visible) return [];
|
||
return this.crumbs.map((widget) => {
|
||
const at = widget.getGlobalPosition();
|
||
return {
|
||
name: crumbText(widget),
|
||
x: at.x + widget.w / 2,
|
||
y: at.y + widget.h / 2,
|
||
left: at.x,
|
||
right: at.x + widget.w,
|
||
clickable: widget instanceof Button,
|
||
};
|
||
});
|
||
}
|
||
|
||
override redraw(): void {
|
||
super.redraw();
|
||
drawHeaderBar(this.bg, this.w, this.h);
|
||
}
|
||
}
|
||
|
||
function crumbText(widget: Widget): string {
|
||
return widget instanceof Button || widget instanceof Label ? widget.text : '';
|
||
}
|
||
|
||
function capped(build: (width?: number) => Widget): Widget {
|
||
const natural = build();
|
||
if (natural.measure().w <= CRUMB_MAX_WIDTH) return natural;
|
||
natural.destroy({ children: true });
|
||
return build(CRUMB_MAX_WIDTH);
|
||
}
|