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

+21 -3
View File
@@ -3,6 +3,7 @@ import { drawControlBox } from '../draw';
import { centreY } from '../math';
import { Theme, textStyle } from '../Theme';
import { type Size, Widget } from '../Widget';
import { truncate } from './Label';
export interface ButtonOptions {
/** Visual weight: 'primary' uses the accent color, 'ghost' has no background until hovered. */
@@ -21,6 +22,8 @@ export interface ButtonOptions {
export class Button extends Widget {
private readonly bg = new Graphics();
private readonly labelText: Text;
/** The label as set. `labelText` holds the possibly shortened version that is on screen. */
private fullText: string;
private pressed = false;
/** Caller-owned toggle state: renders like a primary button but is not flipped by clicks. */
toggled = false;
@@ -37,6 +40,7 @@ export class Button extends Widget {
this.cursor = 'pointer';
this.minHeight = Theme.controlHeight;
if (options.width) this.minWidth = options.width;
this.fullText = text;
this.labelText = new Text({ text, style: textStyle(this.variant === 'primary' ? 'accent' : 'body') });
this.addChild(this.bg, this.labelText);
@@ -61,13 +65,15 @@ export class Button extends Widget {
});
}
/** The full label, not the truncated version on screen. */
get text(): string {
return this.labelText.text;
return this.fullText;
}
/** Repaints only; the parent must relayout when the measured width matters. */
setText(text: string): void {
this.labelText.text = text;
if (text === this.fullText) return;
this.fullText = text;
this.invalidate();
}
@@ -83,13 +89,23 @@ export class Button extends Widget {
}
override measure(): Size {
const metrics = CanvasTextMetrics.measureText(this.labelText.text, this.labelText.style);
// Measured from the full label, never from what is currently on screen: measuring the
// truncated text would let a button that was once too narrow stay too narrow for good.
const metrics = CanvasTextMetrics.measureText(this.fullText, this.labelText.style);
return {
w: Math.max(this.minWidth, Math.ceil(metrics.width) + Theme.padding * 2),
h: Math.max(this.minHeight, Theme.controlHeight),
};
}
/**
* Width the label may occupy. Subclasses that draw something else inside the button take
* their room off here, so the text is shortened instead of running underneath it.
*/
protected labelSlot(): number {
return this.w - Theme.padding * 2;
}
override onKeyDown(event: KeyboardEvent): boolean {
if (event.key === 'Enter' || event.key === ' ') {
this.onClick?.();
@@ -131,8 +147,10 @@ export class Button extends Widget {
} else if (stroke !== null) {
this.bg.roundRect(0.5, 0.5, this.w - 1, this.h - 1, Theme.radius).stroke({ width: 1, color: stroke });
}
// Style first: it decides the glyph widths that the truncation then measures against.
this.labelText.style = textStyle(this.onAccent ? 'accent' : 'body');
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;
label.position.set(Math.round(x), centreY(this.h, label.height));
}