The ninth tab (Shader) pushed the row past the centre column: List
ended at 962 while the column ended at 925, so the last buttons sat
under the inspector and clickText('Container') hit the List tab
instead (CI runs 5 and 6, module 16). Tab buttons now use padding 4
via a new Button padding option; e2e step 14b guards the row width
using the widget box rather than getBounds(), which sees only the
label on an untoggled ghost button.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
a32b7c6daa
commit
69615c1f4b
4 files changed
+42
-8
No files matched your search
+17
-1
@@ -270,8 +270,24 @@ export class Editor implements PlayHost {
|
||||
ui().focus.validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Global x-extent of every tab button, from the widget box – not `getBounds()`, which on an
|
||||
* untoggled ghost tab sees nothing but the label (the background is not drawn at all).
|
||||
*/
|
||||
tabBounds(): Record<CentreTab, { x: number; right: number }> {
|
||||
const out = {} as Record<CentreTab, { x: number; right: number }>;
|
||||
for (const tab of TAB_ORDER) {
|
||||
const button = this.tabButtons[tab];
|
||||
const x = button.toGlobal({ x: 0, y: 0 }).x;
|
||||
out[tab] = { x, right: x + button.w };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private tabButton(tab: CentreTab): Button {
|
||||
return new Button(TAB_LABELS[tab], { variant: 'ghost', minWidth: 90, onClick: () => this.showTab(tab) });
|
||||
// Half the theme padding: nine tabs at the full 8 px overflow the centre column of a 1280 px
|
||||
// window by 27 px, and a button past its edge sits under the inspector (measured, CI run 5).
|
||||
return new Button(TAB_LABELS[tab], { variant: 'ghost', padding: 4, onClick: () => this.showTab(tab) });
|
||||
}
|
||||
|
||||
boot(): Promise<void> {
|
||||
|
||||
@@ -181,6 +181,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
textOverflows,
|
||||
labelsDropped,
|
||||
shownLabel,
|
||||
tabBounds: () => editor.tabBounds(),
|
||||
viewportCenter: () => centreOf(viewport),
|
||||
/** Its drawn box – a step that wants an empty spot must stay inside what the console left over. */
|
||||
viewportRect: () => {
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface ButtonOptions {
|
||||
/** Floor only – for a button whose label changes between forms and must grow with them. */
|
||||
minWidth?: number;
|
||||
align?: 'center' | 'left';
|
||||
/** Horizontal inset of the label; defaults to the theme padding. */
|
||||
padding?: number;
|
||||
/** Text style of the label when the button is not on an accent fill. */
|
||||
style?: TextStyleToken;
|
||||
onClick?: () => void;
|
||||
@@ -28,6 +30,7 @@ export class Button extends Widget {
|
||||
variant: NonNullable<ButtonOptions['variant']>;
|
||||
protected readonly align: NonNullable<ButtonOptions['align']>;
|
||||
protected readonly fixedWidth: number;
|
||||
private readonly padding: number;
|
||||
private readonly style: TextStyleToken;
|
||||
onClick?: () => void;
|
||||
|
||||
@@ -41,6 +44,7 @@ export class Button extends Widget {
|
||||
this.minHeight = Theme.controlHeight;
|
||||
this.fixedWidth = options.width ?? 0;
|
||||
this.minWidth = this.fixedWidth || (options.minWidth ?? 0);
|
||||
this.padding = options.padding ?? Theme.padding;
|
||||
this.fullText = text;
|
||||
this.style = options.style ?? 'body';
|
||||
this.labelText = new Text({ text, style: textStyle(this.variant === 'primary' ? 'accent' : this.style) });
|
||||
@@ -102,14 +106,14 @@ export class Button extends Widget {
|
||||
// Measured bold where it can be drawn bold (on an accent fill), or a danger or toggled tab truncates.
|
||||
const style = textStyle(this.variant === 'default' ? this.style : 'accent');
|
||||
const metrics = CanvasTextMetrics.measureText(this.fullText, style);
|
||||
return { w: Math.max(this.minWidth, Math.ceil(metrics.width) + Theme.padding * 2), h };
|
||||
return { w: Math.max(this.minWidth, Math.ceil(metrics.width) + this.padding * 2), h };
|
||||
}
|
||||
|
||||
/** Width the label may occupy; subclasses that draw inside the button take their room off here. */
|
||||
protected labelSlot(): number {
|
||||
// The padding is an inset, not a claim: on a 24 px icon button it would leave 8 px and
|
||||
// truncate the glyph away entirely. From 48 px up this is the full padding again.
|
||||
return this.w - Math.min(Theme.padding, Math.floor(this.w / 6)) * 2;
|
||||
return this.w - Math.min(this.padding, Math.floor(this.w / 6)) * 2;
|
||||
}
|
||||
|
||||
override onKeyDown(event: KeyboardEvent): boolean {
|
||||
@@ -157,7 +161,7 @@ export class Button extends Widget {
|
||||
this.labelText.style = textStyle(this.onAccent ? 'accent' : this.style);
|
||||
const label = this.labelText;
|
||||
label.text = truncate(this.fullText, label.style, this.labelSlot());
|
||||
const x = this.align === 'left' ? Theme.padding : (this.w - label.width) / 2;
|
||||
const x = this.align === 'left' ? this.padding : (this.w - label.width) / 2;
|
||||
label.position.set(Math.round(x), centreY(this.h, label.height));
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,20 @@ export async function overflow(t) {
|
||||
);
|
||||
}
|
||||
|
||||
// 14b. Every tab, because each one owns a different set of controls.
|
||||
// 14b. The tab row itself: a button past the centre column's edge sits under the inspector, and
|
||||
// a click on it lands there (nine tabs on a 90 px floor did exactly that at 1280 px wide).
|
||||
await evaluate("__gm.showTab('scene')");
|
||||
await sleep(300);
|
||||
const column = await json('__gm.viewportRect()');
|
||||
const tabs = await json('__gm.tabBounds()');
|
||||
const spilled = Object.entries(tabs).filter(([, b]) => b.right > column.x + column.w);
|
||||
check(
|
||||
'every tab button ends inside the centre column',
|
||||
spilled.length === 0,
|
||||
JSON.stringify({ spilled, columnRight: column.x + column.w }),
|
||||
);
|
||||
|
||||
// 14c. Every tab, because each one owns a different set of controls.
|
||||
for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'list']) {
|
||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||||
await sleep(400);
|
||||
@@ -50,7 +63,7 @@ export async function overflow(t) {
|
||||
);
|
||||
}
|
||||
|
||||
// 14c. Shortened, not simply dropped: the value must still be readable and marked as cut.
|
||||
// 14d. Shortened, not simply dropped: the value must still be readable and marked as cut.
|
||||
await evaluate("__gm.showTab('graph')");
|
||||
await sleep(400);
|
||||
const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement'));
|
||||
@@ -61,7 +74,7 @@ export async function overflow(t) {
|
||||
);
|
||||
await shot('36-long-labels');
|
||||
|
||||
// 14d. An inspector full of dropdowns – the parent row and the enum fields – with a selection
|
||||
// 14e. An inspector full of dropdowns – the parent row and the enum fields – with a selection
|
||||
// whose asset name is now overlong too.
|
||||
await evaluate("__gm.showTab('scene')");
|
||||
await sleep(300);
|
||||
@@ -70,7 +83,7 @@ export async function overflow(t) {
|
||||
const inspector = await json('__gm.textOverflows()');
|
||||
check('no label spills in the inspector', inspector.length === 0, JSON.stringify(inspector));
|
||||
|
||||
// 14e. The other direction: a control narrow enough to swallow its whole label draws nothing
|
||||
// 14f. The other direction: a control narrow enough to swallow its whole label draws nothing
|
||||
// at all, which the overflow walk cannot see – an empty text spills over no edge.
|
||||
for (const tab of ['scene', 'graph', 'texture', 'container', 'list']) {
|
||||
await evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||||
|
||||
Reference in new issue
Block a user