Open asset greyed out and real tab cards: Open asset in the scene object menu is only pickable for kinds with an editor of their own, the table of which asset opens which tab moved from Editor.ts into the new module assetTabs.ts with hasAssetEditor so the menu and the editor read the same list, Select asset stays as it was; tabs are drawn as index cards (new module TabButton.ts): the one showing is a light grey card instead of the aggressive blue, rounded above and square below, its name in plain weight and measured bold so it never clips itself, and a mouse down on the tab already showing keeps its colour; the padding left and right grew to 6 px in one place for both kinds of tab, Scene and Game carry an icon too (the scene one from the asset panel, a new game.svg play sign), Plugins and a plugin's own tab have none; new test hook menuEnabled next to menuRows and menuIcons, e2e 01 checks the greyed out entry on a sprite row, 14a-ii the card and the icons of the fixed tabs, 44a the icon at the wider padding (#176)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b5b91eaaa
commit
60ea56a2ce
17 files changed
+287
-47
No files matched your search
@@ -28,6 +28,10 @@ export const Theme = {
|
||||
controlActive: 0x16181d,
|
||||
accent: 0x4c8dff,
|
||||
accentText: 0xffffff,
|
||||
/** Tab row: the bar the index cards sit on, the card showing, and one under the pointer. */
|
||||
tabBar: 0x1f2229,
|
||||
tabActive: 0x3f444e,
|
||||
tabHover: 0x2c3038,
|
||||
text: 0xe6e8ee,
|
||||
textMuted: 0x8a90a0,
|
||||
selection: 0x2b4a80,
|
||||
|
||||
@@ -29,11 +29,11 @@ const ICON_SIZE = 16;
|
||||
const ICON_GAP = 6;
|
||||
|
||||
export class Button extends Widget {
|
||||
private readonly bg = new Graphics();
|
||||
protected readonly bg = new Graphics();
|
||||
protected readonly labelText: Text;
|
||||
/** The label as set; `labelText` holds the possibly truncated version on screen. */
|
||||
private fullText: string;
|
||||
private pressed = false;
|
||||
protected pressed = false;
|
||||
/** Caller-owned: renders like a primary button but is never flipped by a click. */
|
||||
toggled = false;
|
||||
variant: NonNullable<ButtonOptions['variant']>;
|
||||
@@ -114,15 +114,31 @@ export class Button extends Widget {
|
||||
return this.toggled || this.variant === 'primary' || this.variant === 'danger';
|
||||
}
|
||||
|
||||
/** Style of the label as drawn now. */
|
||||
protected labelToken(): TextStyleToken {
|
||||
return this.onAccent ? 'accent' : this.style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Style the width is measured from: the widest the label can get in any state, or a button that
|
||||
* turns bold when toggled would cut its own text short.
|
||||
*/
|
||||
protected measureToken(): TextStyleToken {
|
||||
return this.variant === 'default' && !this.toggle ? this.style : 'accent';
|
||||
}
|
||||
|
||||
/** Colour of the icon in front of the label; it follows the label. */
|
||||
protected iconTint(): number {
|
||||
return this.onAccent ? Theme.colors.accentText : Theme.colors.textMuted;
|
||||
}
|
||||
|
||||
override measure(): Size {
|
||||
const h = Math.max(this.minHeight, Theme.controlHeight);
|
||||
// A given width is a width, not a floor: a picker must not resize around the name it shows.
|
||||
if (this.fixedWidth) return { w: this.fixedWidth, h };
|
||||
// From the full label, never the on-screen one: measuring truncated text would keep a button
|
||||
// that was once too narrow too narrow for good.
|
||||
// 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.toggle ? this.style : 'accent');
|
||||
const metrics = CanvasTextMetrics.measureText(this.fullText, style);
|
||||
const metrics = CanvasTextMetrics.measureText(this.fullText, textStyle(this.measureToken()));
|
||||
return { w: Math.max(this.minWidth, Math.ceil(metrics.width) + this.padding * 2 + this.iconSpace()), h };
|
||||
}
|
||||
|
||||
@@ -131,6 +147,12 @@ export class Button extends Widget {
|
||||
return this.icon ? ICON_SIZE + ICON_GAP : 0;
|
||||
}
|
||||
|
||||
/** Whether the label is drawn in a bold weight (test hook). */
|
||||
get labelBold(): boolean {
|
||||
const weight = this.labelText.style.fontWeight;
|
||||
return weight === 'bold' || weight === 'bolder' || Number(weight) >= 600;
|
||||
}
|
||||
|
||||
/** Whether an icon is drawn in front of the label (test hook). */
|
||||
get hasIcon(): boolean {
|
||||
return this.icon !== null;
|
||||
@@ -151,7 +173,8 @@ export class Button extends Widget {
|
||||
return false;
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
/** The box behind the label. Overridden where a button is not a rounded rectangle (tabs). */
|
||||
protected drawBackground(): void {
|
||||
const c = Theme.colors;
|
||||
const flat = this.variant === 'default' || this.variant === 'ghost';
|
||||
let fill: number | null = c.control;
|
||||
@@ -184,15 +207,19 @@ 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 });
|
||||
}
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
this.drawBackground();
|
||||
// Style first: it decides the glyph widths that the truncation then measures against.
|
||||
this.labelText.style = textStyle(this.onAccent ? 'accent' : this.style);
|
||||
this.labelText.style = textStyle(this.labelToken());
|
||||
const label = this.labelText;
|
||||
label.text = truncate(this.fullText, label.style, this.labelSlot());
|
||||
const content = this.iconSpace() + label.width;
|
||||
const x = this.align === 'left' ? this.padding : (this.w - content) / 2;
|
||||
label.position.set(Math.round(x + this.iconSpace()), centreY(this.h, label.height));
|
||||
if (this.icon) {
|
||||
const color = this.onAccent ? c.accentText : c.textMuted;
|
||||
const color = this.iconTint();
|
||||
if (color !== this.iconColor) {
|
||||
this.iconColor = color;
|
||||
this.iconGfx.clear();
|
||||
|
||||
@@ -98,6 +98,13 @@ export class Menu extends Popup {
|
||||
.map((child) => (child instanceof Button ? child.text : null));
|
||||
}
|
||||
|
||||
/** Per entry of `rowsDrawn`, whether it can be picked; `null` for a separator (test hook). */
|
||||
enabledDrawn(): (boolean | null)[] {
|
||||
return this.list.children
|
||||
.filter((child) => child !== this.empty && child.visible)
|
||||
.map((child) => (child instanceof Button ? child.enabled : null));
|
||||
}
|
||||
|
||||
/** Per entry of `rowsDrawn`, whether it shows an icon; `null` for a separator (test hook). */
|
||||
iconsDrawn(): (boolean | null)[] {
|
||||
return this.list.children
|
||||
|
||||
Reference in new issue
Block a user