Files
STM32F4-Base/Drivers/BSP/UI/ui_content_page.c
T

619 lines
26 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 接入描述符表 s_defs[](架构见 内容页架构.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),
* 主函数仅编排(行数取一次供行窗口/页脚共用),行为零变化
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_content_page.h"
#include "ui_page_device_info.h"
#include "ui_page_ac.h"
#include "ui_page_dc.h"
#include "ui_page_password.h"
#include "ui_page_net_cfg.h"
#include "ui_pwd_def.h"
#include "ui_menu_view.h"
#include "ui_deco.h"
#include "lvgl.h"
#define DBG_TAG "[UI_CONTENT_PAGE]"
#include "dbg_log.h"
/* 私有宏定义区 */
/*
* 布局常量(设备像素;参考图 10/20/21 实测,3x 放大图缩回 160x160 测量):
* 标题 y=2(CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143;
* 字形顶距(CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐
*/
#define CP_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */
#define CP_SCREEN_H 160 /* 屏幕高(px) */
#define CP_TITLE_Y 2 /* 标题放置 y(居中;墨迹自 y=3 起) */
#define CP_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y(x=6 左对齐) */
#define CP_ROW_BUF_LEN 32 /* 动态行栈缓冲长度(同帧同步光栅化;超出行带即被裁切) */
/* 静态变量定义区 */
/*
* 描述符表:页 ID → 页面模块描述符(每页一行;新增页 = 枚举值 + 此处一行,
* 框架机制不变,见 内容页架构.md §七)
*/
static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = {
[CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc,
[CONTENT_PAGE_AC] = &ui_page_ac_desc,
[CONTENT_PAGE_DC] = &ui_page_dc_desc,
[CONTENT_PAGE_PASSWORD] = &ui_page_password_desc,
[CONTENT_PAGE_NET_CFG] = &ui_page_net_cfg_desc,
};
static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */
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:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 card 样式(同 ui_menu_view.c) */
/* 用常量样式(LV_STYLE_CONST_INIT):属性存静态数组,零 LV_MEM 池分配;
* 动态样式首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88B/个) */
static const lv_style_const_prop_t cp_flat_props[] = {
{ .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } },
{ .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } },
{ .prop = LV_STYLE_RADIUS, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } },
LV_STYLE_CONST_PROPS_END
};
LV_STYLE_CONST_INIT(cp_style_flat, cp_flat_props);
/* 函数定义区 */
/*
* 函数功能:判断两区域是否相交(等价 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 = CP_SCREEN_W - 1;
area.y1 = y;
area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = text;
ld.text_static = 1; /* Flash 字符串:不拷内存 */
ld.color = lv_color_white();
/* 本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen */
ld.ofs_x = (CP_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 = 框架默认 CP_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 : CP_TITLE_Y;
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.y1 = ty;
t.y2 = ty + 15;
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 = CP_SCREEN_W - 1;
t.y1 = d->subtitle_y;
t.y2 = d->subtitle_y + 15;
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[CP_ROW_BUF_LEN]; /* 动态行栈缓冲(同帧同步光栅化) */
char dyn_value[CP_ROW_BUF_LEN];
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 ? lv_color_black() : lv_color_white();
lv_area_t strip;
/* 行条带(整宽)不在重绘区:跳过(culling) */
strip.x1 = 0;
strip.x2 = CP_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 = 2;
band.x2 = CP_SCREEN_W - 3; /* 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 = lv_color_white();
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[32]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */
uint8_t cur; /* 当前页码(1 基) */
uint8_t pages; /* 总页数 */
lv_area_t t; /* 页脚条带区域(整宽,放置于 CP_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;
}
/* 页脚条带:整宽,放置于 CP_PAGE_LINE_Y(143,字形顶距已计入放置 y,
* 墨迹与参考图对齐);高 15 = 字体行高 13 + 余量,整行文字一次覆盖 */
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.y1 = CP_PAGE_LINE_Y;
t.y2 = CP_PAGE_LINE_Y + 15;
/* 双重门控:① 本页开启页脚(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=CP_PAGE_LINE_Y):
* 水平 6 = 左对齐、左留白 6px(与实机一致);垂直 0(顶距已计入放置 y) */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = buf; /* 栈缓冲:本页码每次现算,同帧内有效(同步光栅化) */
ld.color = lv_color_white(); /* 页码恒白字(不像选中行那样反色) */
ld.ofs_x = 6;
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_defs[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_defs[s_open];
lv_key_t key = (lv_key_t)lv_event_get_key(e);
/* 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; /* 其余键值忽略 */
}
}
/*
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
* 入口参数:id - 内容页 ID content_page_id_t
* perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY)
* 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败)
* 限定条件: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(content_page_id_t id, uint8_t perm) {
if (id >= CONTENT_PAGE_COUNT || s_open != CONTENT_PAGE_COUNT) {
return false; /* ID 无效或已有页面打开(防御) */
}
const content_page_def_t *d = s_defs[id];
if (d == NULL) {
DBG_ERROR("page open: id=%u no descriptor (enum/table drift)", (unsigned)id);
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, CP_SCREEN_W, CP_SCREEN_H); /* 全屏覆盖 */
lv_obj_add_style(s_cont, &cp_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 = id;
s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */
if (d->init_cb != NULL) {
d->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=%u rows=%u", (unsigned)id, (unsigned)cp_row_total(d));
return true;
}
/*
* 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面)
* 入口参数:无
* 返回值:无
* 限定条件:ui_content_page_is_open() 为 true
* 函数说明:1. 先移焦到菜单容器再删屏——防输入组焦点悬空(删除自动移出组)
* 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落
*/
void ui_content_page_close(void) {
if (s_open == CONTENT_PAGE_COUNT || 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=%u", (unsigned)s_open);
lv_obj_delete(s_cont); /* 先删容器 */
lv_obj_delete(s_scr); /* 再删屏 */
s_scr = NULL;
s_cont = NULL;
s_open = CONTENT_PAGE_COUNT;
s_sel = 0u;
s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */
}
/*
* 函数功能:查询是否有内容页打开
* 入口参数:无
* 返回值:true - 有页面打开;false - 无
* 限定条件:无
* 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页
*/
bool ui_content_page_is_open(void) {
return (s_open != CONTENT_PAGE_COUNT);
}
/*
* 函数功能:查询当前打开的页面是否可编辑
* 入口参数:无
* 返回值:true - 页面支持编辑模式(edit_capable)且被授予修改权限;false - 其余
* 限定条件:无
* 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予
*/
bool ui_content_page_is_editable(void) {
if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) {
return false; /* 无页打开或无描述符(防御) */
}
return (s_defs[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 == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) {
return 0u;
}
if (s_defs[s_open]->selectable == 0u) {
return 0u; /* 信息页/表单页无行选中概念 */
}
return s_sel;
}