Newer
Older
rpg / packages / engine / src / ui / Button.ts
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;
    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 }
};

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;

    constructor(options: ButtonOptions) {
        super();
        this.w = options.width;
        this.h = options.height;
        this.size = options.size ?? 10;
        this.onSelect = options.onSelect ?? null;

        this.bg = new Graphics();
        this.labelText = new PixelText({
            text: options.label,
            size: this.size,
            color: 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 = 'static';
        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 {
        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 = 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 });
    }
}