feat(menu): экранное меню со стеком экранов и прокруткой
Перенесён из KONOR_ds18b20/lib/menu; в OpticalTester лежала такая же копия. Содержимое экрана движок запрашивает обратными вызовами, поэтому один экран описывает и статический список, и перечень датчиков переменной длины. Рисует через Menu_Painter: от драйвера дисплея не зависит, цвет передаётся как есть — подходит и RGB565, и монохром.
This commit is contained in:
71
c/menu/README.md
Normal file
71
c/menu/README.md
Normal 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).
|
||||||
|
Меню от них не зависит: связывает всё адаптер в приложении.
|
||||||
739
c/menu/menu.c
Normal file
739
c/menu/menu.c
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
/**
|
||||||
|
* @file menu.c
|
||||||
|
* @brief Навигация по экранам меню и их отрисовка через Menu_Painter.
|
||||||
|
*
|
||||||
|
* Экран рисуется целиком: верхняя полоса с заголовком, окно пунктов и нижняя
|
||||||
|
* строка состояния. Перерисовка выполняется только по признаку dirty, поэтому
|
||||||
|
* обмен с дисплеем не занимает главный цикл между нажатиями.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "menu.h"
|
||||||
|
|
||||||
|
/** @brief Тёмная тема в формате RGB565. */
|
||||||
|
#define MENU_COLOR_BACKGROUND 0x0000U /**< Чёрный фон. */
|
||||||
|
#define MENU_COLOR_TITLE_FG 0xFFFFU /**< Белый заголовок. */
|
||||||
|
#define MENU_COLOR_TITLE_BG 0x001FU /**< Синяя полоса заголовка. */
|
||||||
|
#define MENU_COLOR_ITEM_FG 0xC618U /**< Светло-серый текст пункта. */
|
||||||
|
#define MENU_COLOR_VALUE_FG 0x07FFU /**< Голубое значение. */
|
||||||
|
#define MENU_COLOR_CURSOR_FG 0x0000U /**< Чёрный текст выбранного пункта. */
|
||||||
|
#define MENU_COLOR_CURSOR_BG 0xFFE0U /**< Жёлтая подсветка выбора. */
|
||||||
|
#define MENU_COLOR_STATUS_FG 0x8410U /**< Серая строка состояния. */
|
||||||
|
#define MENU_COLOR_SCROLL_FG 0xFFE0U /**< Жёлтые указатели прокрутки. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает число пунктов экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Экран.
|
||||||
|
* @return Число пунктов.
|
||||||
|
*/
|
||||||
|
static uint8_t menu_count(const Menu *menu, const Menu_Screen *screen)
|
||||||
|
{
|
||||||
|
if (screen == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (screen->count != 0) {
|
||||||
|
return screen->count((screen->context != 0) ? screen->context : menu->context);
|
||||||
|
}
|
||||||
|
return screen->item_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает контекст, с которым вызываются обработчики экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Экран.
|
||||||
|
* @return Контекст экрана либо контекст меню.
|
||||||
|
*/
|
||||||
|
static void *menu_context(const Menu *menu, const Menu_Screen *screen)
|
||||||
|
{
|
||||||
|
return (screen->context != 0) ? screen->context : menu->context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Высота одной строки списка вместе с просветом.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Высота строки в пикселях.
|
||||||
|
*/
|
||||||
|
static uint16_t menu_row_height(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint16_t)((menu->painter.char_height * menu->theme.item_scale)
|
||||||
|
+ menu->theme.row_gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Высота верхней полосы с заголовком.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Высота полосы в пикселях.
|
||||||
|
*/
|
||||||
|
static uint16_t menu_title_height(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint16_t)((menu->painter.char_height * menu->theme.title_scale)
|
||||||
|
+ (menu->theme.padding * 2U));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Высота нижней строки состояния.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Высота строки в пикселях.
|
||||||
|
*/
|
||||||
|
static uint16_t menu_status_height(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint16_t)(menu->painter.char_height + menu->theme.padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Пересчитывает число видимых пунктов под текущее оформление.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
static void menu_update_rows(Menu *menu)
|
||||||
|
{
|
||||||
|
const uint16_t reserved = (uint16_t)(menu_title_height(menu) + menu_status_height(menu));
|
||||||
|
const uint16_t row = menu_row_height(menu);
|
||||||
|
uint16_t rows = 0U;
|
||||||
|
|
||||||
|
if ((row != 0U) && (menu->painter.height > reserved)) {
|
||||||
|
rows = (uint16_t)((menu->painter.height - reserved) / row);
|
||||||
|
}
|
||||||
|
if (rows == 0U) {
|
||||||
|
rows = 1U;
|
||||||
|
}
|
||||||
|
if (rows > (uint16_t)MENU_MAX_ROWS) {
|
||||||
|
rows = (uint16_t)MENU_MAX_ROWS;
|
||||||
|
}
|
||||||
|
menu->rows = (uint8_t)rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Приводит курсор и окно прокрутки к допустимым значениям.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
static void menu_clamp(Menu *menu)
|
||||||
|
{
|
||||||
|
const uint8_t level = (uint8_t)(menu->depth - 1U);
|
||||||
|
const uint8_t total = menu_count(menu, menu->stack[level]);
|
||||||
|
|
||||||
|
if (total == 0U) {
|
||||||
|
menu->cursor[level] = 0U;
|
||||||
|
menu->first[level] = 0U;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (menu->cursor[level] >= total) {
|
||||||
|
menu->cursor[level] = (uint8_t)(total - 1U);
|
||||||
|
}
|
||||||
|
if (menu->cursor[level] < menu->first[level]) {
|
||||||
|
menu->first[level] = menu->cursor[level];
|
||||||
|
}
|
||||||
|
if (menu->cursor[level] >= (uint8_t)(menu->first[level] + menu->rows)) {
|
||||||
|
menu->first[level] = (uint8_t)(menu->cursor[level] - menu->rows + 1U);
|
||||||
|
}
|
||||||
|
if (total <= menu->rows) {
|
||||||
|
menu->first[level] = 0U;
|
||||||
|
} else if (menu->first[level] > (uint8_t)(total - menu->rows)) {
|
||||||
|
menu->first[level] = (uint8_t)(total - menu->rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_ThemeDefault(Menu_Theme *theme)
|
||||||
|
{
|
||||||
|
if (theme == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
theme->background = MENU_COLOR_BACKGROUND;
|
||||||
|
theme->title_fg = MENU_COLOR_TITLE_FG;
|
||||||
|
theme->title_bg = MENU_COLOR_TITLE_BG;
|
||||||
|
theme->item_fg = MENU_COLOR_ITEM_FG;
|
||||||
|
theme->item_bg = MENU_COLOR_BACKGROUND;
|
||||||
|
theme->value_fg = MENU_COLOR_VALUE_FG;
|
||||||
|
theme->cursor_fg = MENU_COLOR_CURSOR_FG;
|
||||||
|
theme->cursor_bg = MENU_COLOR_CURSOR_BG;
|
||||||
|
theme->status_fg = MENU_COLOR_STATUS_FG;
|
||||||
|
theme->status_bg = MENU_COLOR_BACKGROUND;
|
||||||
|
theme->scroll_fg = MENU_COLOR_SCROLL_FG;
|
||||||
|
theme->title_scale = 2U;
|
||||||
|
theme->item_scale = 2U;
|
||||||
|
theme->padding = 4U;
|
||||||
|
theme->row_gap = 4U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_Init(Menu *menu, const Menu_Painter *painter, const Menu_Theme *theme,
|
||||||
|
const Menu_Screen *root, void *context)
|
||||||
|
{
|
||||||
|
uint8_t level;
|
||||||
|
|
||||||
|
if ((menu == 0) || (painter == 0) || (root == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if ((painter->fill_rect == 0) || (painter->draw_text == 0)
|
||||||
|
|| (painter->char_width == 0U) || (painter->char_height == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (root->label == 0) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu->painter = *painter;
|
||||||
|
if (theme != 0) {
|
||||||
|
menu->theme = *theme;
|
||||||
|
} else {
|
||||||
|
Menu_ThemeDefault(&menu->theme);
|
||||||
|
}
|
||||||
|
if (menu->theme.title_scale == 0U) {
|
||||||
|
menu->theme.title_scale = 1U;
|
||||||
|
}
|
||||||
|
if (menu->theme.item_scale == 0U) {
|
||||||
|
menu->theme.item_scale = 1U;
|
||||||
|
}
|
||||||
|
menu->context = context;
|
||||||
|
for (level = 0U; level < (uint8_t)MENU_MAX_DEPTH; level++) {
|
||||||
|
menu->stack[level] = 0;
|
||||||
|
menu->cursor[level] = 0U;
|
||||||
|
menu->first[level] = 0U;
|
||||||
|
}
|
||||||
|
menu->stack[0] = root;
|
||||||
|
menu->depth = 1U;
|
||||||
|
menu->status[0] = '\0';
|
||||||
|
|
||||||
|
/* Кэш пуст: первая отрисовка обязана пройти по всему экрану. */
|
||||||
|
menu->cache_screen = 0;
|
||||||
|
menu->cache_first = 0U;
|
||||||
|
menu->cache_valid = 0U;
|
||||||
|
menu->cache_title[0] = '\0';
|
||||||
|
menu->cache_status[0] = '\0';
|
||||||
|
for (level = 0U; level < (uint8_t)MENU_MAX_ROWS; level++) {
|
||||||
|
menu->cache_selected[level] = 0U;
|
||||||
|
menu->cache_label[level][0] = '\0';
|
||||||
|
menu->cache_value[level][0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_update_rows(menu);
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Open(Menu *menu, const Menu_Screen *screen)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (screen == 0) || (screen->label == 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (menu->depth >= (uint8_t)MENU_MAX_DEPTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu->stack[menu->depth] = screen;
|
||||||
|
menu->cursor[menu->depth] = 0U;
|
||||||
|
menu->first[menu->depth] = 0U;
|
||||||
|
menu->depth++;
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Back(Menu *menu)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (menu->depth <= 1U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu->depth--;
|
||||||
|
menu->stack[menu->depth] = 0;
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Home(Menu *menu)
|
||||||
|
{
|
||||||
|
if (menu == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (menu->depth > 1U) {
|
||||||
|
menu->depth--;
|
||||||
|
menu->stack[menu->depth] = 0;
|
||||||
|
}
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_HandleKey(Menu *menu, Menu_Key key)
|
||||||
|
{
|
||||||
|
const Menu_Screen *screen;
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t total;
|
||||||
|
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
level = (uint8_t)(menu->depth - 1U);
|
||||||
|
screen = menu->stack[level];
|
||||||
|
total = menu_count(menu, screen);
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case MENU_KEY_UP:
|
||||||
|
if (total != 0U) {
|
||||||
|
menu->cursor[level] = (uint8_t)((menu->cursor[level] == 0U)
|
||||||
|
? (total - 1U) : (menu->cursor[level] - 1U));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_DOWN:
|
||||||
|
if (total != 0U) {
|
||||||
|
menu->cursor[level] = (uint8_t)((menu->cursor[level] >= (total - 1U))
|
||||||
|
? 0U : (menu->cursor[level] + 1U));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_LEFT:
|
||||||
|
if ((screen->adjust != 0) && (total != 0U)) {
|
||||||
|
screen->adjust(menu_context(menu, screen), menu->cursor[level], -1);
|
||||||
|
} else {
|
||||||
|
Menu_Back(menu);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_RIGHT:
|
||||||
|
if ((screen->adjust != 0) && (total != 0U)) {
|
||||||
|
screen->adjust(menu_context(menu, screen), menu->cursor[level], 1);
|
||||||
|
} else if (screen->enter != 0) {
|
||||||
|
Menu_Open(menu, screen->enter(menu_context(menu, screen), menu->cursor[level]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_ENTER:
|
||||||
|
if ((screen->enter != 0) && (total != 0U)) {
|
||||||
|
Menu_Open(menu, screen->enter(menu_context(menu, screen), menu->cursor[level]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_KEY_BACK:
|
||||||
|
default:
|
||||||
|
Menu_Back(menu);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_clamp(menu);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Invalidate(Menu *menu)
|
||||||
|
{
|
||||||
|
if (menu != 0) {
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_IsDirty(const Menu *menu)
|
||||||
|
{
|
||||||
|
return (uint8_t)((menu != 0) ? menu->dirty : 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_SetStatus(Menu *menu, const char *text)
|
||||||
|
{
|
||||||
|
if (menu == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Menu_TextCopy(menu->status, (uint8_t)MENU_TEXT_MAX, text);
|
||||||
|
menu->dirty = 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Menu_Screen *Menu_Current(const Menu *menu)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return menu->stack[menu->depth - 1U];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_Cursor(const Menu *menu)
|
||||||
|
{
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
return menu->cursor[menu->depth - 1U];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t Menu_GetSnapshot(const Menu *menu, Menu_Snapshot *out)
|
||||||
|
{
|
||||||
|
const Menu_Screen *screen;
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t visible;
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
if ((menu == 0) || (out == 0)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if ((menu->depth == 0U) || (menu->cache_valid == 0U)) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
level = (uint8_t)(menu->depth - 1U);
|
||||||
|
screen = menu->stack[level];
|
||||||
|
|
||||||
|
out->title = menu->cache_title;
|
||||||
|
out->status = menu->cache_status;
|
||||||
|
out->first = menu->cache_first;
|
||||||
|
out->total = menu_count(menu, screen);
|
||||||
|
out->cursor = menu->cursor[level];
|
||||||
|
out->depth = menu->depth;
|
||||||
|
|
||||||
|
/* Строк в снимке столько же, сколько занято на панели: окно минус хвост. */
|
||||||
|
visible = menu->rows;
|
||||||
|
if (visible > (uint8_t)MENU_MAX_ROWS) {
|
||||||
|
visible = (uint8_t)MENU_MAX_ROWS;
|
||||||
|
}
|
||||||
|
if (out->total > out->first) {
|
||||||
|
const uint8_t rest = (uint8_t)(out->total - out->first);
|
||||||
|
|
||||||
|
out->rows = (rest < visible) ? rest : visible;
|
||||||
|
} else {
|
||||||
|
out->rows = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)MENU_MAX_ROWS; index++) {
|
||||||
|
if (index < out->rows) {
|
||||||
|
out->label[index] = menu->cache_label[index];
|
||||||
|
out->value[index] = menu->cache_value[index];
|
||||||
|
out->selected[index] = menu->cache_selected[index];
|
||||||
|
} else {
|
||||||
|
out->label[index] = "";
|
||||||
|
out->value[index] = "";
|
||||||
|
out->selected[index] = 0U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_TextCopy(char *out, uint8_t size, const char *text)
|
||||||
|
{
|
||||||
|
uint8_t index = 0U;
|
||||||
|
|
||||||
|
if ((out == 0) || (size == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text != 0) {
|
||||||
|
while ((text[index] != '\0') && (index < (uint8_t)(size - 1U))) {
|
||||||
|
out[index] = text[index];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[index] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_TextInt(char *out, uint8_t size, int32_t value, const char *suffix)
|
||||||
|
{
|
||||||
|
char digits[12];
|
||||||
|
uint8_t count = 0U;
|
||||||
|
uint8_t index = 0U;
|
||||||
|
uint32_t magnitude;
|
||||||
|
|
||||||
|
if ((out == 0) || (size == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
magnitude = (value < 0) ? (uint32_t)(-value) : (uint32_t)value;
|
||||||
|
do {
|
||||||
|
digits[count] = (char)('0' + (magnitude % 10U));
|
||||||
|
magnitude /= 10U;
|
||||||
|
count++;
|
||||||
|
} while ((magnitude != 0U) && (count < sizeof(digits)));
|
||||||
|
|
||||||
|
if ((value < 0) && (index < (uint8_t)(size - 1U))) {
|
||||||
|
out[index] = '-';
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
while ((count != 0U) && (index < (uint8_t)(size - 1U))) {
|
||||||
|
count--;
|
||||||
|
out[index] = digits[count];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (suffix != 0) {
|
||||||
|
uint8_t tail = 0U;
|
||||||
|
|
||||||
|
while ((suffix[tail] != '\0') && (index < (uint8_t)(size - 1U))) {
|
||||||
|
out[index] = suffix[tail];
|
||||||
|
index++;
|
||||||
|
tail++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[index] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Рисует верхнюю полосу с заголовком открытого экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Открытый экран.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @brief Сравнивает две строки с ограничением по длине буфера кэша.
|
||||||
|
*
|
||||||
|
* @param left Первая строка.
|
||||||
|
* @param right Вторая строка.
|
||||||
|
* @return 1, если строки совпадают, иначе 0.
|
||||||
|
*/
|
||||||
|
static uint8_t menu_text_equal(const char *left, const char *right)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)MENU_TEXT_MAX; index++) {
|
||||||
|
if (left[index] != right[index]) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (left[index] == '\0') {
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Копирует строку в буфер кэша с обрезкой по его размеру.
|
||||||
|
*
|
||||||
|
* @param destination Буфер кэша длиной MENU_TEXT_MAX.
|
||||||
|
* @param source Исходная строка.
|
||||||
|
*/
|
||||||
|
static void menu_text_remember(char *destination, const char *source)
|
||||||
|
{
|
||||||
|
uint8_t index;
|
||||||
|
|
||||||
|
for (index = 0U; index < (uint8_t)(MENU_TEXT_MAX - 1U); index++) {
|
||||||
|
destination[index] = source[index];
|
||||||
|
if (source[index] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destination[MENU_TEXT_MAX - 1U] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void menu_draw_title(Menu *menu, const Menu_Screen *screen, uint8_t force)
|
||||||
|
{
|
||||||
|
const uint16_t height = menu_title_height(menu);
|
||||||
|
const Menu_Painter *painter = &menu->painter;
|
||||||
|
const char *const title = (screen->title != 0) ? screen->title : "";
|
||||||
|
|
||||||
|
/* Заголовок перерисовывается только при смене текста: иначе экран мигает. */
|
||||||
|
if ((force == 0U) && (menu_text_equal(menu->cache_title, title) != 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu_text_remember(menu->cache_title, title);
|
||||||
|
|
||||||
|
painter->fill_rect(painter->context, 0, 0, painter->width, height, menu->theme.title_bg);
|
||||||
|
painter->draw_text(painter->context, (int16_t)menu->theme.padding,
|
||||||
|
(int16_t)menu->theme.padding,
|
||||||
|
(screen->title != 0) ? screen->title : "",
|
||||||
|
menu->theme.title_fg, menu->theme.title_bg, menu->theme.title_scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Рисует нижнюю строку состояния и указатели прокрутки.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param total Число пунктов экрана.
|
||||||
|
* @param first Номер верхнего видимого пункта.
|
||||||
|
*/
|
||||||
|
static void menu_draw_status(Menu *menu, uint8_t total, uint8_t first, uint8_t force)
|
||||||
|
{
|
||||||
|
const Menu_Painter *painter = &menu->painter;
|
||||||
|
const uint16_t height = menu_status_height(menu);
|
||||||
|
const int16_t top = (int16_t)(painter->height - height);
|
||||||
|
char marks[4];
|
||||||
|
char signature[MENU_TEXT_MAX];
|
||||||
|
uint8_t index = 0U;
|
||||||
|
uint8_t length;
|
||||||
|
|
||||||
|
/* Список длиннее окна: показываем, в какую сторону есть скрытые пункты. */
|
||||||
|
if (total > menu->rows) {
|
||||||
|
if (first != 0U) {
|
||||||
|
marks[index] = '^';
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if ((uint8_t)(first + menu->rows) < total) {
|
||||||
|
marks[index] = 'v';
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
marks[index] = '\0';
|
||||||
|
|
||||||
|
/* Подпись объединяет текст и указатели, поэтому сравнение одно на всё. */
|
||||||
|
menu_text_remember(signature, menu->status);
|
||||||
|
length = 0U;
|
||||||
|
while ((length < (uint8_t)(MENU_TEXT_MAX - 1U)) && (signature[length] != '\0')) {
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
if ((uint8_t)(length + index) < (uint8_t)(MENU_TEXT_MAX - 1U)) {
|
||||||
|
uint8_t mark;
|
||||||
|
|
||||||
|
for (mark = 0U; mark < index; mark++) {
|
||||||
|
signature[length] = marks[mark];
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
signature[length] = '\0';
|
||||||
|
}
|
||||||
|
if ((force == 0U) && (menu_text_equal(menu->cache_status, signature) != 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu_text_remember(menu->cache_status, signature);
|
||||||
|
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width, height, menu->theme.status_bg);
|
||||||
|
painter->draw_text(painter->context, (int16_t)menu->theme.padding,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.padding / 2U)),
|
||||||
|
menu->status, menu->theme.status_fg, menu->theme.status_bg, 1U);
|
||||||
|
|
||||||
|
if (index != 0U) {
|
||||||
|
const int16_t x = (int16_t)(painter->width - menu->theme.padding
|
||||||
|
- (index * painter->char_width));
|
||||||
|
|
||||||
|
painter->draw_text(painter->context, x,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.padding / 2U)), marks,
|
||||||
|
menu->theme.scroll_fg, menu->theme.status_bg, 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Рисует одну строку списка: название слева, значение справа.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Открытый экран.
|
||||||
|
* @param item Номер пункта.
|
||||||
|
* @param top Верхняя граница строки.
|
||||||
|
* @param selected 1, если пункт выбран курсором.
|
||||||
|
*/
|
||||||
|
static void menu_draw_row(Menu *menu, const Menu_Screen *screen, uint8_t item,
|
||||||
|
int16_t top, uint8_t selected, uint8_t slot, uint8_t force)
|
||||||
|
{
|
||||||
|
const Menu_Painter *painter = &menu->painter;
|
||||||
|
const uint16_t row = menu_row_height(menu);
|
||||||
|
const uint16_t step = (uint16_t)(painter->char_width * menu->theme.item_scale);
|
||||||
|
const uint32_t foreground = (selected != 0U) ? menu->theme.cursor_fg : menu->theme.item_fg;
|
||||||
|
const uint32_t background = (selected != 0U) ? menu->theme.cursor_bg : menu->theme.item_bg;
|
||||||
|
const uint32_t value_fg = (selected != 0U) ? menu->theme.cursor_fg : menu->theme.value_fg;
|
||||||
|
char label[MENU_TEXT_MAX];
|
||||||
|
char value[MENU_TEXT_MAX];
|
||||||
|
uint8_t label_room;
|
||||||
|
uint8_t value_length = 0U;
|
||||||
|
int16_t x;
|
||||||
|
|
||||||
|
label[0] = '\0';
|
||||||
|
screen->label(menu_context(menu, screen), item, label, (uint8_t)MENU_TEXT_MAX);
|
||||||
|
value[0] = '\0';
|
||||||
|
if (screen->value != 0) {
|
||||||
|
screen->value(menu_context(menu, screen), item, value, (uint8_t)MENU_TEXT_MAX);
|
||||||
|
while (value[value_length] != '\0') {
|
||||||
|
value_length++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Название обрезается так, чтобы значение справа осталось целиком. */
|
||||||
|
if (step != 0U) {
|
||||||
|
const uint16_t columns = (uint16_t)((painter->width - (menu->theme.padding * 2U)) / step);
|
||||||
|
|
||||||
|
label_room = (uint8_t)((columns > (value_length + 1U))
|
||||||
|
? (columns - value_length - 1U) : 1U);
|
||||||
|
if (label_room > (uint8_t)(MENU_TEXT_MAX - 1U)) {
|
||||||
|
label_room = (uint8_t)(MENU_TEXT_MAX - 1U);
|
||||||
|
}
|
||||||
|
label[label_room] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Совпадение с прошлой отрисовкой означает, что строку трогать не нужно. */
|
||||||
|
if ((force == 0U) && (slot < (uint8_t)MENU_MAX_ROWS)
|
||||||
|
&& (menu->cache_selected[slot] == selected)
|
||||||
|
&& (menu_text_equal(menu->cache_label[slot], label) != 0U)
|
||||||
|
&& (menu_text_equal(menu->cache_value[slot], value) != 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (slot < (uint8_t)MENU_MAX_ROWS) {
|
||||||
|
menu->cache_selected[slot] = selected;
|
||||||
|
menu_text_remember(menu->cache_label[slot], label);
|
||||||
|
menu_text_remember(menu->cache_value[slot], value);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width, row, background);
|
||||||
|
painter->draw_text(painter->context, (int16_t)menu->theme.padding,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.row_gap / 2U)), label,
|
||||||
|
foreground, background, menu->theme.item_scale);
|
||||||
|
if (value_length != 0U) {
|
||||||
|
x = (int16_t)(painter->width - menu->theme.padding - (value_length * step));
|
||||||
|
painter->draw_text(painter->context, x,
|
||||||
|
(int16_t)(top + (int16_t)(menu->theme.row_gap / 2U)), value,
|
||||||
|
value_fg, background, menu->theme.item_scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu_Render(Menu *menu, uint8_t force)
|
||||||
|
{
|
||||||
|
const Menu_Screen *screen;
|
||||||
|
const Menu_Painter *painter;
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t total;
|
||||||
|
uint8_t first;
|
||||||
|
uint8_t index;
|
||||||
|
uint8_t full;
|
||||||
|
uint16_t row;
|
||||||
|
int16_t top;
|
||||||
|
int16_t list_bottom;
|
||||||
|
|
||||||
|
if ((menu == 0) || (menu->depth == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((menu->dirty == 0U) && (force == 0U)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
painter = &menu->painter;
|
||||||
|
level = (uint8_t)(menu->depth - 1U);
|
||||||
|
screen = menu->stack[level];
|
||||||
|
menu_update_rows(menu);
|
||||||
|
menu_clamp(menu);
|
||||||
|
total = menu_count(menu, screen);
|
||||||
|
first = menu->first[level];
|
||||||
|
row = menu_row_height(menu);
|
||||||
|
top = (int16_t)menu_title_height(menu);
|
||||||
|
list_bottom = (int16_t)(painter->height - menu_status_height(menu));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Полная отрисовка нужна только при смене экрана, прокрутке списка или по
|
||||||
|
* явному запросу. В остальных случаях перерисовываются лишь те строки,
|
||||||
|
* содержимое которых изменилось, поэтому обновление данных не мигает.
|
||||||
|
*/
|
||||||
|
full = (uint8_t)(((force != 0U) || (menu->cache_valid == 0U)
|
||||||
|
|| (menu->cache_screen != screen) || (menu->cache_first != first))
|
||||||
|
? 1U : 0U);
|
||||||
|
|
||||||
|
menu_draw_title(menu, screen, full);
|
||||||
|
for (index = 0U; index < menu->rows; index++) {
|
||||||
|
const uint8_t item = (uint8_t)(first + index);
|
||||||
|
|
||||||
|
if ((top + (int16_t)row) > list_bottom) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (item < total) {
|
||||||
|
menu_draw_row(menu, screen, item, top,
|
||||||
|
(uint8_t)((item == menu->cursor[level]) ? 1U : 0U), index, full);
|
||||||
|
} else if ((full != 0U) || (index >= (uint8_t)MENU_MAX_ROWS)
|
||||||
|
|| (menu->cache_label[index][0] != 0)
|
||||||
|
|| (menu->cache_value[index][0] != 0)) {
|
||||||
|
/* Строка опустела: гасим её и запоминаем пустое содержимое. */
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width, row,
|
||||||
|
menu->theme.background);
|
||||||
|
if (index < (uint8_t)MENU_MAX_ROWS) {
|
||||||
|
menu->cache_label[index][0] = 0;
|
||||||
|
menu->cache_value[index][0] = 0;
|
||||||
|
menu->cache_selected[index] = 0U;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Пустая строка уже погашена в прошлый раз. */
|
||||||
|
}
|
||||||
|
top = (int16_t)(top + (int16_t)row);
|
||||||
|
}
|
||||||
|
if ((full != 0U) && (top < list_bottom)) {
|
||||||
|
painter->fill_rect(painter->context, 0, top, painter->width,
|
||||||
|
(uint16_t)(list_bottom - top), menu->theme.background);
|
||||||
|
}
|
||||||
|
menu_draw_status(menu, total, first, full);
|
||||||
|
|
||||||
|
menu->cache_screen = screen;
|
||||||
|
menu->cache_first = first;
|
||||||
|
menu->cache_valid = 1U;
|
||||||
|
menu->dirty = 0U;
|
||||||
|
}
|
||||||
317
c/menu/menu.h
Normal file
317
c/menu/menu.h
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
/**
|
||||||
|
* @file menu.h
|
||||||
|
* @brief Портируемое меню для дисплея и шести кнопок навигации.
|
||||||
|
*
|
||||||
|
* Движок хранит стек открытых экранов, курсор и окно прокрутки, а содержимое
|
||||||
|
* запрашивает у приложения обратными вызовами: так один и тот же экран описывает
|
||||||
|
* и статический список, и перечень датчиков, число которых меняется на ходу.
|
||||||
|
*
|
||||||
|
* Рисует движок через таблицу Menu_Painter, поэтому от драйвера дисплея он не
|
||||||
|
* зависит: для ST7789V адаптер занимает несколько строк, для знакосинтезирующего
|
||||||
|
* ЖКИ или SSD1306 — столько же.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MENU_H
|
||||||
|
#define MENU_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** Предельная глубина вложенности экранов. */
|
||||||
|
#define MENU_MAX_DEPTH 4U
|
||||||
|
|
||||||
|
/** Размер буфера строки пункта вместе с завершающим нулём. */
|
||||||
|
#define MENU_TEXT_MAX 32U
|
||||||
|
|
||||||
|
/** Предельное число одновременно видимых пунктов; ограничивает кэш строк. */
|
||||||
|
#define MENU_MAX_ROWS 20U
|
||||||
|
|
||||||
|
/** @brief Кнопки, которые понимает движок меню. */
|
||||||
|
typedef enum {
|
||||||
|
MENU_KEY_UP = 0, /**< Предыдущий пункт. */
|
||||||
|
MENU_KEY_DOWN, /**< Следующий пункт. */
|
||||||
|
MENU_KEY_LEFT, /**< Уменьшить значение пункта. */
|
||||||
|
MENU_KEY_RIGHT, /**< Увеличить значение пункта. */
|
||||||
|
MENU_KEY_ENTER, /**< Войти в пункт или подтвердить. */
|
||||||
|
MENU_KEY_BACK /**< Вернуться на предыдущий экран. */
|
||||||
|
} Menu_Key;
|
||||||
|
|
||||||
|
/** @brief Экран меню; описание неизменно и хранится во флеш-памяти. */
|
||||||
|
typedef struct Menu_Screen Menu_Screen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сообщает текущее число пунктов экрана.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @return Число пунктов; ноль допустим.
|
||||||
|
*/
|
||||||
|
typedef uint8_t (*Menu_CountFn)(void *context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Формирует текст пункта или его значения.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @param index Номер пункта.
|
||||||
|
* @param out Буфер приёмника, завершается нулём.
|
||||||
|
* @param size Размер буфера в байтах.
|
||||||
|
*/
|
||||||
|
typedef void (*Menu_TextFn)(void *context, uint8_t index, char *out, uint8_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает нажатие ввода на пункте.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @param index Номер пункта.
|
||||||
|
* @return Экран для открытия либо 0, если пункт лишь выполняет действие.
|
||||||
|
*/
|
||||||
|
typedef const Menu_Screen *(*Menu_EnterFn)(void *context, uint8_t index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Меняет значение пункта кнопками влево и вправо.
|
||||||
|
*
|
||||||
|
* @param context Контекст экрана либо меню.
|
||||||
|
* @param index Номер пункта.
|
||||||
|
* @param delta Шаг: -1 для влево, +1 для вправо.
|
||||||
|
*/
|
||||||
|
typedef void (*Menu_AdjustFn)(void *context, uint8_t index, int8_t delta);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Описание экрана меню.
|
||||||
|
*
|
||||||
|
* Обязательны заголовок и обратный вызов @c label. Число пунктов берётся из
|
||||||
|
* @c count, а при нулевом указателе — из поля @c item_count.
|
||||||
|
*/
|
||||||
|
struct Menu_Screen {
|
||||||
|
const char *title; /**< Заголовок в верхней полосе. */
|
||||||
|
uint8_t item_count; /**< Число пунктов статического экрана. */
|
||||||
|
Menu_CountFn count; /**< Число пунктов динамического экрана либо 0. */
|
||||||
|
Menu_TextFn label; /**< Текст пункта; обязателен. */
|
||||||
|
Menu_TextFn value; /**< Текст значения справа либо 0. */
|
||||||
|
Menu_EnterFn enter; /**< Реакция на ввод либо 0. */
|
||||||
|
Menu_AdjustFn adjust; /**< Реакция на влево и вправо либо 0. */
|
||||||
|
void *context; /**< Контекст экрана; 0 — использовать контекст меню. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Обратные вызовы отрисовки и метрика шрифта.
|
||||||
|
*
|
||||||
|
* Цвета передаются как есть, поэтому в них укладывается и RGB565, и палитра
|
||||||
|
* монохромного индикатора.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** Заливает прямоугольник цветом. */
|
||||||
|
void (*fill_rect)(void *context, int16_t x, int16_t y, uint16_t width,
|
||||||
|
uint16_t height, uint32_t color);
|
||||||
|
/** Выводит строку с непрозрачным фоном заданным масштабом шрифта. */
|
||||||
|
void (*draw_text)(void *context, int16_t x, int16_t y, const char *text,
|
||||||
|
uint32_t color, uint32_t background, uint8_t scale);
|
||||||
|
void *context; /**< Указатель адаптера дисплея. */
|
||||||
|
uint16_t width; /**< Ширина области вывода, пикселей. */
|
||||||
|
uint16_t height; /**< Высота области вывода, пикселей. */
|
||||||
|
uint8_t char_width; /**< Шаг знакоместа при масштабе 1. */
|
||||||
|
uint8_t char_height; /**< Высота знакоместа при масштабе 1. */
|
||||||
|
} Menu_Painter;
|
||||||
|
|
||||||
|
/** @brief Цвета и отступы оформления. */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t background; /**< Фон рабочей области. */
|
||||||
|
uint32_t title_fg; /**< Текст заголовка. */
|
||||||
|
uint32_t title_bg; /**< Фон заголовка. */
|
||||||
|
uint32_t item_fg; /**< Текст обычного пункта. */
|
||||||
|
uint32_t item_bg; /**< Фон обычного пункта. */
|
||||||
|
uint32_t value_fg; /**< Текст значения обычного пункта. */
|
||||||
|
uint32_t cursor_fg; /**< Текст выбранного пункта. */
|
||||||
|
uint32_t cursor_bg; /**< Фон выбранного пункта. */
|
||||||
|
uint32_t status_fg; /**< Текст нижней строки состояния. */
|
||||||
|
uint32_t status_bg; /**< Фон нижней строки состояния. */
|
||||||
|
uint32_t scroll_fg; /**< Указатели прокрутки. */
|
||||||
|
uint8_t title_scale; /**< Масштаб шрифта заголовка. */
|
||||||
|
uint8_t item_scale; /**< Масштаб шрифта пунктов. */
|
||||||
|
uint8_t padding; /**< Отступ от краёв, пикселей. */
|
||||||
|
uint8_t row_gap; /**< Просвет между пунктами, пикселей. */
|
||||||
|
} Menu_Theme;
|
||||||
|
|
||||||
|
/** @brief Состояние меню; принадлежит вызывающему коду. */
|
||||||
|
typedef struct {
|
||||||
|
const Menu_Screen *stack[MENU_MAX_DEPTH]; /**< Стек открытых экранов. */
|
||||||
|
uint8_t cursor[MENU_MAX_DEPTH]; /**< Выбранный пункт по уровням. */
|
||||||
|
uint8_t first[MENU_MAX_DEPTH]; /**< Верхний видимый пункт. */
|
||||||
|
uint8_t depth; /**< Число экранов в стеке. */
|
||||||
|
Menu_Painter painter; /**< Обратные вызовы отрисовки. */
|
||||||
|
Menu_Theme theme; /**< Оформление. */
|
||||||
|
void *context; /**< Контекст приложения. */
|
||||||
|
uint8_t rows; /**< Сколько пунктов видно сразу. */
|
||||||
|
uint8_t dirty; /**< 1 — экран требует перерисовки. */
|
||||||
|
char status[MENU_TEXT_MAX]; /**< Нижняя строка состояния. */
|
||||||
|
/*
|
||||||
|
* Кэш последней отрисовки. Перерисовываются только изменившиеся строки,
|
||||||
|
* поэтому обновление данных не вызывает мигания всего экрана.
|
||||||
|
*/
|
||||||
|
const Menu_Screen *cache_screen; /**< Экран, для которого кэш верен. */
|
||||||
|
uint8_t cache_first; /**< Верхний видимый пункт в кэше. */
|
||||||
|
uint8_t cache_valid; /**< 1 — кэш содержит отрисованное. */
|
||||||
|
uint8_t cache_selected[MENU_MAX_ROWS]; /**< Признак курсора по строкам. */
|
||||||
|
char cache_label[MENU_MAX_ROWS][MENU_TEXT_MAX]; /**< Названия строк. */
|
||||||
|
char cache_value[MENU_MAX_ROWS][MENU_TEXT_MAX]; /**< Значения строк. */
|
||||||
|
char cache_title[MENU_TEXT_MAX]; /**< Заголовок экрана. */
|
||||||
|
char cache_status[MENU_TEXT_MAX]; /**< Строка состояния с указателями. */
|
||||||
|
} Menu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Снимок видимой части экрана.
|
||||||
|
*
|
||||||
|
* Повторяет то, что сейчас нарисовано на панели: движок отдаёт содержимое из
|
||||||
|
* кэша последней отрисовки, не обращаясь к обратным вызовам приложения.
|
||||||
|
* Поэтому снимок дёшев и совпадает с изображением байт в байт.
|
||||||
|
*
|
||||||
|
* Строки указывают внутрь состояния меню и действительны до следующего
|
||||||
|
* вызова Menu_Render.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
const char *title; /**< Заголовок верхней полосы. */
|
||||||
|
const char *status; /**< Нижняя строка вместе с указателями прокрутки. */
|
||||||
|
const char *label[MENU_MAX_ROWS]; /**< Названия видимых пунктов. */
|
||||||
|
const char *value[MENU_MAX_ROWS]; /**< Значения видимых пунктов; пустая строка, если нет. */
|
||||||
|
uint8_t selected[MENU_MAX_ROWS]; /**< 1 — на этой строке курсор. */
|
||||||
|
uint8_t rows; /**< Число заполненных строк снимка. */
|
||||||
|
uint8_t first; /**< Номер верхнего видимого пункта. */
|
||||||
|
uint8_t total; /**< Всего пунктов на открытом экране. */
|
||||||
|
uint8_t cursor; /**< Выбранный пункт, номер в пределах экрана. */
|
||||||
|
uint8_t depth; /**< Глубина стека экранов; 1 — корневой. */
|
||||||
|
} Menu_Snapshot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Заполняет оформление тёмной темой для цветного дисплея.
|
||||||
|
*
|
||||||
|
* Значения цветов задаются в формате RGB565, отступы рассчитаны на шрифт 6x8.
|
||||||
|
*
|
||||||
|
* @param theme Оформление, принадлежащее вызывающему коду.
|
||||||
|
*/
|
||||||
|
void Menu_ThemeDefault(Menu_Theme *theme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Готовит меню к работе и открывает корневой экран.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param painter Обратные вызовы отрисовки; копируются внутрь состояния.
|
||||||
|
* @param theme Оформление; при значении 0 берётся Menu_ThemeDefault().
|
||||||
|
* @param root Корневой экран.
|
||||||
|
* @param context Контекст приложения для экранов без своего контекста.
|
||||||
|
* @return 1 при успешной настройке, 0 при неполных аргументах.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_Init(Menu *menu, const Menu_Painter *painter, const Menu_Theme *theme,
|
||||||
|
const Menu_Screen *root, void *context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Обрабатывает нажатие кнопки навигации.
|
||||||
|
*
|
||||||
|
* Перерисовка не выполняется: функция только меняет состояние и поднимает
|
||||||
|
* признак устаревшего изображения.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param key Нажатая кнопка.
|
||||||
|
*/
|
||||||
|
void Menu_HandleKey(Menu *menu, Menu_Key key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Открывает вложенный экран, если стек не заполнен.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param screen Открываемый экран.
|
||||||
|
*/
|
||||||
|
void Menu_Open(Menu *menu, const Menu_Screen *screen);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращается на предыдущий экран; на корневом ничего не делает.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
void Menu_Back(Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Закрывает все вложенные экраны и возвращается к корневому.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
void Menu_Home(Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Помечает изображение устаревшим после обновления данных.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
*/
|
||||||
|
void Menu_Invalidate(Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сообщает, требуется ли перерисовка.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return 1, если изображение устарело.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_IsDirty(const Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Задаёт текст нижней строки состояния.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param text Строка либо 0, чтобы очистить строку.
|
||||||
|
*/
|
||||||
|
void Menu_SetStatus(Menu *menu, const char *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Перерисовывает экран, если изображение устарело.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param force 1 — рисовать безусловно.
|
||||||
|
*/
|
||||||
|
void Menu_Render(Menu *menu, uint8_t force);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает открытый экран.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Текущий экран либо 0, если меню не настроено.
|
||||||
|
*/
|
||||||
|
const Menu_Screen *Menu_Current(const Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Возвращает номер выбранного пункта открытого экрана.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @return Номер пункта; 0 для пустого экрана.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_Cursor(const Menu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Снимает видимое содержимое экрана для передачи наружу.
|
||||||
|
*
|
||||||
|
* Источник данных — кэш последней отрисовки, поэтому вызывать функцию имеет
|
||||||
|
* смысл после Menu_Render: до первой отрисовки кэш пуст и снимок не выдаётся.
|
||||||
|
*
|
||||||
|
* @param menu Состояние меню.
|
||||||
|
* @param out Снимок, принадлежащий вызывающему коду.
|
||||||
|
* @return 1, если снимок заполнен, 0 при пустом кэше или неверных аргументах.
|
||||||
|
*/
|
||||||
|
uint8_t Menu_GetSnapshot(const Menu *menu, Menu_Snapshot *out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Копирует строку в буфер пункта с обрезкой по размеру.
|
||||||
|
*
|
||||||
|
* Вспомогательная функция для обратных вызовов приложения.
|
||||||
|
*
|
||||||
|
* @param out Буфер приёмника.
|
||||||
|
* @param size Размер буфера вместе с завершающим нулём.
|
||||||
|
* @param text Копируемая строка либо 0.
|
||||||
|
*/
|
||||||
|
void Menu_TextCopy(char *out, uint8_t size, const char *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Печатает целое со знаком без обращения к stdio.
|
||||||
|
*
|
||||||
|
* @param out Буфер приёмника.
|
||||||
|
* @param size Размер буфера вместе с завершающим нулём.
|
||||||
|
* @param value Печатаемое значение.
|
||||||
|
* @param suffix Приписываемая справа строка либо 0.
|
||||||
|
*/
|
||||||
|
void Menu_TextInt(char *out, uint8_t size, int32_t value, const char *suffix);
|
||||||
|
|
||||||
|
#endif /* MENU_H */
|
||||||
Reference in New Issue
Block a user