# UI-контролы — `$.ui` и теги `<ui.*>`

> **Закон интерфейса:** весь UI игры делается на RmlUi
> ([UI_RMLUI_LAW.md](UI_RMLUI_LAW)). Контролы ниже — рабочий HUD-слой
> для существующих игр; новые меню, экраны и диалоги на них не строятся.

Подсистема `widgets.js` дополняет `$.ui` (файл `ui.js`) набором контролов —
аналог `Control`/`Container`/`LineEdit`/`CheckBox`/`Slider`/`ScrollContainer`/
`AcceptDialog` из Godot 4. Всё живёт на обычных узлах интерфейса:

```js
$.ready(() => {
    const menu = $('<ui.col>', { id: 'menu', gap: 10, padding: 16 })
        .at(400, 120).size(320, 260).appendTo($.ui);

    $('<ui.label>', { text: 'Настройки' }).size(220, 28).appendTo(menu);
    $('<ui.checkbox>', { id: 'sound', text: 'Звук' }).size(240, 28).appendTo(menu);
    $('<ui.slider>',   { id: 'vol', min: 0, max: 100, step: 5, value: 70 })
        .size(240, 28).appendTo(menu);
    $('<ui.input>',    { id: 'name', maxLength: 16, placeholder: 'Имя' })
        .size(240, 32).appendTo(menu);

    $('#vol').on('change', (e) => $.sound.volume(e.data.value / 100));
});
```

Все контролы — узлы в координатах окна (`attrs.ui = true`), камера на них не
влияет, и в `$.world.count()` они не попадают. `x`/`y` узла — **центр**, как у
`<ui.button>`.

Поверх абсолютных пикселей модуль даёт **якоря** (узел сам подстраивается под
разрешение окна — §3) и **темы** (общие цвета состояний, размер шрифта и
отступы — §4). Оба механизма включаются явно: узел без якоря и темы ведёт себя
как раньше.

---

## 1. Теги и параметры

| Тег | Назначение | Параметры |
|---|---|---|
| `<ui.row>` | ряд, дети слева направо | `gap`, `padding`, `align`, `color`, `border` |
| `<ui.col>` | колонка, дети сверху вниз | те же |
| `<ui.grid>` | сетка с переносом | те же + `columns` (по умолчанию 2) |
| `<ui.scroll>` | прокручиваемая область | `gap`, `padding`, `align`, `scroll` |
| `<ui.checkbox>` | флажок | `checked`, `text`, `color`, `hoverColor`, `fillColor`, `textColor` |
| `<ui.slider>` | ползунок | `min`, `max`, `step`, `value`, `fillColor`, `color` |
| `<ui.input>` | текстовое поле | `text`/`value`, `maxLength`, `placeholder`, `textColor` |
| `<ui.list>` | вертикальный список (прокрутка и виртуализация) | `items`, `index`, `itemHeight`, `fillColor`, `itemRender` |
| `<ui.dialog>` | модальное окно | `title`, `text`, `buttons`, `closeOnAction` |

**Параметры раскладки**

| Параметр | Тип | По умолчанию | Смысл |
|---|---|---|---|
| `gap` | number | 8 (у `<ui.scroll>` — 6) | расстояние между детьми |
| `padding` | number | 8 | отступ от края контейнера |
| `align` | `'start'`\|`'center'`\|`'end'`\|`'stretch'` | `'start'` | выравнивание по поперечной оси |
| `columns` | number | 2 | число колонок `<ui.grid>` |
| `tabIndex` | number | 0 | порядок обхода Tab (меньше — раньше) |
| `anchorLeft`/`anchorTop`/`anchorRight`/`anchorBottom` | number 0..1 | — | якоря в атрибутах (см. §3) |
| `offsetLeft`/`offsetTop`/`offsetRight`/`offsetBottom` | number | 0 | отступы якоря в пикселях |
| `disabled` | bool | `false` | контрол не берёт ввод и рисуется неактивным |

`<ui.row>`/`<ui.col>`/`<ui.grid>` без `.size()` сами обнимают содержимое
(авторазмер); `<ui.scroll>` наоборот задаёт окно просмотра и прокручивает
содержимое.

Строку `<ui.input>` задавайте через `.value('…')` (метод) или атрибут `text`:
`Node.set('value')` в ядре приводит значение к числу, поэтому строка в
`$('<ui.input>', { value: 'Имя' })` не сохранится — используйте
`$('<ui.input>').value('Имя')` или `{ text: 'Имя' }`.

---

## 2. События

| Событие | Кто шлёт | `event.data` |
|---|---|---|
| `change` | checkbox, slider, input | `{ value }` (у флажка ещё `checked`) |
| `submit` | input по Enter | `{ value }` |
| `select` | list | `{ index, item, value }` |
| `confirm` | dialog | `{ index, text, action }` |
| `cancel` | dialog (кнопка «Отмена», Esc) | `{ index, text, action }` |
| `focus` / `blur` | любой контрол | `{}` |

Плюс общие для всех узлов `$('<ui.*>').on('click'/'mouseenter'/'mouseleave', …)`
и глобальные подписки `$.on('entity:change', …)`.

```js
$('#sound').on('change', (e) => $.store.set('sound', e.data.checked));
$('#vol').on('change', (e) => $.store.set('vol', e.data.value));
$('#name').on('submit', (e) => $.store.set('name', e.data.value));
```

---

## 3. Якоря и относительные размеры

По умолчанию узел стоит в абсолютных пикселях (`.at()`/`.size()`) — на другом
разрешении раскладка разъезжается. Якорь описывает край прямоугольника как
долю родителя плюс пиксельный отступ (модель Godot `Control`):

```
edge = parent.origin + anchor * parent.size + offset
```

`anchorLeft/Top/Right/Bottom` — доли 0..1, `offsetLeft/Top/Right/Bottom` —
отступы. Родитель — узел-`parent_node`; у узла верхнего уровня — окно
(`$.window.size()`). Пока ни один якорь не задан, узел живёт в абсолютных
пикселях, как раньше.

```js
$.ready(() => {
    // Верхняя панель во всю ширину, высотой 48.
    $('<ui.panel>', { id: 'hud', anchorLeft: 0, anchorRight: 1, anchorTop: 0, anchorBottom: 0,
                      offsetTop: 0, offsetBottom: 48 }).appendTo($.ui);

    // Кнопка у правого нижнего угла, 120×40.
    $('<ui.button>', { id: 'ok', text: 'OK' }).size(120, 40)
        .anchorPreset('bottom-right').appendTo($.ui);

    // Полупрозрачная подложка на половину окна.
    $('<ui.panel>', { id: 'half' }).sizePercent('50%', '25%').appendTo($.ui);
});
```

### Пресеты `.anchorPreset(name)`

| Пресет | Якоря (l, t, r, b) | Смысл |
|---|---|---|
| `top-left` / `top-right` / `bottom-left` / `bottom-right` | 0/1 по краям | узел прижат к углу, размер сохраняется |
| `center` | 0.5, 0.5, 0.5, 0.5 | по центру, размер сохраняется |
| `full-rect` (он же `full`) | 0, 0, 1, 1 | на весь родитель |
| `top-wide` / `bottom-wide` | 0,0,1,0 / 0,1,1,1 | во всю ширину сверху/снизу |
| `left-tall` / `right-tall` | 0,0,0,1 / 1,0,1,1 | во всю высоту слева/справа |
| `hcenter` / `vcenter` | 0.5,0,0.5,0 / 0,0.5,0,0.5 | центрирование по одной оси |

Полный список — `$.ui.anchorPresets()`.

### Методы якорей

| Метод | Назначение |
|---|---|
| `.anchor(spec, offsets?)` | якоря строкой (`'left top'`, `'center'`, `'full-rect'`), объектом или числом; `offsets` дополняют отступы, сохраняющие текущий размер (незаданные стороны не схлопываются) |
| `.anchorPreset(name, offsets?)` | готовый пресет из таблицы |
| `.offset(spec)` | точечная правка отступов (число, массив `[l,t,r,b]`, `{left,…}` или `{x,y,w,h}`); значения сторон абсолютные, поэтому сдвиг узла задавайте обеими сторонами оси |
| `.rect(x, y, w, h)` | прямоугольник **от родителя** (в отличие от `.at()`, который считает от окна) |
| `.sizePercent(w, h)` | размер в долях родителя: `'50%'`, `50` или `0.5`; левый верх — из текущего якоря |
| `.anchors()` | текущие якоря `{ left, top, right, bottom }` |
| `.anchorRect()` | посчитанный прямоугольник в координатах родителя `{ x, y, w, h }` |
| `$.ui.anchorPresets()` | список имён пресетов |

Пересчёт идёт в `tickWidgets(dt)`: при изменении размера окна (`$.window.on('resize', …)`
и опрос `$.window.size()`) и при смене спецификации якоря. Якорные узлы **не
участвуют** в автоматической раскладке контейнера — их прямоугольник целиком
считает якорь. Если якорный узел сам является контейнером, его дети
пересобираются при resize.

---

## 4. Темы

Тема — общий набор значений для контролов: цвета состояний, размер шрифта,
отступы, промежутки и правила для отдельных тегов. Значения, которых в теме
нет, остаются такими, как задал узел, поэтому тема ничего не «затирает».

```js
$.ui.theme('dark', {
    extends: 'default',                 // наследование (по умолчанию 'default')
    size: 18, padding: 10, gap: 8,
    colors: {
        normal: '#22304aee', hover: '#2e405f', pressed: '#1a2436', disabled: '#3a4250',
        text: '#ffffff', textDisabled: '#8fa0bb',
        fill: '#4fa3ff', border: '#00000066', accent: '#4fa3ff', focus: '#4fa3ff',
    },
    tags: { 'ui.button': { color: '#2b3a55' } },
});

$('<ui.col>', { id: 'menu' }).theme('dark').appendTo($.ui);   // дети наследуют тему
$('#hint').style({ textColor: '#ffd166', size: 16 });          // точечная правка
```

Короткие ключи — синонимы полей `colors`: `color`, `hoverColor`, `pressedColor`,
`disabledColor`, `textColor`, `textDisabled`, `fillColor`, `borderColor`,
`accentColor`, `focusColor`. Имена состояний (`normal`, `hover`, `pressed`,
`disabled`) тоже можно писать на верхнем уровне темы.

### Наследование и приоритет

1. `.style({…})` на узле — всегда сильнее;
2. `tags['<тег>']` темы узла (например, для `ui.slider`);
3. тема узла (`.theme(name)`) или ближайшего родителя с темой;
4. тема по умолчанию (`$.ui.theme({…})`) — применяется ко всем контролам;
5. значения, заданные на узле напрямую (`.color()`, `.size()`).

`extends` собирает тему цепочкой от `'default'` до указанной; переопределяются
только заданные поля. Применение идёт в `tickWidgets` и при смене темы/стиля,
поэтому прямые `.color()`/`.size()`, вызванные после этого, сохраняются.

### Состояния

| Состояние | Когда | Ключи темы |
|---|---|---|
| `normal` | обычный вид | `normal`/`color`, `text`/`textColor`, `fill`/`fillColor` |
| `hover` | курсор над контролом | `hover`/`hoverColor` |
| `pressed` | кнопка мыши нажата | `pressed`/`pressedColor` |
| `disabled` | `disabled: true` / `.disabled(true)` | `disabled`/`disabledColor`, `textDisabled` |

Акцент (`accent`/`focus`) используется для обводки фокуса и флажка.

### Метод и функция темы

| Метод | Назначение |
|---|---|
| `$.ui.theme(spec)` | задать тему по умолчанию для всех контролов |
| `$.ui.theme(name, spec)` | задать/дополнить именованную тему (сливается со старой) |
| `$.ui.theme(name)` / `$.ui.theme()` | прочитать разобранную тему (с наследованием) |
| `$.ui.themeOf(nodeOrSelector)` | имя действующей темы узла или `null` |
| `$.ui.styleOf(nodeOrSelector)` | размер/отступы/цвета и `states` — для тестов и агента |
| `.theme([name])` | задать/прочитать тему узла, наследуется детьми |
| `.style({…})` | точечные правки поверх темы; без аргумента — чтение |
| `.disabled([bool])` | блокировка: контрол пропускается фокусом и рисуется неактивным |

---

## 5. Публичный API

### `$.ui`

| Метод | Назначение |
|---|---|
| `$.ui.focus(nodeOrSelector)` | поставить фокус на контрол |
| `$.ui.focused()` | обёртка узла в фокусе или `null` |
| `$.ui.focusedId()` | id узла в фокусе или `null` (удобно тестам и агенту) |
| `$.ui.focusNext()` / `$.ui.focusPrev()` | следующий/предыдущий контрол |
| `$.ui.blur([nodeOrSelector])` | снять фокус |
| `$.ui.dialog(opts)` | создать и показать модальный диалог |
| `$.ui.theme(name, spec)` | темы: цвета, размер шрифта, отступы, состояния (см. §4) |
| `$.ui.themeOf(nodeOrSelector)` | имя действующей темы узла |
| `$.ui.styleOf(nodeOrSelector)` | размер/отступы/цвета и `states` узла |
| `$.ui.anchorPresets()` | имена пресетов якорей (см. §3) |

`$.ui.dialog(opts)`: `{ id, title, text, buttons, w, h, onConfirm, onCancel }`.
`buttons` — массив строк или объектов `{ text, action }`; действие `'cancel'`
(или подпись «Отмена»/«Cancel») шлёт `cancel`, остальные — `confirm`.

```js
$.ui.dialog({
    title: 'Выход',
    text: 'Вернуться в меню?',
    buttons: ['Отмена', 'Да'],
    onConfirm: () => $.scene.load('menu'),
});
```

### Методы узла

| Метод | Тег | Назначение |
|---|---|---|
| `.checked([bool])` | checkbox | чтение/запись отметки (шлёт `change`) |
| `.items([array])` | list | элементы списка |
| `.selectedIndex([n])` | list | выбранный индекс (шлёт `select`) |
| `.selectedItem()` | list | значение выбранного элемента |
| `.sliderValue([n])` | slider | значение с приведением к `min`/`max`/`step` |
| `.min(n)` / `.step(n)` | slider | границы и шаг |
| `.maxLength(n)` | input | ограничение длины строки |
| `.placeholder(s)` | input | подсказка пустого поля |
| `.inputValue([s])` | input | строка поля (синоним `.text()`) |
| `.align(s)` / `.gap(n)` / `.padding(n)` | контейнеры | параметры раскладки |
| `.focus()` / `.blur()` | любой | фокус |
| `.openDialog()` / `.closeDialog()` | dialog | показать/скрыть |
| `.anchor(spec, offsets?)` / `.anchorPreset(name)` | любой | якоря и пресеты (§3) |
| `.rect(x,y,w,h)` / `.sizePercent(w,h)` / `.offset(spec)` | любой | прямоугольник от родителя, доли, отступы |
| `.anchors()` / `.anchorRect()` | любой | якоря и посчитанный прямоугольник |
| `.theme([name])` / `.style({…})` | любой | тема узла и точечные правки (§4) |
| `.disabled([bool])` | любой | блокировка контрола |

`value` и `max` — уже существующие методы ядра (`.value(n)`, `.max(n)`), они не
переопределяются.

---

## 6. Фокус и клавиатура

* фокус ставится кликом по контролу или программно;
* `Tab`/`Shift+Tab` обходят контролы в порядке создания, а при заданном
  `tabIndex` — по нему (меньше раньше; при равенстве — порядок создания);
* `<ui.input>`: ввод из `$.input.text()`, `Backspace`/`Delete`, `←`/`→`,
  `Home`/`End`, `Enter` — `submit`, `Esc` — снять фокус, курсор мигает;
* `<ui.slider>`: `←`/`↓`/`→`/`↑` — на шаг, `Home`/`End` — в границы;
* `<ui.checkbox>`: `Space`/`Enter` переключают;
* `<ui.list>`: `↑`/`↓` двигают выбор, `Home`/`End` — в начало/конец;
* `<ui.scroll>`: `↑`/`↓`, `PageUp`/`PageDown`, `Home`;
* `<ui.dialog>`: `←`/`→`/`Tab` — по кнопкам, `Enter`/`Space` — нажать, `Esc` —
  отмена. Пока диалог открыт, остальные контролы не получают ни клавиатуру, ни
  мышь: наведение и клики проходят только по самому окну и его детям.

Ввод за кадр (`$.input.text()`) отдаётся движком один раз, поэтому его
забирает только поле в фокусе — остальные `<ui.input>` его не видят.

---

## 7. Прокрутка

`<ui.scroll>` умеет:

* колесо мыши над контейнером;
* перетаскивание содержимого мышью;
* перетаскивание полосы прокрутки (появляется при переполнении);
* `scroll` всегда ограничен: `scroll ∈ [0, contentHeight − height]`.

Дети, целиком вышедшие за окно просмотра, не рисуются (проверка `_clip` в
отрисовщиках). Частично видимые дети рисуются целиком: `$.gfx.push` не умеет
сциссор (обрезку по прямоугольнику), поэтому точной обрезки текста и спрайтов
по краю нет.

---

## 8. Раскладка и производительность

Пересчёт раскладки идёт в `tickWidgets(dt)` и только для «грязных»
контейнеров: подпись включает детей, их размеры и параметры контейнера
(`gap`, `padding`, `align`, `columns`, `scroll`). Если ничего не менялось,
дерево за кадр не обходится.

Чистые функции раскладки экспортируются и покрыты qjs-тестом:

| Функция | Назначение |
|---|---|
| `layoutRow(box, items, opts)` | прямоугольники детей ряда |
| `layoutCol(box, items, opts)` | прямоугольники детей колонки |
| `layoutGrid(box, items, opts)` | прямоугольники детей сетки |
| `hitTestRect(rect, x, y)` | попадание точки в прямоугольник |
| `pointInNode(node, x, y)` | попадание точки в узел (координаты — центр) |
| `parseAnchor(spec)` / `anchorPreset(name, w, h)` | разбор якорей и пресеты |
| `defaultAnchorOffsets(anchors, w, h)` / `parseOffsets(spec)` | отступы якоря |
| `computeAnchorRect(anchors, offsets, parent)` | прямоугольник якоря в родителе |
| `percentValue(v)` / `isAnchored(node)` | доля из `'50%'`/`50`/`0.5`; есть ли якорь |
| `mergeTheme(base, spec)` / `resolveTheme(name)` | слияние и разбор тем с `extends` |

`box`/`items` — прямоугольники левым верхним углом, `opts` —
`{ gap, padding, align, columns }`.

Порядок в кадре: `applyAnchors()` → `layoutTree()` (раскладка контейнеров) →
`applyThemes()`. Якорные узлы раскладка не трогает, а пересчёт якорей идёт по
`ctx.nodes` в порядке создания (родитель раньше детей).

---

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

* нет сциссора: содержимое `<ui.scroll>` обрезается только «да/нет» по центру
  узла, а не попиксельно;
* `<ui.grid>` выравнивает по поперечной оси внутри ячейки, но не центрирует
  элементы по вертикали строки;
* строку `<ui.input>` нельзя задать атрибутом `value` в конструкторе (ядро
  приводит `value` к числу) — используйте `.value('…')` или `{ text: '…' }`;
* один общий список фокуса: фокус нельзя «запереть» в поддереве, кроме случая
  модального `<ui.dialog>`; заблокированные (`.disabled(true)`) контролы в
  список фокуса не попадают;
* отрисовка идёт в порядке создания узлов, поэтому контейнер создавайте до
  детей (иначе фон контейнера перекроет их), а диалог при показе поднимается
  в конец списка сам;
* стили задаются полями узла (`color`/`hoverColor`/`fillColor`/`textColor`,
  `border`, `size`) или темой (§4); шрифт — системный, размер задаётся `.size()`;
* якорный узел считается от прямоугольника родителя, но родитель-контейнер
  пересобирает свою раскладку в том же кадре до детей: если контейнер меняет
  размер, якорный ребёнок догонит его на следующем кадре;
* у якорного ребёнка `<ui.scroll>` нет области обрезки `_clip` — он рисуется
  без «да/нет»-отсечения по окну просмотра;
* `.theme(null)`/`.style()` не возвращают узлу значения из `TAGS`: снятая тема
  оставляет последние применённые цвета до следующей смены темы/стиля.

---

## 10. Тесты

```bash
build/_deps/quickjs-build/qjs tests/js/widgets_test.mjs
build/_deps/quickjs-build/qjs tests/js/widgets_anchor_test.mjs
```

Первый тест проверяет раскладку row/col/grid, перенос по строкам, хит-тест,
шаг ползунка, обрезку по `maxLength`, порядок фокуса Tab, клики по флажку и
списку, модальность диалога и ограничение прокрутки. Второй — разбор якорей и
пресетов, расчёт прямоугольников при разных размерах окна, реакцию на resize,
процентные размеры, `.rect()` от родителя, атрибуты `anchorLeft/offsetTop`,
темы с наследованием `extends` и от родителя, per-tag правила, `.style()`,
`disabled` и совместимость узлов без якорей и тем.

Интеграционный прогон в движке — `tests/agent/highlevel_widgets_test.py`
(фикстура `tests/fixtures/widgets/`) и `tests/agent/highlevel_widgets_anchor_test.py`
(фикстура `tests/fixtures/widgets_anchor/`); их запускает интегратор после сборки.


---

## Виртуализация списка `<ui.list>`

Список рисует **только видимые строки**: в кадре обходятся не все элементы, а те,
что попали в окно. Список в 100 000 строк рисует ~12 строк за кадр (проверено
`tests/agent/highlevel_list_test.py`), в 100 000 — не «потом», а сразу.

```js
$('<ui.list>', { id: 'inv', itemHeight: 20 }).at(160, 120).size(300, 200)
    .appendTo($.ui);

$.ui.listItems('#inv', items, { index: 0 });   // строки или объекты
$.ui.listScroll('#inv', 400);                  // без аргумента — прочитать
$.ui.listScrollBy('#inv', -40);                // сдвинуть
$.ui.listRange('#inv');                        // { first, last, drawn, total }
$.ui.listIndex('#inv', 3);                     // выбрать (и показать)
```

| Вызов | Смысл |
|---|---|
| `listItems(sel, items, opts?)` | задать строки; `opts.index`, `opts.itemHeight` |
| `listScroll(sel, value?)` | прокрутка в пикселях (зажимается содержимым) |
| `listScrollBy(sel, delta)` | сдвинуть прокрутку |
| `listIndex(sel, value?)` | прочитать или выбрать строку |
| `listRange(sel)` | `{ first, last, drawn, total }` — что реально нарисовано |

**Элементы.** Строка — это строка или объект:

```js
{ text: 'Меч', sub: 'x1', color: '#ffcc00' }
```

`sub` рисуется справа мелким шрифтом, `color` — цвет строки. Свой рендер —
атрибут `itemRender(item, i)`, возвращающий `{ text, sub, color, size }`;
`itemIndex(item, i)` — если текстом элемента служит не само значение.

**Прокрутка.** Колесо мыши прокручивает список под курсором (как `<ui.scroll>`);
стрелки вверх/вниз и `Home`/`End` двигают выборку, и выбранная строка
**удерживается на экране** — иначе стрелка «выбирала» бы то, чего не видно.
Полоса прокрутки рисуется только когда содержимое не помещается.

**Ограничения.** Виртуализация считает строки **одинаковой высоты**
(`itemHeight`); строки разной высоты не поддержаны — для них нужен список
своих узлов внутри `<ui.scroll>`. Горизонтальной прокрутки нет.


---

## Поле ввода `<ui.input>`: буфер обмена, выделение и IME

| Клавиши | Действие |
|---|---|
| `Ctrl+C` / `Ctrl+Insert` | копировать выделение (или весь текст) |
| `Ctrl+X` / `Shift+Delete` | вырезать |
| `Ctrl+V` / `Shift+Insert` | вставить (заменяет выделение, соблюдает `maxLength`) |
| `Ctrl+A` | выделить всё |
| `Shift` + `←`/`→`/`Home`/`End` | выделение с якорем |
| `Backspace` / `Delete` | удалить выделение, иначе один символ |

**IME.** Незавершённая композиция (`nihao` → 你好) приходит событием
`SDL_EVENT_TEXT_EDITING`; сам текст вставляется только по завершении. Окно
кандидатов показывается **у поля**: движку передаётся прямоугольник поля
(`engine.textInputArea`) — без этого кандидаты всплывали в углу окна.

**Буфер обмена** — системный (`SDL_SetClipboardText`/`SDL_GetClipboardText`).
Отдельного `$`-вызова на чтение и запись у игры нет: буфером владеют горячие
клавиши полей ввода выше (вопрос о `$.ui.clipboard` — в [TASKS.md](TASKS)
§13). В headless-прогоне системного буфера нет, поэтому тест подменяет его
на стороне JS и проверяет **горячие клавиши и логику правки**, а не платформу.

**Ограничения.** Выделение не рисуется подсветкой (текст остаётся одного цвета) и
мышью не выделяется — только клавиатурой. Перетаскивание выделения не
поддержано.
