import { Container } from 'pixi.js';
import { Button } from './Button';
import { ListCursor } from './listCursor';
import { ListScroll } from './listScroll';
/**
* Вертикальное меню: список кнопок с клавиатурным фокусом и навигацией.
* Игра сама читает ввод и вызывает moveCursor/activate — движок не привязан к раскладке.
* Недоступные пункты (disabled) приглушены: курсор их пропускает, клик игнорируется.
* Длинные списки: visibleRows задаёт окно видимых строк (скролл — ListScroll);
* hover мышью переставляет курсор; пункт может нести value (громкость, счётчик).
*/
/** Пункт меню. */
export interface MenuItem {
label: string;
onSelect?: () => void;
/** Недоступный пункт: приглушён, курсор пропускает, активация игнорируется. */
disabled?: boolean;
/** Значение справа (громкость, счётчик) — обновляется через setValue. */
value?: string;
}
export interface MenuListOptions {
width: number;
height: number;
/** Отступ между кнопками (виртуальные пиксели). */
gap?: number;
size?: number;
/** Видимых строк (скролл); нет/0 — окно = весь список, как раньше. */
visibleRows?: number;
}
export class MenuList extends Container {
private buttons: Button[] = [];
private listCursor: ListCursor;
private scroll: ListScroll | null = null;
constructor(private options: MenuListOptions) {
super();
this.listCursor = new ListCursor(0);
}
/** Пересобрать список пунктов (старые кнопки уничтожаются); фокус сохраняется. */
setItems(items: MenuItem[]): void {
for (const b of this.buttons) b.destroy({ children: true });
this.buttons = [];
const rows = this.options.visibleRows ?? 0;
this.scroll = rows > 0 ? new ListScroll(rows) : null;
let y = 0;
for (const item of items) {
const btn = new Button({
label: item.label,
width: this.options.width,
height: this.options.height,
size: this.options.size,
value: item.value,
disabled: item.disabled,
onSelect: item.onSelect
});
btn.position.set(0, y);
// Hover мышью/тачем переставляет курсор (как выбор в DialogueBox).
const index = this.buttons.length;
btn.on('pointerover', () => {
if (!item.disabled && index !== this.listCursor.index) {
this.listCursor.index = index;
this.applyFocus();
}
});
this.addChild(btn);
this.buttons.push(btn);
y += this.step();
}
const keep = Math.min(this.listCursor.index, Math.max(0, items.length - 1));
this.listCursor = new ListCursor(items.length, (i) => !items[i]?.disabled);
this.listCursor.index = keep;
if (items[keep]?.disabled) this.listCursor.move(1); // старт на выбираемом
this.applyFocus();
}
/** Сдвинуть фокус (↑ = -1, ↓ = +1); недоступные пункты пропускаются. */
moveCursor(delta: number): void {
this.listCursor.move(delta);
this.applyFocus();
}
/** Выбранный индекс. */
get index(): number {
return this.listCursor.index;
}
/** Активировать кнопку под фокусом (Enter). */
activate(): void {
this.buttons[this.listCursor.index]?.activate();
}
/** Обновить значение пункта справа (настройки — без пересборки списка). */
setValue(index: number, value: string): void {
const btn = this.buttons[index];
if (btn) btn.value = value;
}
/** Окно видимых строк (или весь список, если скролла нет). */
get window(): { first: number; last: number } | null {
return this.scroll ? { first: this.scroll.first, last: this.scroll.last } : null;
}
private applyFocus(): void {
this.buttons.forEach((b, i) => (b.focused = i === this.listCursor.index));
if (this.scroll) {
this.scroll.sync(this.listCursor.index, this.buttons.length);
for (let i = 0; i < this.buttons.length; i++) {
const visible = i >= this.scroll.first && i < this.scroll.last;
this.buttons[i]!.visible = visible;
this.buttons[i]!.position.y = (i - this.scroll.first) * this.step();
}
}
}
private step(): number {
return this.options.height + (this.options.gap ?? 2);
}
}