# Экраны и меню — `$.screen`

Подсистема `screen.js` собирает экран (пауза, главное меню, настройки) из
обычных `ui.*`-узлов, но **без ручных координат**: экран описывается данными —
строками/колонками, отступами и выравниванием, — а `.at()` считает раскладка.

`$.screen` — рабочая подсистема для существующих игр, но по закону интерфейса
([UI_RMLUI_LAW.md](UI_RMLUI_LAW)) новые меню и экраны делаются документами
RmlUi (`$.ui.doc`).

```js
$.ready(() => {
    $.screen.define('pause', {
        anchor: 'center', gap: 10, padding: 16, backdrop: true,
        rows: [
            { id: 'title', tag: 'ui.label', text: 'Пауза', style: 'title', h: 40 },
            { id: 'resume', text: 'Продолжить' },
            { id: 'quit',   text: 'В меню', action: 'quit' },
        ],
    });

    $.screen.on('activate', (e) => {
        if (e.id === 'resume') $.screen.close();
        if (e.id === 'quit') $.scene.load('menu');
    });

    $.input.bind('pause', ['escape']);
    $.update(() => { if ($.input.pressed('pause')) $.screen.open('pause'); });
});
```

Узлы живут в координатах окна (`attrs.ui = true`), камера на них не влияет, и в
`$.world.count()` они не попадают.

---

## 1. Описание экрана

| Поле | Тип | По умолчанию | Смысл |
|---|---|---|---|
| `rows` | массив | — | элементы сверху вниз (главная ось — вертикаль) |
| `columns` | массив | — | элементы слева направо |
| `anchor` | строка/объект | `'center'` | положение панели: `'center'`, `'top'`, `'bottom-right'`, `'top left'`, `'full'`, `{ x: 'left', y: 'bottom' }` |
| `margin` | number | 8 | отступ панели от края окна |
| `w` / `h` | number | по содержимому | размер панели; без них панель обнимает содержимое |
| `gap` | number | 10 | расстояние между элементами |
| `padding` | number | 16 | отступ от края панели |
| `align` | `'start'`\|`'center'`\|`'end'`\|`'stretch'` | `rows` → `'stretch'`, `columns` → `'center'` | выравнивание по поперечной оси |
| `style` | string | — | стиль `$.font` для всех элементов (§4) |
| `color` | цвет | `'#101722ee'` | фон панели |
| `backdrop` | bool | `true` | затемняющая подложка на всё окно |
| `backdropColor` | цвет | `'#00000088'` | цвет подложки |
| `id` | string | — | имя панели: узел `#__screen_<id>` |

Элемент — либо **лист** (становится узлом), либо **вложенная группа**
(`rows`/`columns` внутри элемента):

| Поле листа | Тип | По умолчанию | Смысл |
|---|---|---|---|
| `id` | string | — | id узла и адрес для `$.screen.focus/item/rect` |
| `tag` | string | `'ui.button'` | тег узла; подпись — `'ui.label'` |
| `text` | string | — | текст узла |
| `tr` | string | — | ключ `$.i18n` (узел попадёт в автоподстановку `tickI18n`) |
| `size` | number | тег | размер шрифта |
| `color` | цвет | тег | цвет (у контролов — фон, см. `font.md` §2) |
| `w` / `h` | number | таблица ниже | размер; при `align: 'stretch'` растягивается |
| `grow` | number | 0 | делит остаток главной оси пропорционально весу |
| `align` | строка | у группы | своё выравнивание по поперечной оси |
| `style` | string | — | стиль `$.font` именно для этого элемента |
| `action` | string | — | что передать в событие `activate` |
| `on` | объект | — | подписки узла: `{ click: () => … }` |
| `attrs` | объект | — | прочие атрибуты узла как есть |
| `focusable` | bool | по тегу | берёт ли элемент фокус экрана |
| `disabled` | bool | `false` | узел с `attrs.disabled`, фокус не берёт |

Размеры листа по умолчанию (`SCREEN_LEAF_DEFAULTS`):

| Тег | w × h | Тег | w × h |
|---|---|---|---|
| `ui.button` | 200 × 44 | `ui.input` | 240 × 32 |
| `ui.label` | 160 × 28 | `ui.checkbox` | 200 × 28 |
| `ui.panel` | 200 × 100 | `ui.slider` | 240 × 28 |
| `ui.image` | 64 × 64 | `ui.list` / `ui.scroll` | 220 × 160 / 240 × 160 |
| `ui.bar` | 200 × 16 | прочие | 160 × 32 |

## 2. Как считается раскладка

* главная ось группы: `rows` — сумма высот, `columns` — сумма ширин; плюс
  `gap` между элементами и `padding` по краям;
* поперечная ось — максимум поперечных размеров; `align: 'stretch'`
  растягивает элемент на всю внутреннюю ширину (поэтому пункты меню в `rows`
  по умолчанию одной ширины), `center`/`end`/`start` сдвигают его;
* панель без `w`/`h` обнимает содержимое, с `w`/`h` — фиксирована;
* `grow` делит свободное место главной оси: `grow: 1` и `grow: 3` получат
  остаток в отношении 1:3;
* `anchor: 'full'` растягивает панель на всё окно (удобно для настроек);
* вложенная группа **без** `tag` своего узла не создаёт — её дети просто
  оказываются внутри ближайшего родителя с узлом; с `tag` группа становится
  отдельным узлом (её `id` тоже доступен в `focus`/`rect`);
* координаты в раскладке — левый верхний угол (как в CSS), в узлы они
  переводятся центром (`x + w/2`): `ui.*`-узлы позиционируются центром.

Чистые функции (проверяются qjs без движка):

| Функция | Результат |
|---|---|
| `layoutScreen(spec, viewport)` | `{ panel: {x,y,w,h}, items: [{ id, tag, x, y, w, h, parent, focusable }] }` |
| `normalizeScreen(spec)` | нормализованное дерево описания |
| `anchorPosition(anchor, w, h, viewport, margin)` | левый верхний угол панели |
| `parseScreenAnchor(spec)` | `{ hx, vy, full }` |
| `isFocusableTag(tag)` | может ли тег получить фокус |
| `SCREEN_LEAF_DEFAULTS` | размеры листа по умолчанию |

## 3. Функции `$.screen`

| Функция | Назначение |
|---|---|
| `$.screen.define(id, spec)` | объявить экран |
| `$.screen.has(id)` / `list()` / `remove(id)` | реестр экранов |
| `$.screen.open(idOrSpec)` | открыть по имени или по описанию; возвращает обёртку панели или `null` |
| `$.screen.close()` | закрыть и уничтожить узлы (`true`, если было что закрывать) |
| `$.screen.isOpen()` / `current()` | открыт ли экран и его имя |
| `$.screen.panel()` | обёртка панели или `null` |
| `$.screen.item(id)` | обёртка узла элемента или `null` |
| `$.screen.rect(id)` | `{ x, y, w, h }` элемента (левый верхний угол) |
| `$.screen.panelRect()` | прямоугольник панели |
| `$.screen.items()` | id всех элементов в порядке раскладки |
| `$.screen.focus(id)` | поставить фокус (неизвестный/нефокусируемый id → `false` + лог) |
| `$.screen.focused()` / `focusedNode()` | id и обёртка узла в фокусе |
| `$.screen.next()` / `prev()` | фокус по кругу |
| `$.screen.activate()` | «нажать» на элементе в фокусе |
| `$.screen.on(name, fn)` / `off(name, fn)` | подписка на события экрана |
| `$.screen.listenerCount(name)` | сколько подписчиков (для тестов) |

## 4. События

| Событие | Когда | `data` |
|---|---|---|
| `open` | экран открыт | `{ id, node }` |
| `focus` | фокус перешёл на элемент (мышью, стрелками или `focus()`) | `{ id, node, index }` |
| `activate` | `activate()` или Enter/Space | `{ id, action, node, index }` |
| `close` | экран закрыт (в том числе Escape) | `{ id }` |

Узел при активации получает обычный `click` (`{ button: 'left', keyboard: true, id, action }`),
поэтому подписки `on: { click }` из описания работают и с клавиатуры, и от мыши.

## 5. Клавиатура, мышь и фокус

* **стрелки** `↑`/`←` — предыдущий элемент, `↓`/`→` — следующий (по кругу);
* **Enter** / **Space** — активация элемента в фокусе;
* **Escape** — закрыть экран;
* **Tab** экран не перехватывает: это обход контролов в `widgets.js`;
* **мышь**: фокус переходит на элемент под курсором. Клик по узлу отправляет
  уже существующий ui-слой (`ui.js`, `_tick`), поэтому `activate()` из мыши не
  вызывается — иначе одно нажатие приходило бы в игру дважды.

Фокус виден: узлу ставится класс `screen-focus`, `attrs.screenFocus = true` и
подсветка цветом `hover_color` (у `ui.button` это и есть «наведённый» вид).
Цветом, а не только `.hovered`, потому что `tickScreen()` выполняется раньше
`ctx.ui._tick()`, и та в конце кадра сбрасывает `.hovered` по положению мыши.
При уходе фокуса исходный цвет возвращается.

## 6. Ограничения

* экран **не модален**: мир и другие подсистемы продолжают получать ввод.
  Нужна пауза — `$.time.pause()` или своё состояние игры;
* экран один на процесс: `open()` при открытом экране сначала закрывает
  прежний (событие `close` с его id);
* узлы создаются заново на каждый `open()` и уничтожаются в `close()` —
  обёртки, взятые до закрытия, становятся мёртвыми;
* при `open()` снимается фокус `widgets.js` (`$.ui.blur()`), поэтому
  `$.ui.focusedId()` не показывает элемент экрана: у экрана свой фокус
  (`$.screen.focused()`);
* не используйте `ui.row`/`ui.col`/`ui.grid`/`ui.scroll` как `tag` группы:
  `widgets.js` пересчитает их раскладку и затрёт координаты. Для визуальной
  группы берите `ui.panel`;
* при смене размера окна раскладка пересчитывается в `tickScreen()`: узлы
  элементов пересоздаются (панель и подложка остаются), порядок и id
  сохраняются;
* настройки (слайдеры, поля ввода) внутрь экрана ставить можно, но ввод
  текста и Tab остаются за `widgets.js` — экран их не перехватывает.

## 7. Проверка

```bash
build/_deps/quickjs-build/qjs tests/js/screen_test.mjs
python3 tests/agent/highlevel_dialog_test.py    # интеграция, после сборки
```

Покрыто: строки/колонки/вложенность/`grow`/якоря, построение узлов и подложки,
фокус по кругу, стрелки/Enter/Space/Escape, мышь без двойного `click`,
подсветка фокуса, пересчёт при resize, неизвестный экран.
