/** * @file menu.h * @brief Портируемое меню для дисплея и шести кнопок навигации. * * Движок хранит стек открытых экранов, курсор и окно прокрутки, а содержимое * запрашивает у приложения обратными вызовами: так один и тот же экран описывает * и статический список, и перечень датчиков, число которых меняется на ходу. * * Рисует движок через таблицу Menu_Painter, поэтому от драйвера дисплея он не * зависит: для ST7789V адаптер занимает несколько строк, для знакосинтезирующего * ЖКИ или SSD1306 — столько же. */ #ifndef MENU_H #define MENU_H #include /** Предельная глубина вложенности экранов. */ #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 */