import { Container, Graphics, Rectangle, FederatedPointerEvent } from 'pixi.js';
import { PixelText } from './PixelText';
/**
* Кнопка UI: пиксельная рамка, состояния normal/hover/pressed/focused.
* Активация — клик мышью или onSelect() из клавиатурной навигации (MenuList).
*/
export interface ButtonOptions {
label: string;
width: number;
height: number;
size?: number;
/** Недоступная кнопка: приглушена, клик/активация игнорируются. */
disabled?: boolean;
onSelect?: () => void;
}
type ButtonState = 'normal' | 'hover' | 'pressed' | 'focused';
const STATE_COLORS: Record<ButtonState, { fill: number; border: number; text: number }> = {
normal: { fill: 0x16161f, border: 0x8899aa, text: 0xcccccc },
hover: { fill: 0x1e1e2a, border: 0xf0d878, text: 0xffffff },
pressed: { fill: 0x2a2a38, border: 0xf0d878, text: 0xffffff },
focused: { fill: 0x1e1e2a, border: 0xd99a32, text: 0xffffff }
};
const DISABLED_COLORS = { fill: 0x101018, border: 0x555560, text: 0x777780 };
export class Button extends Container {
readonly labelText: PixelText;
onSelect: (() => void) | null;
private bg: Graphics;
private state: ButtonState = 'normal';
private readonly w: number;
private readonly h: number;
private readonly size: number;
private readonly disabled: boolean;
constructor(options: ButtonOptions) {
super();
this.w = options.width;
this.h = options.height;
this.size = options.size ?? 10;
this.disabled = options.disabled ?? false;
this.onSelect = options.onSelect ?? null;
this.bg = new Graphics();
this.labelText = new PixelText({
text: options.label,
size: this.size,
color: this.disabled ? DISABLED_COLORS.text : STATE_COLORS.normal.text
});
this.labelText.anchor.set(0.5);
this.labelText.position.set(this.w / 2, this.h / 2);
this.addChild(this.bg, this.labelText);
// Недоступная кнопка прозрачна для ввода (клики проходят сквозь)
this.eventMode = this.disabled ? 'none' : 'static';
if (!this.disabled) {
this.cursor = 'pointer';
this.hitArea = new Rectangle(0, 0, this.w, this.h);
this.on('pointerover', () => this.setState(this.state === 'pressed' ? 'pressed' : 'hover'));
this.on('pointerout', () => this.setState(this.focused ? 'focused' : 'normal'));
this.on('pointerdown', (e: FederatedPointerEvent) => {
e.stopPropagation();
this.setState('pressed');
});
this.on('pointerup', () => this.setState('hover'));
this.on('pointerupoutside', () => this.setState('normal'));
this.on('pointertap', () => this.onSelect?.());
}
this.draw();
}
/** Клавиатурный фокус (визуально — рамка акцентного цвета). */
set focused(value: boolean) {
if (value && this.state === 'normal') this.setState('focused');
else if (!value && this.state === 'focused') this.setState('normal');
}
get focused(): boolean {
return this.state === 'focused';
}
/** Программная активация (Enter из MenuList). */
activate(): void {
if (this.disabled) return;
this.onSelect?.();
}
private setState(s: ButtonState): void {
this.state = s;
this.labelText.style.fill = STATE_COLORS[s].text;
this.draw();
}
private draw(): void {
const c = this.disabled ? DISABLED_COLORS : STATE_COLORS[this.state];
this.bg.clear();
this.bg.rect(0, 0, this.w, this.h).fill({ color: c.fill, alpha: 0.95 });
this.bg.rect(0, 0, this.w, this.h).stroke({ color: c.border, width: 1 });
}
}