Add a decimals option, fix dropdown overflow and the texture filter

- NumberField takes a decimals option (default 3); frame rects and the
  slice dialog use 0, matching their integer model.
- Button truncates its label to the drawn width; Dropdown reserves the
  chevron slot. NodeView reuses the shared truncate().
- Setting scaleMode leaves the style's shared resource id in place, so
  the old GPU sampler stayed in use until a reload. Call style.update().
- Graph gets a header bar like the Texture tab: GraphEditor is now the
  frame, the former editor is GraphCanvas. A selection the tab does not
  concern no longer unloads the behavior.
- New e2e step 14 guards text overflow across all widgets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 15:05:28 +02:00
1 parent 35a1a23af8
commit f16801aef3
22 files changed
+874 -493

No files matched your search

+42 -3
View File
@@ -50,6 +50,42 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
walk(ui().overlay);
return out;
};
/**
* Visible texts drawn wider than the widget that owns them – the shape of bug where a label
* runs past its own control instead of being shortened to fit.
*
* The owner's box is its declared `w`/`h` at its global position, NOT `getBounds()`: a Pixi
* container has no size of its own, so its bounds are the union of its children – including
* the very text that overflows, which makes every overflow measure as zero. Texts inside a
* scroll container are skipped, where sticking out is the point and the mask hides it.
*/
const textOverflows = (all = false): { text: string; owner: string; over: number }[] => {
const out: { text: string; owner: string; over: number }[] = [];
const walk = (node: Container, owner: Widget | null, clipped: boolean): void => {
if (!node.visible) return;
const isWidget = node instanceof Widget;
const box = isWidget ? node : owner;
// A mask further up – a panel clipping its contents – does not excuse a label spilling
// out of its own control, so the question restarts at every widget. Only a mask at or
// below the owner (a text field's scroll window, a scroll container) makes it intended.
// `Boolean`, not `!== null`: Pixi's mask getter returns `undefined` when nothing masks,
// so comparing against null marks every container as clipped and hides every finding.
const masked = (isWidget ? false : clipped) || Boolean(node.mask);
if (node instanceof Text) {
if (!box || !node.text || (masked && !all)) return;
const text = node.getBounds();
const origin = box.getGlobalPosition();
const over = Math.max(text.x + text.width - (origin.x + box.w), origin.x - text.x);
if (over > 1 || all) {
out.push({ text: node.text, owner: box.constructor.name, over: Math.round(over) });
}
return;
}
for (const child of node.children) walk(child, box, masked);
};
walk(app.stage, null, false);
return out;
};
/** Screen rectangle of the open popup (the backdrop is not a widget), or null when none is. */
const overlayRect = (): { x: number; y: number; w: number; h: number } | null => {
const popups = ui().overlay.children.filter((node): node is Widget => node instanceof Widget);
@@ -63,6 +99,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
locate,
overlayTexts,
overlayRect,
textOverflows,
viewportCenter: () => centreOf(viewport),
viewportInfo: () => viewport.debugInfo(),
gameInfo: () => gameView.debugInfo(),
@@ -77,9 +114,11 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
graph: {
current: () => graphEditor.currentAssetId,
portAt: (nodeId: string, port: string, isOutput: boolean) =>
graphEditor.portGlobal(nodeId, port, isOutput),
nodeAt: (nodeId: string) => graphEditor.nodeGlobal(nodeId),
center: () => centreOf(graphEditor),
graphEditor.canvas.portGlobal(nodeId, port, isOutput),
nodeAt: (nodeId: string) => graphEditor.canvas.nodeGlobal(nodeId),
// The canvas, not the whole tab: a point measured through the header would sit above
// the drawing area a right-click test aims at.
center: () => centreOf(graphEditor.canvas),
},
texture: {
current: () => textureEditor.currentAssetId,