feat: added viewport scrolling

This commit is contained in:
aZholtikov committed 2026-10-27 09:07:13 +03:00
1 parent 4116a94c5c
commit 4377cbf7b9
5 files changed
+211 -15

No files matched your search

+81 -8
View File
@@ -13,8 +13,9 @@
3. **Настраиваемые обратные вызовы** — `enter_cb`, `exit_cb` и `change_cb` для реакции на вход в меню, выход из него и изменение текущей позиции.
4. **Выбираемые и простые элементы** — флаг `selectable` разделяет элементы, входящие в режим ручной корректировки, и элементы с одноразовым действием.
5. **Циклическая навигация** — опциональный режим зацикливания при перемещении `UP` / `DOWN` по элементам.
6. **Минимальный расход памяти** — динамически выделяется только один хэндл; узлы меню (`zh_menu_node_t`) не копируются и используют пользовательскую память.
7. **Встроенная статистика** — счётчик переполнений очереди обработки и watermark свободного стека задачи.
6. **Viewport scrolling** — при заданном `viewport_rows` отображается только часть элементов; выделенный элемент всегда виден, индикаторы `↑` / `↓` (определяются пользователем в `change_cb`) показывают скрытые элементы сверху/снизу. Возвращается через `zh_menu_get_view()`.
7. **Минимальный расход памяти** — динамически выделяется только один хэндл; узлы меню (`zh_menu_node_t`) не копируются и используют пользовательскую память.
8. **Встроенная статистика** — счётчик переполнений очереди обработки и watermark свободного стека задачи.
## Внутреннее устройство
@@ -23,7 +24,7 @@
Внутренняя структура хэндла меню (opaque-тип, доступен только через API компонента):
| Поле | Тип | Описание |
|------|-----|----------|
| ------ | ----- | ---------- |
| `menu_stack` | `zh_menu_node_t *[ZH_MENU_MAX_DEPTH]` | Стек пройденных уровней меню для навигации `ZH_MENU_BACK` |
| `current_menu` | `zh_menu_node_t *` | Указатель на текущий активный уровень меню |
| `root` | `zh_menu_node_t *` | Корневой узел дерева меню, установленный при инициализации |
@@ -33,6 +34,8 @@
| `loop` | `bool` | Флаг циклической навигации по элементам |
| `menu_depth` | `uint8_t` | Текущая глубина вложенности (0 — корневой уровень) |
| `current_index` | `uint8_t` | Индекс выделенного элемента на текущем уровне |
| `viewport_rows` | `uint8_t` | Количество видимых строк на экране (0 = показать все) |
| `scroll_offset` | `uint8_t` | Индекс первой видимой строки в окне прокрутки |
| `is_activated` | `bool` | Флаг активации меню (true после `ZH_MENU_ENTER`) |
| `is_selected` | `bool` | Флаг состояния выбора (true — режим корректировки выбираемого элемента) |
| `lock` | `portMUX_TYPE` | Спинлок для защиты полей от одновременного доступа |
@@ -42,7 +45,7 @@
Описывает один элемент меню, определённый пользователем в статическом массиве:
| Поле | Тип | Описание |
|------|-----|----------|
| ------ | ----- | ---------- |
| `label` | `const char *` | Отображаемое имя элемента |
| `submenu` | `struct zh_menu_node_t *` | Указатель на массив вложенных элементов или `NULL` |
| `item_numbers` | `uint8_t` | Количество элементов в массиве `submenu` |
@@ -56,7 +59,7 @@
Передаётся в `zh_menu_init()` для настройки меню:
| Поле | Тип | Описание |
|------|-----|----------|
| ------ | ----- | ---------- |
| `root` | `zh_menu_node_t *` | Корневой узел дерева меню (must not be NULL) |
| `change_cb` | `void (*)(zh_menu_node_t *, uint8_t)` | Колбэк изменения позиции (может быть NULL) |
| `enter_cb` | `void (*)(void *)` | Колбэк входа в меню (может быть NULL) |
@@ -65,6 +68,7 @@
| `stack_size` | `uint16_t` | Размер стека задачи обработки (минимум `configMINIMAL_STACK_SIZE`) |
| `task_priority` | `uint8_t` | Приоритет задачи обработки (минимум 1) |
| `queue_size` | `uint8_t` | Размер очереди команд (минимум 1) |
| `viewport_rows` | `uint8_t` | Количество видимых строк на экране (0 = показать все) |
### Статистика `zh_menu_stats_t`
@@ -75,6 +79,17 @@
| `queue_overflow_error` | `uint32_t` | Счётчик переполнений очереди команд |
| `min_stack_size` | `uint32_t` | Минимальный остаток свободного стека задачи (watermark) |
### Видимое окно меню `zh_menu_view_t`
Возвращается `zh_menu_get_view()` для рендеринга только той части меню, которая помещается на экране:
| Поле | Тип | Описание |
|------|-----|----------|
| `first_visible` | `uint8_t` | Индекс первой строки для отображения |
| `visible_count` | `uint8_t` | Количество строк для отображения (может быть меньше `viewport_rows`) |
Заголовок (метка текущего узла меню) всегда отображается отдельно и не входит в окно прокрутки.
### Задача обработки и очередь
Компонент создаёт фоновую задачу FreeRTOS (`zh_menu`) и очередь сообщений (`_queue_handle`), которые разделяются между всеми инициализированными меню. Параметры (приоритет, размер стека, размер очереди) берутся из конфигурации **первого** инициализированного меню и применяются ко всем последующим. При деинициализации последнего меню задача и очередь удаляются.
@@ -82,7 +97,7 @@
Очередь передаёт команды навигации из контекста вызова публичных функций в фоновую задачу:
| Поле | Тип | Описание |
|------|-----|----------|
| ------ | ----- | ---------- |
| `command` | `zh_menu_navigate_t` | Команда навигации |
| `handle` | `zh_menu_handle_t *` | Указатель на хэндл меню |
| `arg_size` | `uint32_t` | Размер аргумента в байтах |
@@ -108,6 +123,7 @@
| Функция | Описание |
|---------|----------|
| `zh_menu_get()` | Заполняет структуру `zh_menu_status_t` флагами активности меню и состояния выбора текущего элемента. |
| `zh_menu_get_view()` | Возвращает видимое окно текущего уровня меню (`zh_menu_view_t`): индекс первой строки и количество отображаемых строк. Вызывайте из `change_cb` для рендеринга только той части меню, которая помещается на экране. |
### Статистика
@@ -123,10 +139,11 @@
| `zh_menu_handle_t` | Opaque-хэндл меню, представляющий состояние меню. Все операции выполняются через указатель на этот тип. |
| `zh_menu_navigate_t` | Перечисление команд навигации: `ZH_MENU_ENTER`, `ZH_MENU_SHOW`, `ZH_MENU_UP`, `ZH_MENU_DOWN`, `ZH_MENU_SELECT`, `ZH_MENU_BACK`, `ZH_MENU_MAX`. |
| `zh_menu_node_t` | Узел дерева меню, определённый пользователем в статическом массиве. |
| `zh_menu_init_config_t` | Конфигурация инициализации: корневой узел, колбэки, флаг циклической навигации, параметры задачи. |
| `zh_menu_init_config_t` | Конфигурация инициализации: корневой узел, колбэки, флаг циклической навигации, параметры задачи, размер viewport. |
| `zh_menu_status_t` | Текущее состояние меню: `is_active` и `is_selected`. |
| `zh_menu_view_t` | Видимое окно текущего уровня меню: `first_visible` и `visible_count`. |
| `zh_menu_stats_t` | Статистика: `queue_overflow_error` и `min_stack_size`. |
| `ZH_MENU_INIT_CONFIG_DEFAULT()` | Макрос для инициализации конфигурации по умолчанию: приоритет 1, стек `configMINIMAL_STACK_SIZE`, размер очереди 1. |
| `ZH_MENU_INIT_CONFIG_DEFAULT()` | Макрос для инициализации конфигурации по умолчанию: приоритет 1, стек `configMINIMAL_STACK_SIZE`, размер очереди 1, `viewport_rows = 0` (показать все). |
## Потокобезопасность
@@ -139,6 +156,18 @@
## Алгоритмы
### Viewport Scrolling
Когда `viewport_rows > 0` и количество элементов на текущем уровне превышает `viewport_rows`, включается прокрутка:
1. **Инициализация** — при `ZH_MENU_ENTER`, входе в подменю, возврате `ZH_MENU_BACK` и деактивации `scroll_offset` сбрасывается в 0.
2. **Корректировка окна** — при каждом изменении `current_index` (команды `UP` / `DOWN`) вызывается `_zh_menu_adjust_scroll()`:
- Если выделенный элемент выше видимого окна (`current_index < scroll_offset`), окно сдвигается вверх: `scroll_offset = current_index`.
- Если выделенный элемент ниже видимого окна (`current_index >= scroll_offset + viewport_rows`), окно сдвигается вниз: `scroll_offset = current_index - viewport_rows + 1`.
- Окно не может выходить за пределы списка: `scroll_offset` не может быть больше `total - viewport_rows`.
3. **Получение окна** — `zh_menu_get_view()` возвращает `first_visible = scroll_offset` и `visible_count` — количество строк, помещающихся в окне (не больше `viewport_rows` и не больше оставшихся элементов).
4. **Рендеринг** — пользовательский код в `change_cb` вызывает `zh_menu_get_view()` и отображает только видимые строки. Индикаторы `↑` / `↓` определяются пользователем: `↑` — если `scroll_offset > 0`, `↓` — если `first_visible + visible_count < total`.
### Навигация по уровням меню
При каждой команде навигации фоновая задача выполняет следующие шаги:
@@ -353,6 +382,49 @@ void app_main(void)
}
```
### Viewport Scrolling
```c
#include "zh_menu.h"
zh_menu_handle_t *menu_handle = NULL;
void zh_menu_change_callback(zh_menu_node_t *node, uint8_t index)
{
zh_menu_view_t view = {0};
zh_menu_get_view(&menu_handle, &view);
printf("\n=== %s ===\n", node->label);
// Индикатор прокрутки вверх
if (view.first_visible > 0) {
printf(" ↑\n");
}
// Отображение только видимых строк
for (uint8_t i = 0; i < view.visible_count; ++i) {
uint8_t item_idx = view.first_visible + i;
printf("%c %s\n", item_idx == index ? '>' : ' ', node->submenu[item_idx].label);
}
// Индикатор прокрутки вниз
if (view.first_visible + view.visible_count < node->item_numbers) {
printf(" ↓\n");
}
fflush(stdout);
}
void app_main(void)
{
zh_menu_init_config_t menu_config = ZH_MENU_INIT_CONFIG_DEFAULT();
menu_config.root = &main_menu;
menu_config.viewport_rows = 5; // Показывать по 5 строк
menu_config.change_cb = &zh_menu_change_callback;
ESP_ERROR_CHECK(zh_menu_init(&menu_config, &menu_handle));
// ...
}
```
## Логирование
Компонент использует ESP-IDF логирование с тегом `"zh_menu"`. Сообщения об ошибках включают имя файла, номер строки и имя кода ошибки.
@@ -380,6 +452,7 @@ esp_log_level_set("zh_menu", ESP_LOG_ERROR);
5. **Колбэки вызываются вне критической секции** — пользовательские функции (`change_cb`, `enter_cb`, `exit_cb`, `item_cb`) не должны выполнять длительные блокирующие операции.
6. **Размер аргумента ограничен 128 байтами** — буфер `arg` в очереди команд имеет фиксированный размер `ZH_MENU_ARG_MAX_SIZE`.
7. **Публичные функции не защищены мьютексом** — их следует вызывать из одной задачи или обеспечивать внешнюю синхронизацию при доступе из нескольких контекстов.
8. **Viewport_rows = 0 означает «показать все»** — при `viewport_rows == 0` прокрутка отключена и `zh_menu_get_view()` возвращает все элементы меню.
## Требования для сборки