feat(menu): экранное меню со стеком экранов и прокруткой

Перенесён из KONOR_ds18b20/lib/menu; в OpticalTester лежала такая же копия.

Содержимое экрана движок запрашивает обратными вызовами, поэтому один
экран описывает и статический список, и перечень датчиков переменной
длины. Рисует через Menu_Painter: от драйвера дисплея не зависит,
цвет передаётся как есть — подходит и RGB565, и монохром.
This commit is contained in:
2026-08-23 01:15:12 +03:00
parent 54f0ba7b0e
commit 1248a6551a
3 changed files with 1127 additions and 0 deletions

71
c/menu/README.md Normal file
View File

@@ -0,0 +1,71 @@
# menu
Экранное меню для дисплея и шести кнопок навигации.
Движок хранит стек открытых экранов, курсор и окно прокрутки, а содержимое
запрашивает у приложения обратными вызовами: так один и тот же экран описывает
и статический список, и перечень датчиков, число которых меняется на ходу.
Рисует движок через таблицу `Menu_Painter`, поэтому от драйвера дисплея он не
зависит: для ST7789V адаптер занимает несколько строк, для знакосинтезирующего
ЖКИ или SSD1306 — столько же.
```
приложение
count/text/enter/adjust ← чем наполнен экран
menu.c стек экранов, курсор, прокрутка, перерисовка
Menu_Painter fill_rect, draw_text
драйвер дисплея
```
## Состав
| Файл | Что делает | Зависимости |
|---|---|---|
| `menu.h`, `menu.c` | стек экранов до 4 уровней, курсор, прокрутка, тема оформления, строка состояния | `stdint.h` |
Вспомогательное: `Menu_TextCopy()` и `Menu_TextInt()` — форматирование строк
пунктов без `snprintf`. `Menu_GetSnapshot()` отдаёт состояние без отрисовки,
что удобно для хостовых тестов.
## Что нужно от платформы
Только два вызова отрисовки — цвет передаётся как есть, поэтому в него
укладывается и RGB565, и палитра монохромного индикатора:
```c
void fill_rect(void *ctx, int16_t x, int16_t y, uint16_t w, uint16_t h, uint32_t color);
void draw_text(void *ctx, int16_t x, int16_t y, const char *text,
uint32_t color, uint32_t background, uint8_t scale);
```
## Быстрый старт
```c
static const Menu_Screen screen_root = {
.title = "Главное меню",
.count = root_count, /* сколько пунктов сейчас */
.text = root_text, /* текст пункта по индексу */
.enter = root_enter, /* вернуть вложенный экран или 0 */
.context = &app,
};
Menu menu;
Menu_Theme theme;
Menu_ThemeDefault(&theme);
Menu_Init(&menu, &painter, &theme, &screen_root);
Menu_HandleKey(&menu, MENU_KEY_DOWN);
Menu_Render(&menu, 0); /* перерисует только если что-то изменилось */
```
Данные, меняющиеся сами по себе (температура, состояние связи), обновляются
вызовом `Menu_Invalidate()` — движок перерисует экран на ближайшем `Menu_Render()`.
## Проверено в проектах
`KONOR_ds18b20`, `OpticalTester` — поверх [`st7789`](../st7789) и [`keypad`](../keypad).
Меню от них не зависит: связывает всё адаптер в приложении.