Files
STM32F4-Base/Drivers/BSP/UI/ui_menu_view.c
T
wandering1 7708f12c10 fix(ui/screenlink): 按键应答扣到按键后画面,反馈典型 250-450 ms 降至 70-135 ms
按键召唤答的是按键前的旧帧:uiTask 还没消化键答案就发走了,新画面
只能等下一次周期召唤(280 ms/轮)才带得出——真机反馈 250-450 ms 的
主构成(根因账本 docs/子系统设计/屏板链路设计说明.md §7.1)。

改动(用户批准范围 A+B+C;屏板固件未动,决策三不变):
- sb_link:按键召唤改为只入队不起发;uiTask 同拍重绘并组帧成功后交
  凭据(sb_link_key_rendered),TIM3 1 kHz 的 sb_link_ms_tick 以 ≤1 ms
  放行新帧。140 ms 截止(SB_KEY_RESP_MAX_MS,+59.6 ms 线上 <205 ms
  屏板放弃线)保底发旧帧,"一召一答"契约不破;线忙时同旧丢单、
  不留扣件。起发两中断源(USART1 IDLE/TIM3)共用 sb_tx_kick,
  "判忙→占位"在 PRIMASK 临界区原子完成。
- ui_task:排空注键挪到组帧之前,注键→即派即绘→组帧同拍闭环;
  组帧返回 BSP_OK 才交重绘凭据(BUSY 拍帧还没反映按键)。
- ui_port:注键单槽改 8 深环(连按不再互相覆盖,满丢最旧+计数);
  新增 ui_port_service_keys:同拍 lv_indev_read + lv_refr_now,
  省掉 indev ≤20 ms + 刷新 ≤20 ms 两段等待。
- ui_menu_view:菜单移动选中只失效新旧两行带(黄金参考零像素变化),
  滚动/异常回退整框失效。
- simulator:窗口循环 sleep 扣除本轮已耗时(原先无条件睡满
  lv_timer_handler 返回值,叠加 Windows Sleep 15.6 ms 粒度放大观感)。
- 串口 TX 上报新增 dfok/dfold(凭据放行新帧数/超时保底旧帧数):
  dfold 连涨 = uiTask 被饿 ≥140 ms 的直接现场证据。

验证:test/host/sb_link 164/164(新增 18 条:不立刻起发/凭据放行/
140 ms 截止保底/忙时不留扣件/陈旧凭据不放行等边界);菜单 8 状态 +
内容页 66 张黄金像素 PASS;Keil @build 0 错误 0 警告(两次);
BOM 99 文件无漂移、代码规范、doc-links 本次文件零问题、cppcheck PASS。
文档:屏板规划文档已被主干收编进设计说明,延迟账本落
docs/子系统设计/屏板链路设计说明.md §7.1,并同步 §4 口径、§8 接口、
§9.1 单测台账 146→164、§10 坑账本补条。

RAM 台账(分支不跑 --write,并行分支提交约定):本笔盖戳行
RW_IRAM1 94,336 B (72.0%) / RW_CCM 57,376 B (87.9%),较上一台账行
+4,400 B——为距上次台账构建以来分支累计差(含批 3 状态灯等未盖戳
提交),非全部本笔引入;本次自身增量按对象计为 ui_port.o +12 B
(8 深键环+丢计数+indev 指针)与 sb_link/o 数十字节级。
gate: only mem-budget/mem-numbers pending lead ledger backfill
(合并落主干后 Lead 统一 --write 回填受管区)。
2026-10-30 09:50:31 +08:00

686 lines
29 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI Menu View (菜单显示模型渲染层)
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/持久容器/框体),
* 行不是对象——框体 DRAW_MAIN 自绘(节点下标注册 + GET_SELF_SIZE 虚拟滚动);
* 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-04
* 修改记录:
* 2026-09-04 王建锋 创建:自旧 ui_menu.c 单页模型拆分出显示层(高内聚低耦合)
* 2026-09-08 王建锋 按代码规范 V1.0 补全:分区注释、公共函数注释、关键行注释
* 2026-09-08 王建锋 menu_frame_t 瘦身(uint8_t/int16_t,96→48 B/层);
* s_row_text/s_row_arrow 并入 s_row(省 512 B,行 label 颜色继承)
* 2026-09-08 王建锋 行对象→自绘重写:删行容器/文本/箭头 label 对象,框体
* DRAW_MAIN 自绘行(反色带/文本/▶),GET_SELF_SIZE 虚报
* 内容高实现虚拟滚动(省约 2 KB/行 对象开销)
* 2026-09-08 王建锋 4 层容器→1 个持久容器(焦点/键唯一入口;帧深度由逻辑层
* s_depth 决定,键回调 user_data=depth 冗余);根框装饰
* (浅 V)+状态行改容器自绘(删 3 lv_line + 1 label 对象)
* 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义)
* 2026-10-30 王建锋 按键提速:view_apply_sel 选中移动且未滚动时
* 只失效新旧两行竖带(lv_obj_invalidate_area),绘制量
* 从整框降到 ~2/7;滚动/首建仍整框兜底
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_menu_view.h"
#include "ui_menu_tab.h"
#include "ui_deco.h"
#include "ui_style.h"
#include "ui_tokens.h"
#include "lvgl.h"
#define DBG_TAG "[UI_MENU_VIEW]"
#include "dbg_log.h"
/* 私有宏定义区 */
/*
* 布局常量(设备像素;参考图 02~07 实测 + 字体度量速查推导):
* 汉字步进 12px/字、▶ 步进 10px、行高 15(7 行×15+2 边框=107,与参考图根框一致);
* 行内留白取最小值 2px——最深 4 框级联(根 66 + 42 + 42 + 空框 6 - 3 共享边框)
* = 153px,须 < 屏宽 160
*/
#define MENU_ROW_H 15 /* 行高 */
#define MENU_ROW_TXT_TOP 1 /* 行内文字垂直居中偏移((15-13)/2,字体行高 13) */
#define MENU_ROW_PAD_L 2 /* 行内文字左留白(紧留白,保证级联总宽 < 160) */
#define MENU_ROW_PAD_R 2 /* 行内右侧留白 */
#define MENU_ARROW_GAP 2 /* 行内 文本→箭头 间距 */
#define MENU_FRAME_BORDER 1 /* 框边框宽(子框与父框共享该边框列) */
#define MENU_ROOT_BOX_X 0 /* 根框左(左对齐,贴左缘) */
#define MENU_ROOT_BOX_Y 44 /* 根框顶 */
#define MENU_ROOT_STATUS_X 17 /* 状态行左 */
#define MENU_ROOT_STATUS_Y 20 /* 状态行顶 */
/* 私有类型定义区 */
/*
* 框几何缓存:位置/尺寸是"现算"的(不落导航状态),但子框对齐要用父框刚算出的值,
* 故按层缓存一份;sel 为最近一次应用的高亮(子框垂直对齐依据),由逻辑层经
* build/refresh 保持同步
*/
typedef struct {
uint8_t built; /* 框已建(框体 + 行对象存在):0/1(uint8_t 与系统存储一致) */
uint8_t sel; /* 最近应用的选中行 */
uint8_t item_cnt; /* 行数 */
int16_t x, y; /* 框左上角(含边框;16 位够:y 钳制前可为负) */
int16_t w, h; /* 框外廓尺寸(16 位够:最大 16×15+2=242) */
} menu_frame_t;
/* 静态变量定义区 */
static lv_obj_t *s_cont; /* 单一持久全屏容器(焦点/键唯一入口;各层框均建其内;永不删) */
static lv_obj_t *s_frame_box[MENU_PAGE_MAX]; /* 框体:d=0 持久(根框);d>=1 进入时建、返回时毁(含自绘回调) */
static uint8_t s_row_node[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行数据:节点下标(名称/箭头绘制时现查,无对象) */
static menu_frame_t s_frame[MENU_PAGE_MAX]; /* 每层框几何缓存:位置/尺寸/选中行(现算不落状态,供子框对齐;见上方类型注释) */
/* 样式(静态,一次性初始化,常驻) */
/* flat(透明、无边框/圆角/内边距,剥离主题 card)取自共享样式表 ui_style.c:
* 与 ui_content_page / ui_standby 共用同一 const 对象,不再本地重复定义 */
static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
/* 函数定义区 */
/*
* 函数功能:初始化显示层样式(静态样式,仅执行一次)
* 入口参数:无
* 返回值:无
* 限定条件:无
* 函数说明: 1. flat 剥离主题给对象附加的卡片样式(边框/圆角/内边距;常量样式,见上方定义)
* 2. frame 为所有级联框(含根框)的 1px 白边框
* 3. 选中行反色不用样式:自绘回调按 sel 直接画白带(行不是对象)
*/
static void view_styles_init(void) {
static bool inited = false;
/* 一次性初始化:已初始化则直接返回 */
if (inited) {
return;
}
inited = true;
/* frame:透明底 + 1px 白边框(所有级联框含根框) */
lv_style_init(&s_style_frame);
lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_frame, MENU_FRAME_BORDER);
lv_style_set_border_color(&s_style_frame, UI_COL_FG);
lv_style_set_radius(&s_style_frame, 0);
lv_style_set_pad_all(&s_style_frame, 0);
lv_style_set_pad_gap(&s_style_frame, 0);
/* scrollbar:白色 4px 宽(行超框高时自动出现) */
lv_style_init(&s_style_scrollbar);
lv_style_set_bg_opa(&s_style_scrollbar, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_scrollbar, UI_COL_FG);
lv_style_set_width(&s_style_scrollbar, UI_SCROLLBAR_W);
}
/*
* 函数功能:计算框外廓宽(内容自适应)
* 入口参数:kids - 子节点下标数组(各行内容) const uint8_t *
* cnt - 条目数 uint8_t
* 返回值:框外廓宽 px int32_t = 最宽行内容 + 左右边框
* 限定条件:LVGL 已初始化(字体度量可用)
* 函数说明:行内容宽 = 左留白 + 文本 + (菜单 ? 间距 + ▶ : 0) + 右留白;
* 文本/▶ 宽取字体实际步进(汉字 12px、▶ 10px);
* 注意本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen
*/
static int32_t view_frame_width(const uint8_t *kids, uint8_t cnt) {
const lv_font_t *font = LV_FONT_DEFAULT;
int32_t w = 0;
/* 遍历各行,取内容宽最大值 */
for (uint8_t i = 0; i < cnt; i++) {
const char *name = ui_menu_tab_name(kids[i]);
/* 量文本实际步进(本 LVGL 版 length=0 意为"不测量",须传 strlen) */
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), font, 0);
int32_t cw = MENU_ROW_PAD_L + tw + MENU_ROW_PAD_R;
/* 菜单行(有子项):加箭头间距 + ▶ 宽 */
if (ui_menu_tab_has_children(kids[i])) {
cw += MENU_ARROW_GAP + lv_text_get_width("▶", (uint32_t)strlen("▶"), font, 0);
}
/* 取最宽行 */
if (cw > w) {
w = cw;
}
}
/* 加左右边框 */
return w + 2 * MENU_FRAME_BORDER;
}
/*
* 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头)
* 入口参数:a - 区域 1 const lv_area_t *
* b - 区域 2 const lv_area_t *
* 返回值:true - 相交;false - 不相交
* 限定条件:无
* 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 lv_area.c 同语义)
*/
static bool areas_intersect(const lv_area_t *a, const lv_area_t *b) {
int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1;
int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1;
int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2;
int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2;
return (x1 <= x2 && y1 <= y2);
}
/*
* 函数功能:框体虚拟内容高上报(滚动范围 = 内容高 - 内容区高)
* 入口参数:e - 事件指针(user_data = 层深度) lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_GET_SELF_SIZE,布局阶段由 LVGL 调用
* 函数说明:行不是子对象,经 GET_SELF_SIZE 虚报内容高,
* LVGL 据此算滚动范围与自动滚动条(替代旧"行对象撑内容高")
*/
static void view_frame_self_size_cb(lv_event_t *e) {
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
lv_point_t *p = lv_event_get_param(e);
p->x = 0; /* 无横向虚拟内容(行整宽,无横向滚动) */
p->y = (int32_t)s_frame[depth].item_cnt * MENU_ROW_H; /* 内容高 = 行数×行高 */
}
/*
* 函数功能:框体自绘(选中行反色带 + 行文本 + 箭头)
* 入口参数:e - 事件指针(user_data = 层深度) lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,框体已建、行已注册
* 函数说明: 1. 行不是对象:按滚动偏移枚举可见行号,逐行画白带/文本/▶
* 2. 选中行反色(白底黑字)、非选中黑底白字(与旧 label 渲染同
* 字体光栅化路径,1bpp 下像素级一致)
* 3. 不在重绘区的行跳过(culling)
*/
static void view_frame_draw_cb(lv_event_t *e) {
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
const menu_frame_t *f = &s_frame[depth];
lv_obj_t *box = s_frame_box[depth];
lv_layer_t *layer = lv_event_get_layer(e);
lv_area_t area;
int32_t i_first, i_last, i;
lv_obj_get_content_coords(box, &area); /* 内容区(框内、边框之内) */
int32_t sy = lv_obj_get_scroll_y(box); /* 纵向滚动偏移(px) */
/* 可见行范围:行 i 占内容 y ∈ [i*15, i*15+15) */
i_first = sy / MENU_ROW_H;
i_last = (sy + lv_obj_get_content_height(box) - 1) / MENU_ROW_H;
if (i_last >= (int32_t)f->item_cnt) {
i_last = (int32_t)f->item_cnt - 1;
}
for (i = i_first; i <= i_last; i++) {
int32_t row_y = area.y1 + i * MENU_ROW_H - sy;
lv_area_t row_area;
const char *name = ui_menu_tab_name(s_row_node[depth][i]);
bool is_sel = (i == (int32_t)f->sel);
lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG;
row_area.x1 = area.x1;
row_area.x2 = area.x2;
row_area.y1 = row_y;
row_area.y2 = row_y + MENU_ROW_H - 1;
/* 行不在重绘区:跳过(culling) */
if (!areas_intersect(&row_area, &layer->_clip_area)) {
continue;
}
/* 选中行:整行反色(白带) */
if (is_sel) {
lv_draw_rect_dsc_t fd;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = UI_COL_SEL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &row_area);
}
/* 行文本:左留白、垂直居中偏移(与旧 label 同位) */
lv_draw_label_dsc_t ld;
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = name;
ld.text_static = 1; /* Flash 字符串:不拷内存 */
ld.color = color; /* 选中黑 / 非选中白 */
ld.ofs_x = MENU_ROW_PAD_L;
ld.ofs_y = MENU_ROW_TXT_TOP;
lv_draw_label(layer, &ld, &row_area);
/* 箭头:仅菜单行(有子项),紧跟文本(间距自适应) */
if (ui_menu_tab_has_children(s_row_node[depth][i])) {
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), LV_FONT_DEFAULT, 0);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = "▶";
ld.text_static = 1;
ld.color = color;
ld.ofs_x = MENU_ROW_PAD_L + tw + MENU_ARROW_GAP;
ld.ofs_y = MENU_ROW_TXT_TOP;
lv_draw_label(layer, &ld, &row_area);
}
}
}
/*
* 函数功能:把选中行滚入可见区(手动跟随滚动,替代 scroll_to_view)
* 入口参数:box - 框体 lv_obj_t *
* f - 框几何缓存(含 sel) const menu_frame_t *
* 返回值:无
* 限定条件:box 已创建且已虚报内容高
* 函数说明:选中行在可见区之上 → 顶对齐;之下 → 底对齐;已在区内 → 不动
* ANIM_OFF:回读滚动位 = 最终值(逻辑层靠回读持久化)
*/
static void view_scroll_to_sel(lv_obj_t *box, const menu_frame_t *f) {
int32_t sy;
int32_t row_top;
int32_t view_h;
if (f->item_cnt == 0u) {
return; /* 空框无行可跟随 */
}
sy = lv_obj_get_scroll_y(box);
row_top = (int32_t)f->sel * MENU_ROW_H;
view_h = lv_obj_get_content_height(box); /* 内容区高(框内、边框之内) */
if (row_top < sy) {
sy = row_top; /* 选中行在可见区之上:顶对齐 */
}
else if (row_top + MENU_ROW_H > sy + view_h) {
sy = row_top + MENU_ROW_H - view_h; /* 选中行在可见区之下:底对齐 */
}
if (sy < 0) {
sy = 0;
}
if (sy != lv_obj_get_scroll_y(box)) {
lv_obj_scroll_to_y(box, sy, LV_ANIM_OFF);
}
}
/*
* 函数功能:应用指定深度框的选中(选中数据 + 跟随滚动 + 失效重绘)
* 入口参数:depth - 深度 uint8_t
* sel - 选中行 uint8_t
* 返回值:无
* 限定条件:该层框体已建
* 函数说明:1. 行不是对象:选中只是数据,自绘回调绘制时定反色带/文字色
* 2. 选中行滚入可见区(手动计算,无行对象可供 scroll_to_view)
* 3. 失效范围按改动面收敛(2026-10-30 按键提速 C):
* · 选中变了且**没发生滚动** → 只有新旧两行外观变化,
* 只脏这两行组成的竖带(根框 107 px 高、行高 15 px,绘制量
* 从整框降到 ~2/7;自绘回调本就有 clip 剔除,带外行直接跳过)
* · 发生滚动 / 首次应用(新旧同号)→ 整框失效兜底
* 4. 失效区用绝对坐标,与自绘回调同一坐标框架
* (content_coords 起算、减滚动偏移)
*/
static void view_apply_sel(uint8_t depth, uint8_t sel) {
menu_frame_t *f = &s_frame[depth];
lv_obj_t *box = s_frame_box[depth];
uint8_t sel_old = f->sel;
int32_t sy_before = lv_obj_get_scroll_y(box);
f->sel = sel;
view_scroll_to_sel(box, f); /* 选中行必在可见区内 */
if ((sel_old != sel) && (lv_obj_get_scroll_y(box) == sy_before)) {
lv_area_t ca;
lv_area_t band;
int32_t y_old;
int32_t y_new;
lv_obj_get_content_coords(box, &ca);
y_old = ca.y1 + (int32_t)sel_old * MENU_ROW_H - sy_before;
y_new = ca.y1 + (int32_t)sel * MENU_ROW_H - sy_before;
band.x1 = ca.x1;
band.x2 = ca.x2;
band.y1 = (y_old < y_new) ? y_old : y_new;
band.y2 = (y_old > y_new) ? y_old : y_new;
band.y2 += (MENU_ROW_H - 1);
if (band.y1 < ca.y1) {
band.y1 = ca.y1; /* 旧选中行可能被框顶裁掉(级联框贴屏上沿时) */
}
if (band.y2 > ca.y2) {
band.y2 = ca.y2; /* 同理框底 */
}
if (band.y1 <= band.y2) {
lv_obj_invalidate_area(box, &band);
return;
}
}
lv_obj_invalidate(box); /* 兜底:有滚动或首次建立,整框重绘 */
}
/*
* 函数功能:配置框体滚动行为(自绘行无子对象,仅配置自动滚动条)
* 入口参数:box - 框体 lv_obj_t *
* 返回值:无
* 限定条件:box 已创建
* 函数说明:行内容超框高时框内滚动(本菜单最深 9 行不触发,机制为更长列表保留)
*/
static void view_box_layout(lv_obj_t *box) {
/* 自动滚动条:仅当行内容超框高时出现(框内滚动) */
lv_obj_set_scrollbar_mode(box, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_style(box, &s_style_scrollbar, LV_PART_SCROLLBAR);
}
/*
* 函数功能:构建指定深度的框(几何现算 + 行数据注册 + 选中/滚动应用)
* 入口参数:depth - 深度(0=根框) uint8_t
* node - 本框展示节点(行 = 其子项) uint8_t
* sel - 本框选中行 uint8_t
* scroll - 本框滚动偏移(px,0=顶部) uint16_t
* 返回值:无
* 限定条件:depth>0 时 depth-1 层框已建(父框几何是其对齐依据)
* 函数说明: 1. d=0:根框框体已存在,仅(重)注册行数据
* 2. d>0:右贴靠父框右缘;顶边=父框选中行行顶,超屏底则
* 上移使本框底边对齐父框选中行行底,仍超顶则钳制顶部并改框内滚动
* 3. 框宽 = 最宽行(文本 + ▶ + 左右留白)+ 边框;条目现算不落地
*/
void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll) {
menu_frame_t *f = &s_frame[depth];
lv_obj_t *box;
uint8_t kids[MENU_ITEM_MAX];
uint8_t cnt = ui_menu_tab_children(node, kids, MENU_ITEM_MAX);
if (depth == 0u) {
box = s_frame_box[0]; /* 根框框体持久(view_init 已建) */
}
else {
if (f->built) {
ui_menu_view_frame_destroy(depth); /* 防御:重入 = 先毁后建 */
}
const menu_frame_t *p = &s_frame[depth - 1u];
/* X:右贴靠——子框左缘 = 父框右缘(共享边框列、零间隙) */
f->x = p->x + p->w - MENU_FRAME_BORDER;
/* Y:顶对齐父框"选中行"行顶;(顶边+框高) 超屏底 → 上移使本框底边
* 对齐父框"选中行"行底(顶部不钳制,可叠到状态行区);
* 仍超顶(比整屏高)→ 钳制顶部,框高截到可见区、行改框内滚动 */
int32_t row_top = p->y + MENU_FRAME_BORDER + (int32_t)p->sel * MENU_ROW_H;
int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER;
f->y = row_top;
if (f->y + h > MENU_SCREEN_H) {
f->y = row_top + MENU_ROW_H - h;
}
if (f->y < 0) {
f->y = 0;
}
if (f->y + h > MENU_SCREEN_H) {
h = MENU_SCREEN_H - f->y;
}
f->h = h;
f->w = (cnt > 0u) ? view_frame_width(kids, cnt)
: (2 * MENU_FRAME_BORDER + MENU_ROW_PAD_L + MENU_ROW_PAD_R);
box = lv_obj_create(s_cont);
if (box == NULL) {
return; /* LVGL 池耗尽:框未建,状态回滚 */
}
lv_obj_add_style(box, &ui_style_flat, 0);
lv_obj_add_style(box, &s_style_frame, 0);
lv_obj_add_flag(box, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(box, f->x, f->y);
lv_obj_set_size(box, f->w, f->h);
view_box_layout(box);
/* 自绘 + 虚拟内容高:回调 user_data 装层深度(回调内拆回) */
lv_obj_add_event_cb(box, view_frame_draw_cb, LV_EVENT_DRAW_MAIN, (void *)(uintptr_t)depth);
lv_obj_add_event_cb(box, view_frame_self_size_cb, LV_EVENT_GET_SELF_SIZE, (void *)(uintptr_t)depth);
s_frame_box[depth] = box;
}
/* 更新框几何缓存(子框对齐的依据) */
f->built = 1u;
f->item_cnt = cnt;
f->sel = sel;
/* 行数据:条目现算(= parent==node 过滤),注册节点下标、自绘(无对象) */
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row_node[depth][i] = 0u; /* 清旧(超 cnt 的行不绘制) */
}
for (uint8_t i = 0; i < cnt; i++) {
s_row_node[depth][i] = kids[i];
}
/* 滚动位:有保存先还原,再跟随滚动保证选中行可见 */
if (scroll > 0u) {
lv_obj_scroll_to_y(box, (int32_t)scroll, LV_ANIM_OFF);
}
view_apply_sel(depth, sel); /* 选中 + 跟随滚动 + 失效重绘 */
}
/*
* 函数功能:销毁指定深度的框(框体连自绘回调整体删除,容器保留)
* 入口参数:depth - 深度 uint8_t
* 返回值:无
* 限定条件:depth>0(根框持久不可销毁)
* 函数说明:返回(关最右框)时调用,LVGL 池内存随即回落
*/
void ui_menu_view_frame_destroy(uint8_t depth) {
menu_frame_t *f = &s_frame[depth];
if (depth == 0u || !f->built) {
return; /* 根框持久不可销毁 */
}
lv_obj_delete(s_frame_box[depth]); /* 框体(含自绘回调)整体删除,容器保留 */
s_frame_box[depth] = NULL;
/* 清空行数据与几何缓存 */
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row_node[depth][i] = 0u;
}
f->built = 0u;
f->item_cnt = 0u;
f->sel = 0u;
f->x = 0;
f->y = 0;
f->w = 0;
f->h = 0;
}
/*
* 函数功能:刷新指定深度框的选中高亮
* 入口参数:depth - 深度 uint8_t
* sel - 新选中行 uint8_t
* 返回值:无
* 限定条件:ui_menu_view_frame_build(depth,…) 已完成
* 函数说明:只改选中数据 + 跟随滚动、重绘框体,不重建对象
*/
void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel) {
const menu_frame_t *f = &s_frame[depth];
/* 未建或越界直接返回 */
if (!f->built || depth >= MENU_PAGE_MAX || sel >= f->item_cnt) {
return;
}
view_apply_sel(depth, sel); /* 选中数据 + 跟随滚动 + 失效(自绘决定反色带) */
}
/*
* 函数功能:应用指定深度框的滚动偏移
* 入口参数:depth - 深度 uint8_t
* scroll - 滚动偏移(px) uint16_t
* 返回值:无
* 限定条件:该层框已建
* 函数说明:行对象重建后按此还原用户滚动位置
*/
void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll) {
/* 未建框直接返回 */
if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) {
return;
}
lv_obj_scroll_to_y(s_frame_box[depth], (int32_t)scroll, LV_ANIM_OFF);
}
/*
* 函数功能:读取指定深度框当前滚动偏移
* 入口参数:depth - 深度 uint8_t
* 返回值:滚动偏移(px) uint16_t 未建框返回 0
* 限定条件:无
* 函数说明:跟随滚动后由逻辑层读回,写入导航状态持久化
*/
uint16_t ui_menu_view_get_scroll(uint8_t depth) {
/* 未建框返回 0 */
if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) {
return 0u;
}
return (uint16_t)lv_obj_get_scroll_y(s_frame_box[depth]);
}
/*
* 函数功能:焦点置于持久容器
* 入口参数:无
* 返回值:无
* 限定条件:ui_menu_view_init() 已完成且容器已加入输入组
* 函数说明:焦点=唯一键入口(框深度由逻辑层 s_depth 决定);
* 进/退时由逻辑层调用(防御性:保证容器恒为焦点对象)
*/
void ui_menu_view_focus(void) {
if (s_cont != NULL) {
lv_group_focus_obj(s_cont);
}
}
/*
* 函数功能:取持久容器
* 入口参数:无
* 返回值:容器对象 lv_obj_t * 未初始化返回 NULL
* 限定条件:ui_menu_view_init() 已完成
* 函数说明:逻辑层在容器上绑定输入组与键事件(显示层不碰输入)
*/
lv_obj_t *ui_menu_view_container(void) {
return s_cont;
}
/*
* 根框装饰(浅 V):几何/绘制/culling 由共享 ui_deco 模块承载(无对象,容器自绘);
* 状态行 culling 区为本模块私有(文本约 104px 宽 + 余量)
*/
static const lv_area_t s_status_area = {MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y, 130, 33}; /* 状态行 culling 区 */
/*
* 函数功能:自绘根框装饰 + 状态行(容器 DRAW_MAIN;替代旧 4 个 LVGL 对象)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,容器已建
* 函数说明: 1. 浅 V:共享 ui_deco 模块(两腿 3px / 水平线 2px,绘制次序左腿、右腿、水平线)
* 2. 状态行:静态字符串、白色,与旧 label 同字体同位置
* 3. 重绘区不覆盖对应 culling 区则跳过(culling)
*/
static void view_deco_draw_cb(lv_event_t *e) {
lv_layer_t *layer = lv_event_get_layer(e);
/* "浅 V"装饰:共享 ui_deco 模块(几何/绘制次序/culling 均在其内) */
ui_deco_draw(layer, &layer->_clip_area);
/* 状态行:当前模式(静态文本) */
if (areas_intersect(&s_status_area, &layer->_clip_area)) {
lv_draw_label_dsc_t sd;
lv_draw_label_dsc_init(&sd);
sd.base.layer = layer;
sd.text = "当前模式 : 无模式";
sd.text_static = 1; /* Flash 字符串:不拷内存 */
sd.color = UI_COL_FG;
sd.ofs_x = 0; /* 区域原点即状态行左上 (17,20) */
sd.ofs_y = 0;
lv_draw_label(layer, &sd, &s_status_area);
}
}
/*
* 函数功能:初始化显示层(样式 + 持久容器 + 根框框体;装饰/状态行自绘)
* 入口参数:无
* 返回值:0 - 成功,其他 - 失败(对象创建失败)
* 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成)
* 函数说明:1. 持久容器:全屏覆盖、持久不删(焦点/键唯一入口;各层框均建其内)
* 2. 根框装饰(浅 V + 状态行)由容器自绘(无对象);根框框体持久
* 3. 行数据由逻辑层经 ui_menu_view_frame_build 注册,框体自绘行
*/
int ui_menu_view_init(void) {
/* 取活动屏幕:NULL = ui_port_init/sim_port_init 未完成、LVGL 未就绪 */
lv_obj_t *scr = lv_screen_active();
if (scr == NULL) {
return -1;
}
/* 样式一次性初始化(静态样式、常驻,仅此处建一次) */
view_styles_init();
/* 读屏幕实际分辨率(不硬编码,MCU/模拟器两端共用本代码) */
lv_display_t *disp = lv_display_get_default();
int32_t sw = lv_display_get_horizontal_resolution(disp);
int32_t sh = lv_display_get_vertical_resolution(disp);
/* 屏幕根容器:铺满屏幕;子对象全部绝对定位(级联框须同屏叠加,不用 flex 堆叠) */
lv_obj_t *menu = lv_obj_create(scr);
if (menu == NULL) {
return -1;
}
lv_obj_add_style(menu, &ui_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */
lv_obj_set_size(menu, sw, sh);
/* 持久容器:全屏覆盖、永不删(焦点/键唯一入口;各层框均建其内,防悬空) */
s_cont = lv_obj_create(menu);
if (s_cont == NULL) {
return -1;
}
lv_obj_add_style(s_cont, &ui_style_flat, 0); /* 透明、无边框 */
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级 flex 布局 */
lv_obj_set_pos(s_cont, 0, 0);
lv_obj_set_size(s_cont, sw, sh); /* 全屏覆盖 */
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */
/* 根框装饰(浅 V + 状态行):容器自绘(无对象),回调仅此处挂一次 */
lv_obj_add_event_cb(s_cont, view_deco_draw_cb, LV_EVENT_DRAW_MAIN, NULL);
/* 根框框体:持久;宽 = 内容自适应(主界面 node 0 的子项) */
uint8_t kids[MENU_ITEM_MAX];
uint8_t cnt = ui_menu_tab_children(0u, kids, MENU_ITEM_MAX); /* 主界面子项(7 个 L1 条目) */
int32_t w = view_frame_width(kids, cnt); /* 宽 = 最宽行内容 + 左右边框(当前 66) */
int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER; /* 高 = 行数×行高 + 上下边框(当前 107) */
s_frame_box[0] = lv_obj_create(s_cont);
if (s_frame_box[0] == NULL) {
return -1;
}
lv_obj_add_style(s_frame_box[0], &ui_style_flat, 0); /* 透明底 */
lv_obj_add_style(s_frame_box[0], &s_style_frame, 0); /* 1px 白边框 */
lv_obj_add_flag(s_frame_box[0], LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_frame_box[0], MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y); /* 左上 (0,44):装饰/状态行之下 */
lv_obj_set_size(s_frame_box[0], w, h);
view_box_layout(s_frame_box[0]); /* 自动滚动条(行自绘,无子对象) */
/* 自绘 + 虚拟内容高:根框持久,回调仅此处挂一次 */
lv_obj_add_event_cb(s_frame_box[0], view_frame_draw_cb, LV_EVENT_DRAW_MAIN, (void *)(uintptr_t)0u);
lv_obj_add_event_cb(s_frame_box[0], view_frame_self_size_cb, LV_EVENT_GET_SELF_SIZE, (void *)(uintptr_t)0u);
/* 写根框几何缓存:子框右贴靠其右缘、顶对齐其选中行,均以此为准 */
s_frame[0].built = 1u; /* 框体就绪;行数据由逻辑层 frame_build(0) 注册 */
s_frame[0].x = MENU_ROOT_BOX_X;
s_frame[0].y = MENU_ROOT_BOX_Y;
s_frame[0].w = w;
s_frame[0].h = h;
DBG_INFO("view init: root frame w=%d h=%d", (int)w, (int)h);
return 0;
}