205 lines
7.4 KiB
C
205 lines
7.4 KiB
C
/*
|
|
* 模块名称:UI Menu (菜单导航逻辑层)
|
|
* 模块功能:g_menu_tab 父子索引表驱动的横向级联菜单导航——持有导航状态
|
|
* (路径/选中/深度/滚动,共 17 B)与键语义(↑↓移选、→/ENTER 进、←/ESC 退),
|
|
* 单向驱动显示层 ui_menu_view 建/毁/刷新级联框;本层不持有任何 LVGL 对象
|
|
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
|
* 作者:王建锋
|
|
* 创建日期:2026-08-29
|
|
* 修改记录:
|
|
* 2026-08-29 王建锋 创建初始版本
|
|
* 2026-09-02 王建锋 弃用已废弃的 lv_menu 控件,改为基元控件页面导航
|
|
* 2026-09-02 王建锋 修复选中行文字不可见(白底白字)
|
|
* 2026-09-03 王建锋 主界面按设计稿重构(根页 7 项/装饰/状态行)
|
|
* 2026-09-04 王建锋 重构为"横向级联框"模型(菜单页面设计.md):拆出显示层
|
|
* ui_menu_view 与数据层 ui_menu_tab;本层只留导航状态机
|
|
*/
|
|
#include "ui_menu.h"
|
|
|
|
#include "ui_menu_view.h"
|
|
#include "ui_menu_tab.h"
|
|
|
|
#include <stdint.h>
|
|
#include "lvgl.h"
|
|
|
|
#define DBG_TAG "[UI_MENU]"
|
|
#include "dbg_log.h"
|
|
|
|
/*
|
|
* 导航状态:唯一"必须持久"的导航历史(共 17 B,见设计 §7.2);
|
|
* 条目列表/框几何均可由 g_menu_tab + 本状态随时重算,不落 RAM
|
|
*/
|
|
static uint8_t s_path[MENU_PAGE_MAX]; /* 路径:s_path[d]=第 d 层展示节点(s_path[0]=0 主界面) */
|
|
static uint8_t s_sel[MENU_PAGE_MAX]; /* 各层选中下标(返回时还原,选中不跳) */
|
|
static uint8_t s_depth; /* 当前深度(0=根框)= 焦点所在(最右)框 */
|
|
static uint16_t s_scroll[MENU_PAGE_MAX]; /* 各框滚动偏移(px);框销毁再进入时还原 */
|
|
|
|
/*
|
|
* 函数功能:移选后的层刷新(高亮 + 跟随滚动 + 滚动位回读持久化)
|
|
* 入口参数:depth - 层深度 uint8_t
|
|
* 返回值:无
|
|
* 限定条件:该层框已建
|
|
* 函数说明:显示层跟随滚动后,滚动位写回 s_scroll 持久(框重建时还原)
|
|
*/
|
|
static void menu_refresh_layer(uint8_t depth)
|
|
{
|
|
ui_menu_view_frame_refresh(depth, s_sel[depth]);
|
|
s_scroll[depth] = ui_menu_view_get_scroll(depth);
|
|
}
|
|
|
|
/*
|
|
* 函数功能:进入选中条目(压层)
|
|
* 入口参数:child - 选中子节点下标 uint8_t
|
|
* 返回值:无
|
|
* 限定条件:s_depth+1 < MENU_PAGE_MAX
|
|
* 函数说明: 1. 叶子(无子项)同样压层,生成空白终点框(占位,无条目)
|
|
* 2. 同节点再次进入:还原上次选中与滚动位;换节点则清零
|
|
*/
|
|
static void menu_open(uint8_t child)
|
|
{
|
|
uint8_t d = s_depth;
|
|
bool same = (s_path[d + 1u] == child);
|
|
|
|
s_depth = (uint8_t)(d + 1u);
|
|
s_path[d + 1u] = child;
|
|
s_sel[d + 1u] = same ? s_sel[d + 1u] : 0u;
|
|
|
|
ui_menu_view_frame_build(d + 1u, child, s_sel[d + 1u],
|
|
(uint16_t)(same ? s_scroll[d + 1u] : 0u));
|
|
ui_menu_view_focus(d + 1u);
|
|
}
|
|
|
|
/*
|
|
* 函数功能:关闭最右框(弹层)
|
|
* 入口参数:无
|
|
* 返回值:无
|
|
* 限定条件:s_depth > 0
|
|
* 函数说明: 1. 销毁最右框行对象(既有屏上框不动、无需重建)
|
|
* 2. 焦点回父框;父层选中/滚动原样保留(选中不跳)
|
|
*/
|
|
static void menu_back(void)
|
|
{
|
|
ui_menu_view_frame_destroy(s_depth);
|
|
s_depth--;
|
|
ui_menu_view_focus(s_depth);
|
|
}
|
|
|
|
/*
|
|
* 函数功能:按键语义分发(移/进/退)
|
|
* 入口参数:depth - 焦点所在层深度 uint8_t
|
|
* key - LVGL 键值 lv_key_t
|
|
* 返回值:无
|
|
* 限定条件:ui_menu_init() 已完成
|
|
* 函数说明: 1. UP/DOWN:移选(首尾环绕);选中越出可见区由显示层跟随滚动
|
|
* 2. ENTER/RIGHT:进子框(叶子 → 空白终点框)
|
|
* 3. ESC/LEFT:关最右框(根框不响应)
|
|
* 4. 空框(终点页 0 条目)只响应返回
|
|
*/
|
|
static void menu_key_handle(uint8_t depth, lv_key_t key)
|
|
{
|
|
uint8_t kids[MENU_ITEM_MAX];
|
|
uint8_t cnt = ui_menu_tab_children(s_path[depth], kids, MENU_ITEM_MAX);
|
|
|
|
switch (key) {
|
|
case LV_KEY_UP:
|
|
if (cnt > 0u) {
|
|
s_sel[depth] = (uint8_t)((s_sel[depth] + (uint8_t)cnt - 1u) % cnt);
|
|
menu_refresh_layer(depth);
|
|
}
|
|
break;
|
|
case LV_KEY_DOWN:
|
|
if (cnt > 0u) {
|
|
s_sel[depth] = (uint8_t)((s_sel[depth] + 1u) % cnt);
|
|
menu_refresh_layer(depth);
|
|
}
|
|
break;
|
|
case LV_KEY_ENTER:
|
|
case LV_KEY_RIGHT:
|
|
if (cnt > 0u && depth + 1u < MENU_PAGE_MAX) {
|
|
menu_open(kids[s_sel[depth]]);
|
|
}
|
|
break;
|
|
case LV_KEY_ESC:
|
|
case LV_KEY_LEFT:
|
|
if (depth > 0u) {
|
|
menu_back();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 函数功能:页面容器按键事件入口(每层容器各挂一个,user_data=层深度)
|
|
* 入口参数:e - 事件指针 lv_event_t *
|
|
* 返回值:无
|
|
* 限定条件:容器已加入默认输入组并获焦
|
|
* 函数说明:键值经默认输入组送达焦点容器;统一分发到 menu_key_handle
|
|
*/
|
|
static void menu_key_cb(lv_event_t *e)
|
|
{
|
|
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
|
|
|
|
if (depth < MENU_PAGE_MAX) {
|
|
menu_key_handle(depth, (lv_key_t)lv_event_get_key(e));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 函数功能:构建菜单(横向级联框)
|
|
* 入口参数:无
|
|
* 返回值:0 - 成功,其他 - 失败(表自检失败/对象创建失败)
|
|
* 限定条件:ui_port_init()/sim_port_init() 已成功调用,活动屏幕存在
|
|
* 函数说明: 1. 数据表自检(parent 下标均有效,捕获表内笔误)
|
|
* 2. 重置导航状态 → 初始化显示层
|
|
* 3. 各层容器加入默认输入组并挂键回调(按键由逻辑层统一处理)
|
|
* 4. 建根框(主界面 node 0 子项)并置焦点
|
|
* 5. 构建前后监控 LVGL 池内存并打印
|
|
*/
|
|
int ui_menu_init(void)
|
|
{
|
|
if (!ui_menu_tab_valid()) {
|
|
DBG_ERROR("menu table: INVALID (parent index out of range)");
|
|
return -1;
|
|
}
|
|
|
|
for (uint8_t d = 0u; d < MENU_PAGE_MAX; d++) {
|
|
s_path[d] = 0u;
|
|
s_sel[d] = 0u;
|
|
s_scroll[d] = 0u;
|
|
}
|
|
s_depth = 0u;
|
|
|
|
lv_mem_monitor_t mon;
|
|
lv_mem_monitor(&mon);
|
|
DBG_INFO("menu build start: total=%u free=%u big=%u",
|
|
(unsigned)mon.total_size,
|
|
(unsigned)mon.free_size, (unsigned)mon.free_biggest_size);
|
|
|
|
if (ui_menu_view_init() != 0) {
|
|
return -1;
|
|
}
|
|
|
|
/* 键接线:各层容器加入默认输入组;按键一律由逻辑层处理(显示层纯渲染) */
|
|
for (uint8_t d = 0u; d < MENU_PAGE_MAX; d++) {
|
|
lv_obj_t *cont = ui_menu_view_container(d);
|
|
if (cont == NULL) {
|
|
return -1;
|
|
}
|
|
lv_group_add_obj(lv_group_get_default(), cont);
|
|
lv_obj_add_event_cb(cont, menu_key_cb, LV_EVENT_KEY, (void *)(uintptr_t)d);
|
|
}
|
|
|
|
/* 根框:主界面(node 0)子项;建一次、持久 */
|
|
ui_menu_view_frame_build(0u, 0u, 0u, 0u);
|
|
ui_menu_view_focus(0u);
|
|
|
|
lv_mem_monitor(&mon);
|
|
DBG_INFO("menu build done: total=%u free=%u big=%u used=%u",
|
|
(unsigned)mon.total_size,
|
|
(unsigned)mon.free_size, (unsigned)mon.free_biggest_size,
|
|
(unsigned)(mon.total_size - mon.free_size));
|
|
return 0;
|
|
}
|