feat: added viewport scrolling
This commit is contained in:
1 parent
4116a94c5c
commit
4377cbf7b9
5 files changed
+211
-15
No files matched your search
+81
-8
@@ -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()` возвращает все элементы меню.
|
||||
|
||||
## Требования для сборки
|
||||
|
||||
|
||||
Reference in new issue
Block a user