Files
STM32F4-Base/Drivers/BSP/UI/ui_content_page.c
T
super_agent_bot 493fa6ba0e feat(ui/remo): 7003 总线新增 STATUS 身份查询与 GOTO 直达页(#69)
STATUS(命令 4)一帧回答"现在在哪个画面":kind(MENU/PAGE/STANDBY)
+ 菜单路径/选中 + 页 id,布局契约与字节序见 ui_net_sink.h。由 uiTask
轮询驱动,同上下文读静态导航状态:零画面副作用、免锁、不发日志。
新增只读接口 ui_menu_get_nav_state / ui_content_page_current_desc。

GOTO(命令 5)按页 id 查生成表直开内容页,以 VIEW 权限进入(与
"已过查看密码"同档,不代输任何密码)。编译期门控
DTU_BENCH || UI_REMO_ALLOW_GOTO:产品构建恒拒(status=4),模拟器
构建默认开启便于无板验证;执行类页(clear/reset)固件侧同样拒绝
(status=2,与主机 EXEC_PAGE_IDS 同一事实源);待机中拒绝(status=5),
由主机按键唤醒后再来。成功应答带回目标叶节点下标便于对账。

ui_sink 主机单测补导航侧链接假件 + 最小 lvgl 桩(仅本测试目录)。
产品构建 Keil 0 Error 0 Warning;ui_sink 单测 17/17。
2026-11-02 22:19:58 +08:00

616 lines
27 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 Content Page (内容页框架)
* 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架——全屏容器
* 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象;
* 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制;
* 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经
* content_page_def_t 提供,框架按**描述符指针**开页;"哪个叶子进哪一页"
* 的绑定在生成物 ui_page_registry.c(唯一真源 = ui_menu_tree.txt 的
* #page= 注记;架构见 内容页架构.md);
* 进入时切屏并获焦、ESC 返回菜单屏并还原焦点;
* 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-09
* 修改记录:
* 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21 实测几何)
* 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义)
* 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入
* ui_page_xxx 模块,本文件只留机制 + 描述符表;绘制/按键改
* 按描述符驱动(静态表/refresh + 4 钩子),行为零变化
* 2026-09-09 王建锋 密码页(用户密钥):绘制链支持每页 title_y/"浅 V"/页脚开关;
* 按键分发支持每页 ← 拦截(ESC 恒不可拦截);新 API
* ui_content_page_invalidate(表单页触发整页重绘)
* 2026-09-09 王建锋 权限模型:open(id, perm) 记录授予权限(页面经
* ui_content_page_is_editable 查询:edit_capable 且修改权限)
* 2026-09-10 王建锋 网口设置页(node 30):新增 CONTENT_PAGE_NET_CFG 入描述符表;
* 新 API ui_content_page_selected_row(页面钩子感知选中行)+
* 描述符 init_cb(开页后调一次,页面模块重置运行态)
* 2026-09-13 王建锋 cp_draw_cb 按区块拆为四个绘制子函数(cp_draw_deco/header/rows/footer),
* 主函数仅编排(行数取一次供行窗口/页脚共用),行为零变化
* 2026-10-15 王建锋 UI 设计令牌层:屏幕/骨架/行距/缓冲/1bpp 语义色字面量收进
* ui_tokens.h(纯替换,像素逐位一致)
* 2026-10-15 王建锋 UI 页面原型化:删 content_page_id_t 枚举与 s_defs[],open() 改收
* 描述符指针、s_open 变指针(NULL = 未打开);7 个页面头包含移交
* ui_page_registry.c;日志页身份改打描述符 id(ASCII 页名,比原
* id=%u 更可定位);绘制/按键/几何一律未动
* 2026-11-02 DTU 项目组 新增 ui_content_page_current_desc(#69 板端导航身份查询:
* 页 id 从"只有串口开/关沿可见"升级为可按帧查询)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_content_page.h"
#include "ui_standby.h"
#include "ui_pwd_def.h"
#include "ui_menu_view.h"
#include "ui_deco.h"
#include "ui_style.h"
#include "ui_tokens.h"
#include "lvgl.h"
#define DBG_TAG "[UI_CONTENT_PAGE]"
#include "dbg_log.h"
/* 私有宏定义区 */
/*
* 布局取值全部来自 ui_tokens.h(L0 设计令牌层,本文件不再自带同值字面量):
* 屏幕 UI_SCREEN_W/H、标题 UI_TITLE_Y、页脚 UI_PAGE_LINE_Y、文本条带
* UI_TEXT_BAND_H、行缓冲 UI_BUF_STRLEN、选中带内缩 UI_SEL_BAND_INSET_L/R、
* 页脚左起 UI_FOOTER_X、语义色 UI_COL_FG/BG/SEL_FG/SEL_BG。
* 几何实测来源仍是参考图 10/20/21(3x 放大图缩回 160x160 测量):标题 y=2
* (CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143;字形顶距
* (CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐
*/
/* 静态变量定义区 */
/*
* 当前打开的页:直接持页面描述符指针(NULL = 未打开)。
* 页身份由调用方给出(逻辑层经 ui_page_registry_for / _password 取描述符),
* 框架不再维护"页 ID 枚举 ↔ s_defs 下标 ↔ 菜单节点"三处必须手工对齐的映射
* ——那正是旧 menu_content_page_for() 手写 case 的错源(表序一变即错位)。
*/
static const content_page_def_t *s_open = NULL;
static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续) */
static uint8_t s_perm; /* 本次授予权限(ui_perm_t;开页时装载,关页重置) */
static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */
static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */
/* 容器样式:flat(透明 + 无边框/圆角/内边距)取自共享样式表 ui_style.c——
* 与 ui_menu_view / ui_standby 共用同一 const 对象(原先三处各定义一份逐字相同
* 的 9 项属性数组);常量样式零 LV_MEM 池分配的纪律见 ui_style.h */
/* 函数定义区 */
/*
* 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头)
* 入口参数:a - 区域 1 const lv_area_t *
* b - 区域 2 const lv_area_t *
* 返回值:true - 相交;false - 不相交
* 限定条件:无
* 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_menu_view.c 同实现)
*/
static bool cp_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);
}
/*
* 函数功能:取页面总行数(静态页 = 描述符 row_count,动态页 = count_cb)
* 入口参数:d - 页面描述符 const content_page_def_t *
* 返回值:总行数 uint16_t
* 限定条件:d 非空
* 函数说明:行数唯一入口;绘制(窗口/页脚)与按键(移选取模)均经此取
*/
static uint16_t cp_row_total(const content_page_def_t *d) {
if (d->count_cb != NULL) {
return d->count_cb();
}
return d->row_count;
}
/*
* 函数功能:失效当前打开的页面容器(触发整页重绘)
* 入口参数:无
* 返回值:无
* 限定条件:页面打开时(s_cont 非空)
* 函数说明:供页面模块(表单/操作页)自身状态变化后调用;无页打开时空操作
*/
void ui_content_page_invalidate(void) {
if (s_cont != NULL) {
lv_obj_invalidate(s_cont);
}
}
/*
* 函数功能:绘制居中单行文本(标题/副标题)
* 入口参数:layer - 绘制层 lv_layer_t *
* text - 文本(Flash) const char *
* y - 放置 y int32_t
* 返回值:无
* 限定条件:text 非空;重绘区已含该行区域(调用方 culling)
* 函数说明:x = (屏宽 - 文本实际步进)/2 居中;白色、静态字符串不拷内存
*/
static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y) {
lv_area_t area;
lv_draw_label_dsc_t ld;
area.x1 = 0;
area.x2 = UI_SCREEN_W - 1;
area.y1 = y;
area.y2 = y + UI_TEXT_BAND_H; /* 覆盖字体行高(13)+ 余量 */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = text;
ld.text_static = 1; /* Flash 字符串:不拷内存 */
ld.color = UI_COL_FG;
/* 本 LVGL 版 lv_text_get_width:length=0 表示"不测量"(返回 0);
* length≠0 时循环无 NUL 检查,传 -1 会越串尾死循环——必须传 strlen */
ld.ofs_x = (UI_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &area);
}
/*
* 函数功能:绘制"浅 V"装饰(共享 ui_deco 模块,与菜单根框同几何)
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* 返回值:无
* 限定条件:页面已打开
* 函数说明:每页可关(hide_deco;密码页跟随实机:无装饰)
*/
static void cp_draw_deco(lv_layer_t *layer, const content_page_def_t *d) {
if (d->hide_deco == 0u) {
ui_deco_draw(layer, &layer->_clip_area);
}
}
/*
* 函数功能:绘制标题/副标题(居中;整宽带 + culling)
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* 返回值:无
* 限定条件:页面已打开
* 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 UI_TITLE_Y);
* 副标题仅信息页(subtitle 非空);重绘区不相交则跳过区块
*/
static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) {
lv_area_t t;
/* 标题(居中;放置 y 每页可配,0 = 框架默认) */
{
int32_t ty = (d->title_y != 0) ? d->title_y : UI_TITLE_Y;
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = ty;
t.y2 = ty + UI_TEXT_BAND_H;
if (cp_areas_intersect(&t, &layer->_clip_area)) {
cp_draw_centered_text(layer, d->title, ty);
}
}
/* 副标题(仅信息页) */
if (d->subtitle != NULL) {
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = d->subtitle_y;
t.y2 = d->subtitle_y + UI_TEXT_BAND_H;
if (cp_areas_intersect(&t, &layer->_clip_area)) {
cp_draw_centered_text(layer, d->subtitle, d->subtitle_y);
}
}
}
/*
* 函数功能:绘制行数据(列表页窗口跟随选中行所在页,信息页全量;逐行 culling)
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* nrows - 总行数 int32_t(调用方取一次,与页脚同帧共用)
* 返回值:无
* 限定条件:页面已打开
* 函数说明: 1. 列表页窗口顶行 = (sel/每页行数)*每页行数,末页不足整页截断
* 2. 行条带(整宽)不在重绘区则跳过(culling)
* 3. 选中行:整行白带(x=2..156,高 band_h)+ 黑字,非选中白字
* 4. 行文本:静态 = Flash 表(不拷内存);动态 = refresh() 逐行取
* 入栈缓冲(同帧同步光栅化,安全)
*/
static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) {
int32_t first = 0;
int32_t cnt = nrows;
if (d->selectable != 0u && d->rows_per_page > 0u) {
first = (int32_t)(s_sel / d->rows_per_page) * d->rows_per_page; /* 窗口顶行 */
cnt = nrows - first;
if (cnt > (int32_t)d->rows_per_page) {
cnt = (int32_t)d->rows_per_page; /* 末页不足整页时截断 */
}
}
bool dynamic = (d->refresh != NULL);
char dyn_name[UI_BUF_STRLEN]; /* 动态行栈缓冲(同帧同步光栅化) */
char dyn_value[UI_BUF_STRLEN];
for (int32_t i = 0; i < cnt; i++) {
int32_t row_idx = first + i; /* 数据下标(跨页连续) */
int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */
bool is_sel = (d->selectable != 0u) && (row_idx == (int32_t)s_sel);
lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG;
lv_area_t strip;
/* 行条带(整宽)不在重绘区:跳过(culling) */
strip.x1 = 0;
strip.x2 = UI_SCREEN_W - 1;
strip.y1 = slot_y;
strip.y2 = slot_y + d->pitch - 1;
if (!cp_areas_intersect(&strip, &layer->_clip_area)) {
continue;
}
/* 选中行:整行白带(x=2..156,高 band_h) */
if (is_sel && d->band_h > 0) {
lv_draw_rect_dsc_t fd;
lv_area_t band;
band.x1 = UI_SEL_BAND_INSET_L;
band.x2 = UI_SCREEN_W - UI_SEL_BAND_INSET_R; /* x=156(与参考图实测一致) */
band.y1 = slot_y;
band.y2 = slot_y + d->band_h - 1;
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, &band);
}
/* 行文本:静态 = Flash 表(不拷内存);动态 = refresh 逐行取(栈缓冲) */
const char *name;
const char *value;
if (dynamic) {
d->refresh((uint16_t)row_idx, dyn_name, dyn_value);
name = dyn_name;
value = dyn_value;
}
else {
name = d->rows[row_idx].name;
value = d->rows[row_idx].value;
}
{
lv_draw_label_dsc_t ld;
/* ofs_x/ofs_y 相对 coords 原点(strip.y1=slot_y),勿再叠加 slot_y */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = name;
ld.text_static = (uint32_t)(!dynamic); /* 静态不拷;动态缓冲同帧内有效 */
ld.color = color; /* 选中黑 / 非选中白 */
ld.ofs_x = d->left_x;
ld.ofs_y = d->text_dy;
lv_draw_label(layer, &ld, &strip);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = value;
ld.text_static = (uint32_t)(!dynamic);
ld.color = color;
ld.ofs_x = d->right_x;
ld.ofs_y = d->text_dy;
lv_draw_label(layer, &ld, &strip);
}
}
}
/*
* 函数功能:绘制页脚"第 X 页,共 Y 页"
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* nrows - 总行数 int32_t(调用方取一次,与行窗口同帧同值)
* 返回值:无
* 限定条件:页面已打开
* 函数说明:列表页随选中行现算,信息页按参考图固定值;每页可关
* (hide_footer;密码页跟随实机)
*/
static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) {
char buf[UI_BUF_STRLEN]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */
uint8_t cur; /* 当前页码(1 基) */
uint8_t pages; /* 总页数 */
lv_area_t t; /* 页脚条带区域(整宽,放置于 UI_PAGE_LINE_Y) */
/* 页码计算,两种来源:
* - 列表页(selectable):随选中行现算——当前页 = 选中行/每页行数 + 1
* (选中行所在页);总页数 = ceil(nrows/每页行数),
* 写成经典向上取整 (n + p - 1) / p(nrows 恰为整页倍数时不多算一页);
* nrows 由调用方取一次、与行窗口共用,保证同帧页码与窗口一致
* - 信息/表单页(非 selectable):取参考图固定值(fixed_cur_page / fixed_total_pages) */
if (d->selectable != 0u) {
cur = (uint8_t)(s_sel / d->rows_per_page + 1u);
pages = (uint8_t)((nrows + (int32_t)d->rows_per_page - 1) / d->rows_per_page);
}
else {
cur = d->fixed_cur_page;
pages = d->fixed_total_pages;
}
/* 页脚条带:整宽,放置于 UI_PAGE_LINE_Y(143,字形顶距已计入放置 y,
* 墨迹与参考图对齐);高 15 = 字体行高 13 + 余量,整行文字一次覆盖 */
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = UI_PAGE_LINE_Y;
t.y2 = UI_PAGE_LINE_Y + UI_TEXT_BAND_H;
/* 双重门控:① 本页开启页脚(hide_footer;密码页跟随实机:无页码)
* ② 页脚条带与重绘区相交(culling)——两者皆成立才格式化并绘制 */
if (d->hide_footer == 0u && cp_areas_intersect(&t, &layer->_clip_area)) {
/* lv_snprintf 而非 snprintf:MSVC 对 printf 族做格式串扫描,中文字面量会误报 C4819 */
lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)pages);
lv_draw_label_dsc_t ld;
/* ofs 相对 coords 原点(t.x1=0,t.y1=UI_PAGE_LINE_Y):
* 水平 6 = 左对齐、左留白 6px(与实机一致);垂直 0(顶距已计入放置 y) */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = buf; /* 栈缓冲:本页码每次现算,同帧内有效(同步光栅化) */
ld.color = UI_COL_FG; /* 页码恒白字(不像选中行那样反色) */
ld.ofs_x = UI_FOOTER_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &t);
}
}
/*
* 函数功能:页面自绘(编排:浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码 + 页面钩子)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,页面已打开
* 函数说明:行数取一次(cp_row_total)供行窗口与页脚页码共用(同帧同值);
* 各区块由子函数绘制(deco/header/rows/footer),均按 culling 区
* 跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容,在内置区块之后)
*/
static void cp_draw_cb(lv_event_t *e) {
const content_page_def_t *d = s_open;
lv_layer_t *layer = lv_event_get_layer(e);
int32_t nrows = (int32_t)cp_row_total(d); /* 行数取一次:供行窗口与页脚页码共用 */
cp_draw_deco(layer, d);
cp_draw_header(layer, d);
cp_draw_rows(layer, d, nrows);
cp_draw_footer(layer, d, nrows);
/* 页面钩子:自定义内容(表单/操作页;在内置区块之后绘制) */
if (d->draw_cb != NULL) {
d->draw_cb(layer, &layer->_clip_area);
}
}
/*
* 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回 / 页面钩子)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:容器已加入默认输入组并获焦
* 函数说明: 1. ESC:恒关页返回菜单(框架保证,页面钩子不可拦截)
* 2. ←:页面置拦截标志(key_intercept)时先交 key_cb(true = 页已处理不关页),
* 否则关页返回(密码页光标 ←/→ 即由此处理)
* 3. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置)
* 4. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘;
* 信息页不响应;动态行数可能缩小,选中行越界时重置回首行
*/
static void cp_key_cb(lv_event_t *e) {
const content_page_def_t *d = s_open;
lv_key_t key = (lv_key_t)lv_event_get_key(e);
ui_standby_notify_activity(); /* 用户活动:重置待机空闲计时 */
/* ESC:关闭页面返回菜单屏(焦点/导航状态还原;恒不可拦截) */
if (key == LV_KEY_ESC) {
ui_content_page_close();
return;
}
/* ←:页面置拦截标志且钩子已处理则不关页(密码页光标 ←/→);否则照旧返回 */
if (key == LV_KEY_LEFT) {
if (d->key_intercept != 0u && d->key_cb != NULL && d->key_cb(key)) {
return;
}
ui_content_page_close();
return;
}
/* 其余键:页面钩子先调:true = 页已处理,框架跳过内置语义 */
if (d->key_cb != NULL && d->key_cb(key)) {
return;
}
switch (key) {
case LV_KEY_UP: {
uint16_t total = cp_row_total(d);
if (d->selectable != 0u && total > 0u) {
if (s_sel >= total) {
s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */
}
/* 上移、首尾环绕:先加 total 再减 1 保证无符号下不为负 */
s_sel = (uint16_t)((s_sel + total - 1u) % total);
lv_obj_invalidate(s_cont); /* 重绘(选中带/窗口/页码可能变化) */
}
break;
}
case LV_KEY_DOWN: {
uint16_t total = cp_row_total(d);
if (d->selectable != 0u && total > 0u) {
if (s_sel >= total) {
s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */
}
s_sel = (uint16_t)((s_sel + 1u) % total); /* 下移、越尾绕回首行 */
lv_obj_invalidate(s_cont);
}
break;
}
default:
break; /* 其余键值忽略 */
}
}
/*
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
* 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password)
* const content_page_def_t *
* perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY)
* 返回值:true - 成功;false - 失败(desc 为 NULL / 已有页面打开 / 对象创建失败)
* 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在)
* 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象)
* 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管
* 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原
* 4. 记录授予权限(页面经 ui_content_page_is_editable 查询)
*/
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm) {
if (s_open != NULL) {
return false; /* 已有页面打开(防御:页面按需建毁,同一时刻至多一页) */
}
if (desc == NULL) {
DBG_ERROR("page open: NULL descriptor (node has no bound page)");
return false; /* 调用方把未绑定内容页的叶子当页面开了:拒开并留痕,不崩溃 */
}
s_perm = perm; /* 本次授予权限(页面可编辑性 = edit_capable 且修改权限) */
s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */
if (s_scr == NULL) {
return false; /* LVGL 池耗尽 */
}
s_cont = lv_obj_create(s_scr);
if (s_cont == NULL) {
lv_obj_delete(s_scr); /* 回滚:屏已建、容器失败 */
s_scr = NULL;
return false;
}
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级布局 */
lv_obj_set_size(s_cont, UI_SCREEN_W, UI_SCREEN_H); /* 全屏覆盖 */
lv_obj_add_style(s_cont, &ui_style_flat, 0); /* 透明、无边框:剥离主题 card */
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */
lv_obj_add_event_cb(s_cont, cp_key_cb, LV_EVENT_KEY, NULL);
lv_obj_add_event_cb(s_cont, cp_draw_cb, LV_EVENT_DRAW_MAIN, NULL);
s_open = desc;
s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */
if (desc->init_cb != NULL) {
desc->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */
}
/* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */
lv_group_t *g = lv_group_get_default();
if (g != NULL) {
lv_group_add_obj(g, s_cont);
lv_group_focus_obj(s_cont);
}
lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */
DBG_INFO("page open: id=%s rows=%u", desc->id, (unsigned)cp_row_total(desc));
return true;
}
/*
* 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面)
* 入口参数:无
* 返回值:无
* 限定条件:ui_content_page_is_open() 为 true
* 函数说明:1. 先移焦到菜单容器再删屏——防输入组焦点悬空(删除自动移出组)
* 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落
*/
void ui_content_page_close(void) {
if (s_open == NULL || s_cont == NULL) {
return; /* 未打开(防御) */
}
lv_obj_t *menu_cont = ui_menu_view_container();
if (menu_cont != NULL) {
lv_group_focus_obj(menu_cont); /* 焦点先回菜单容器 */
lv_screen_load(lv_obj_get_screen(menu_cont)); /* 切回菜单屏(对象未动,原样还原) */
}
DBG_INFO("page close: id=%s", s_open->id);
lv_obj_delete(s_cont); /* 先删容器 */
lv_obj_delete(s_scr); /* 再删屏 */
s_scr = NULL;
s_cont = NULL;
s_open = NULL;
s_sel = 0u;
s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */
}
/*
* 函数功能:查询是否有内容页打开
* 入口参数:无
* 返回值:true - 有页面打开;false - 无
* 限定条件:无
* 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页
*/
bool ui_content_page_is_open(void) {
return (s_open != NULL);
}
/*
* 函数功能:查询当前打开的页面是否可编辑
* 入口参数:无
* 返回值:true - 页面支持编辑模式(edit_capable)且被授予修改权限;false - 其余
* 限定条件:无
* 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予
*/
bool ui_content_page_is_editable(void) {
if (s_open == NULL) {
return false; /* 无页打开或无描述符(防御) */
}
return (s_open->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY);
}
/*
* 函数功能:查询当前列表页的选中行(全局下标,跨页连续)
* 入口参数:无
* 返回值:选中行下标;无页打开/非列表页 = 0 uint16_t
* 限定条件:无
* 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置
*/
uint16_t ui_content_page_selected_row(void) {
if (s_open == NULL) {
return 0u;
}
if (s_open->selectable == 0u) {
return 0u; /* 信息页/表单页无行选中概念 */
}
return s_sel;
}
/*
* 函数功能:查询当前打开页面的描述符(契约见 ui_content_page.h,Issue #69)
* 入口参数:无
* 返回值:当前页面描述符指针(Flash 常量,只读);无页打开 = NULL
* 限定条件:同上下文(uiTask)读取才有一致性语义;本函数零副作用、不打日志
* 函数说明:与 ui_content_page_is_open 的区别只在于顺带交出身份(desc->id),
* 推流 STATUS 用它把"开着哪一页"随帧带回,不再只有串口开/关沿可见
*/
const content_page_def_t *ui_content_page_current_desc(void) {
return s_open;
}