Tabs are open documents: Scene, Game and Plugins stay as fixed tabs, every other asset opens in its own tab (double click or Enter in the asset panel, on creation, from the list button), several assets of one kind can be open at once, a tab is labelled with its kind dimmed before the name, cut at 160 px and renamed with its asset, closes with its x or a middle click and goes away when its asset is deleted; a tab always shows its own document instead of following the selection, the picker in the tab head is gone, zoom and scroll are kept per document for the session and dropped on close, a double click on a call node stays in the same tab with breadcrumbs per tab; one editor per kind is switched between documents, the placement rules live in the pure module documents.ts, and the test hook showTab stays as a compatibility path; new e2e module 44 documents (#171)
game-maker / gate (push) Successful in 1m59s
game-maker / suite (push) Successful in 6m53s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-19 15:49:54 +02:00
1 parent 4f546c0e08
commit cc285af10a
33 files changed
+895 -225

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
import { CanvasTextMetrics, type FederatedPointerEvent, Rectangle, Text } from 'pixi.js';
import { Button, centreY, Theme, textStyle } from '../ui';
const GAP = 4;
const CLOSE = '×';
/** The × is small; the hit around it is not, or closing a tab would take aim. */
const CLOSE_SLOP = 4;
/** A long name is cut here rather than pushing every other tab behind the `»` button. */
const MAX_NAME_WIDTH = 160;
/**
* A tab that stands for an open document (#171): its kind in muted type before the name, so three
* assets called "Player" can be told apart, and an × that closes it – as does a middle click.
*/
export class DocumentTab extends Button {
private readonly kindText: Text;
private readonly closeText: Text;
constructor(
label: string,
private readonly kind: string,
onPick: () => void,
private readonly onClose: () => void,
) {
super(label, { variant: 'ghost', padding: 4, align: 'left', onClick: onPick });
this.kindText = new Text({ text: kind, style: textStyle('muted') });
this.closeText = new Text({ text: CLOSE, style: textStyle('muted') });
this.closeText.eventMode = 'static';
this.closeText.cursor = Theme.hoverCursor;
// Stopped on the way down as well, or the tab under it would count the press as its own click.
this.closeText.on('pointerdown', (e: FederatedPointerEvent) => e.stopPropagation());
this.closeText.on('pointerup', (e: FederatedPointerEvent) => {
if (e.button !== 0) return;
e.stopPropagation();
this.onClose();
});
this.on('pointerup', (e: FederatedPointerEvent) => {
if (e.button === 1) this.onClose();
});
this.addChild(this.kindText, this.closeText);
}
/** The × in global coordinates, for the check scripts. */
closePoint(): { x: number; y: number } {
const at = this.closeText.getGlobalPosition();
return { x: at.x + this.closeText.width / 2, y: at.y + this.closeText.height / 2 };
}
get kindLabel(): string {
return this.kind;
}
override measure(): { w: number; h: number } {
const own = super.measure();
return { w: Math.min(own.w, MAX_NAME_WIDTH + this.padding * 2) + this.extraWidth(), h: own.h };
}
protected override labelSlot(): number {
return super.labelSlot() - this.extraWidth();
}
override redraw(): void {
super.redraw();
const style = textStyle(this.toggled ? 'accent' : 'muted');
this.kindText.style = style;
this.closeText.style = style;
// On the accent fill the muted grey would vanish; the accent text, dimmed, stays the quieter one.
this.kindText.alpha = this.toggled ? 0.7 : 1;
this.kindText.position.set(this.padding, centreY(this.h, this.kindText.height));
this.labelText.x = this.padding + this.kindText.width + GAP;
const closeX = this.w - this.padding - this.closeText.width;
this.closeText.position.set(closeX, centreY(this.h, this.closeText.height));
this.closeText.hitArea = new Rectangle(
-CLOSE_SLOP,
-CLOSE_SLOP,
this.closeText.width + CLOSE_SLOP * 2,
this.closeText.height + CLOSE_SLOP * 2,
);
}
private extraWidth(): number {
const style = textStyle('muted');
const kind = CanvasTextMetrics.measureText(this.kind, style).width;
const close = CanvasTextMetrics.measureText(CLOSE, style).width;
return Math.ceil(kind + close) + GAP * 2;
}
}