重构显示逻辑为 MVP 架构,进行显示模块的解耦
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"clangd.arguments": [
|
||||
"--compile-commands-dir=build"
|
||||
],
|
||||
"editor.multiCursorModifier": "alt",
|
||||
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:conda",
|
||||
"python-envs.pythonProjects": []
|
||||
|
||||
@@ -26,20 +26,28 @@ endif()
|
||||
# 主可执行程序 DTU-HMI 及其源码列表
|
||||
add_executable(DTU-HMI
|
||||
src/main.c
|
||||
src/common/utf8.c
|
||||
src/thread_utils.c
|
||||
src/remoteDisplay.c
|
||||
src/Drv/menu.c
|
||||
src/Drv/menu/app/menu.c
|
||||
src/Drv/menu/model/display.c
|
||||
src/Drv/menu/model/menu_model.c
|
||||
src/Drv/menu/view/menu_view.c
|
||||
src/Drv/menu/presenter/menu_presenter.c
|
||||
src/Drv/menu/model/menu_tree_builder.c
|
||||
src/Drv/menu/view/menu_layout.c
|
||||
src/Drv/menu/presenter/menu_navigator.c
|
||||
src/Drv/menu/view/menu_renderer_lcd.c
|
||||
src/Drv/lcd/lcd.c
|
||||
src/Drv/lcd/lcd_draw.c
|
||||
src/Drv/lcd/lcd_text.c
|
||||
src/Drv/key.c
|
||||
src/Drv/lcd/ascii.c
|
||||
src/Drv/display.c
|
||||
src/TCP/tcp.c
|
||||
)
|
||||
|
||||
# 可执行程序头文件搜索路径
|
||||
target_include_directories(DTU-HMI PRIVATE include src src/TCP)
|
||||
target_include_directories(DTU-HMI PRIVATE include src src/TCP src/Drv/lcd src/common)
|
||||
|
||||
# 按平台链接系统库:
|
||||
# - Windows 需要 Winsock2(ws2_32)
|
||||
|
||||
1
Testing/Temporary/CTestCostData.txt
Normal file
1
Testing/Temporary/CTestCostData.txt
Normal file
@@ -0,0 +1 @@
|
||||
---
|
||||
188
docs/系统架构设计文档.md
188
docs/系统架构设计文档.md
@@ -24,26 +24,40 @@
|
||||
|
||||
## 4. 系统分层架构
|
||||
|
||||
```text
|
||||
+----------------------+
|
||||
| AppLayer |
|
||||
| main.c |
|
||||
+----------+-----------+
|
||||
|
|
||||
+--------------+--------------+
|
||||
| |
|
||||
v v
|
||||
+---------+----------+ +---------+------------------+
|
||||
| MenuLayer | | RemoteDisplayLayer |
|
||||
| menu.c | | remoteDisplay.c |
|
||||
+----+-----------+---+ +-----+-----------+----------+
|
||||
| | | |
|
||||
v v v v
|
||||
+------+-----+ +--+---------+ +---+----+ +---+------------------+
|
||||
|DisplayLayer| |InputLayer | |Input | |PlatformLayer |
|
||||
|lcd/Ascii/ | |key.c | |key.c | |tcp.c + thread_utils.c |
|
||||
|display.c | +------------+ +-------+ +------------------------+
|
||||
+------------+
|
||||
```mermaid
|
||||
block-beta
|
||||
columns 1
|
||||
a["菜单"]
|
||||
block:textdraw
|
||||
columns 2
|
||||
a1["文字显示"] a2["图像显示"]
|
||||
end
|
||||
block:draw
|
||||
columns 2
|
||||
block:text_group
|
||||
columns 2
|
||||
block:left_text
|
||||
columns 1
|
||||
b["Lcd_ShowStr"]
|
||||
block:showstr
|
||||
columns 2
|
||||
s1["Lcd_Pub_Ascii"]
|
||||
s2["Lcd_Pub_UTF8"]
|
||||
end
|
||||
end
|
||||
b1["utf8_next"]
|
||||
end
|
||||
block:draw_group
|
||||
columns 4
|
||||
c1["Lcd_LineH"]
|
||||
c2["Lcd_LineV"]
|
||||
c3["Lcd_Invert"]
|
||||
c4["Lcd_FillRect"]
|
||||
end
|
||||
end
|
||||
block:lcd
|
||||
lcda["Lcd_Init"] lcdb["Lcd_SetPixel"] lcdc["Lcd_GetPixel"]
|
||||
end
|
||||
```
|
||||
|
||||
### 4.1 应用层
|
||||
@@ -56,15 +70,16 @@
|
||||
|
||||
### 4.2 菜单业务层
|
||||
|
||||
- 文件:`src/Drv/menu.c`、`src/Drv/menu.h`
|
||||
- 文件:`src/Drv/menu/`、`src/Drv/menu/app/menu.h`
|
||||
- 职责:
|
||||
- 采用 MVP 分层组织菜单模块(`Model / Presenter / View`)
|
||||
- 基于静态菜单模型构建运行时菜单树
|
||||
- 处理按键驱动的菜单状态迁移
|
||||
- 处理按键驱动的菜单状态迁移与路径重建
|
||||
- 执行菜单显示坐标计算与多级菜单渲染调度
|
||||
|
||||
### 4.3 显示层
|
||||
|
||||
- 文件:`src/Drv/lcd/lcd.c`、`src/Drv/lcd/lcd_draw.c`、`src/Drv/lcd/lcd_text.c`、`src/Drv/lcd/text_codec.c`、`src/Drv/lcd/ascii.c`、`src/Drv/display.c`
|
||||
- 文件:`src/Drv/lcd/lcd.c`、`src/Drv/lcd/lcd_draw.c`、`src/Drv/lcd/lcd_text.c`、`src/Drv/lcd/text_codec.c`、`src/Drv/lcd/ascii.c`、`src/Drv/menu/model/display.c`
|
||||
- 职责:
|
||||
- 管理 LCD 显存 `g_tCVsr` 与像素绘制
|
||||
- 提供 ASCII/UTF-8 字符显示能力
|
||||
@@ -96,9 +111,9 @@
|
||||
|
||||
### 5.1 菜单模型与菜单树
|
||||
|
||||
- 静态菜单模型:`tagMenuModel`(定义于 `display.h`,数据在 `display.c`)
|
||||
- 运行时菜单项:`tagMenuItem`(定义于 `menu.c` 内)
|
||||
- 全局控制:`g_tMenuCtrl`、`g_tDspCtrl`
|
||||
- 静态菜单模型:`tagMenuModel`(定义于 `src/Drv/menu/model/display.h`,数据在 `src/Drv/menu/model/display.c`)
|
||||
- 运行时菜单项:`tagMenuItem`(定义于 `src/Drv/menu/common/menu_item_types.h`,实例由 `src/Drv/menu/app/menu.c` 管理)
|
||||
- 运行时控制:`tagMenuCtrl`、`tagDspCtrl`(定义于 `src/Drv/menu/common/menu_state_types.h`,实例为 `app/menu.c` 内部私有)
|
||||
|
||||
关键关系:
|
||||
|
||||
@@ -128,7 +143,7 @@
|
||||
[系统初始化]
|
||||
|
|
||||
v
|
||||
[Menu_Route]
|
||||
[MenuApp_PollInput]
|
||||
|
|
||||
v
|
||||
[Sleep 20ms]
|
||||
@@ -137,9 +152,9 @@
|
||||
[计数器累加]
|
||||
|
|
||||
v
|
||||
[是否到刷新周期?] --否--> [Menu_Route]
|
||||
[是否到刷新周期?] --否--> [MenuApp_PollInput]
|
||||
|
|
||||
+--是--> [Menu_Show_Proc] --> [Menu_Route]
|
||||
+--是--> [MenuApp_Render] --> [MenuApp_PollInput]
|
||||
```
|
||||
|
||||
### 6.2 菜单交互流程
|
||||
@@ -150,7 +165,85 @@
|
||||
- 左/ESC:回退上级或退回主层
|
||||
- 右/确认:进入子级或执行叶子回调
|
||||
- 渲染:
|
||||
- `Menu_Show_Proc` 根据路径增量刷新或全量刷新
|
||||
- `MenuApp_Render` 调用 Presenter/View,根据路径进行增量刷新或全量刷新
|
||||
|
||||
### 6.4 当前 Menu 详细设计(MVP)
|
||||
|
||||
#### 6.4.1 模块拆分
|
||||
|
||||
- `src/Drv/menu/app/menu.c`
|
||||
- 菜单应用 Facade(`MenuApp_*` 对外接口)
|
||||
- 持有运行时私有状态(`s_menuCtrl`、`s_dspCtrl`、`s_menuItems`)
|
||||
- `src/Drv/menu/presenter/menu_presenter.c`
|
||||
- 控制调度中枢:处理输入事件、调用导航器、触发重建路径与刷新
|
||||
- `src/Drv/menu/model/menu_model.c`
|
||||
- 模型初始化:树构建、菜单名修饰、初始状态建立
|
||||
- `src/Drv/menu/view/menu_view.c`
|
||||
- 纯渲染:顶部栏、多级菜单框、高亮反显、全量/增量刷新策略
|
||||
- `src/Drv/menu/presenter/menu_navigator.c`
|
||||
- 纯导航状态机:按键到 `MenuNavResult`(是否刷新、是否跳过渲染)
|
||||
- `src/Drv/menu/view/menu_layout.c`
|
||||
- 菜单布局计算:宽度统计、层级矩形定位、越界回退策略
|
||||
- `src/Drv/menu/model/display.c`
|
||||
- 静态菜单模型表 `g_tMenuModelTab`(业务菜单定义)
|
||||
|
||||
#### 6.4.2 运行时调用时序
|
||||
|
||||
```text
|
||||
MenuApp_Init
|
||||
-> MenuPresenter_Setup
|
||||
-> MenuPresenter_InitModel
|
||||
-> MenuModel_Bootstrap
|
||||
-> MenuTree_0LevelNumCal
|
||||
-> MenuTree_MainCreate
|
||||
-> MenuView_Layout
|
||||
-> MenuLayout_PositionCal
|
||||
|
||||
主循环:
|
||||
MenuApp_PollInput
|
||||
-> Key_Read
|
||||
-> MenuApp_HandleInput(key)
|
||||
-> MenuPresenter_HandleInput
|
||||
-> MenuNavigator_ProcessKey
|
||||
-> (needRefresh) MenuNavigator_RebuildRoute
|
||||
-> MenuPresenter_Refresh
|
||||
-> MenuView_RenderByState
|
||||
```
|
||||
|
||||
#### 6.4.3 关键状态数据
|
||||
|
||||
- `tagMenuItem`
|
||||
- 菜单节点实体,包含树关系(`ptHigher/ptLower/ptBefore/ptBehind`)与显示矩形
|
||||
- `tagMenuCtrl`
|
||||
- 导航核心状态(`ptCurrent`、`ptRoute[4]`、`ptCurBak`、`pt0Level` 等)
|
||||
- `tagDspCtrl`
|
||||
- 显示控制状态(当前主要使用首帧标记 `bFirst`)
|
||||
- `MenuNavState / MenuNavResult`
|
||||
- Presenter 与 Navigator 之间的状态快照与处理结果
|
||||
|
||||
#### 6.4.4 对外数据接口(当前基线)
|
||||
|
||||
- `void MenuApp_Init(void)`
|
||||
- 初始化菜单应用(Presenter/Model/View)
|
||||
- `void MenuApp_HandleInput(uint8_t keyVal)`
|
||||
- 注入输入事件并驱动导航状态变更
|
||||
- `void MenuApp_PollInput(void)`
|
||||
- 从 `Key_Read()` 读取按键并转发到 `MenuApp_HandleInput`
|
||||
- `void MenuApp_Render(void)`
|
||||
- 主动触发一次渲染(用于周期刷新或外部强制重绘)
|
||||
- `const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count)`
|
||||
- 只读导出当前菜单项数组(调试/测试使用)
|
||||
|
||||
#### 6.4.5 内部接口边界(模块间)
|
||||
|
||||
- Model -> View
|
||||
- 仅通过控制状态与布局结果共享,不直接调用绘制原语
|
||||
- Presenter -> Model
|
||||
- 仅在初始化阶段触发 `MenuModel_Bootstrap`
|
||||
- Presenter -> View
|
||||
- 通过 `MenuPresenter_Refresh` 驱动渲染,不暴露底层 LCD 细节
|
||||
- Presenter -> Navigator
|
||||
- 通过 `MenuNavState` 快照交互,避免导航器直接操作外部全局变量
|
||||
|
||||
### 6.3 远程显示协议流程
|
||||
|
||||
@@ -220,10 +313,12 @@ ctest --test-dir build -C Debug --output-on-failure
|
||||
## 10. 模块依赖关系(代码级)
|
||||
|
||||
- `main.c` 依赖:`menu`、`key`、`remoteDisplay`、`tcp`、`thread_utils`
|
||||
- `menu.c` 依赖:`lcd`、`display`、`key`
|
||||
- `src/Drv/menu/app/menu.c` 依赖:`presenter/menu_presenter`、`model/display`、`key`、`lcd`
|
||||
- `src/Drv/menu/presenter/menu_presenter.c` 依赖:`model/menu_model`、`view/menu_view`、`presenter/menu_navigator`
|
||||
- `src/Drv/menu/view/menu_view.c` 依赖:`model/menu_layout`、`view/menu_render_port`、`lcd`
|
||||
- `remoteDisplay.c` 依赖:`lcd`、`key`、`tcp`、`thread_utils`
|
||||
- `lcd.c` 依赖:`ascii`
|
||||
- `display.c` 提供:静态菜单表(被 `menu.c` 使用)
|
||||
- `src/Drv/menu/model/display.c` 提供:静态菜单表(被 menu 模块使用)
|
||||
|
||||
## 11. 已知风险与改进建议
|
||||
|
||||
@@ -242,3 +337,32 @@ ctest --test-dir build -C Debug --output-on-failure
|
||||
- 每次新增模块或调整主流程时同步更新本文档
|
||||
- 测试策略更新需同步维护“第 9 章 测试架构”
|
||||
|
||||
## 13. 菜单重构故障复盘(2026-03)
|
||||
|
||||
### 13.1 现象
|
||||
|
||||
- 菜单分层重构后,程序在启动阶段(当前入口为 `MenuApp_Init`)出现访问冲突,表现为“运行即崩溃”。
|
||||
|
||||
### 13.2 根因
|
||||
|
||||
- 根因位于菜单树构建模块 `MenuTree_MainCreate`。
|
||||
- 在“层级回退(`byCurClass > byNextClass`)”分支中,缺少对中间层级链表的及时收口(首尾成环)处理。
|
||||
- 后续 `Menu_PositionCal` 在遍历同级链表时访问到异常节点,导致崩溃。
|
||||
|
||||
### 13.3 修复措施
|
||||
|
||||
- 恢复并对齐原稳定逻辑:
|
||||
- 当层级回退时,立即对回退区间层级执行首尾成环收口。
|
||||
- 每轮处理后更新 `ptCurrent = ptNextNode`,保证状态推进一致。
|
||||
- 循环结束后按当前实际层级执行最终收口。
|
||||
|
||||
### 13.4 回归防线
|
||||
|
||||
- 新增启动路径集成回归用例:`tests/test_p2_menu_runtime_startup.c`。
|
||||
- 覆盖最易回归的启动路径:
|
||||
- `MenuApp_Init()`
|
||||
- `Key_Init()`
|
||||
- 首次 `MenuApp_PollInput()`
|
||||
- `MenuApp_Render()`
|
||||
- 该用例用于防止“菜单树可编译但启动崩溃”的问题再次进入主干。
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* - 坐标区间语义在不同函数中不同,使用时需特别注意(见各函数注释)。
|
||||
* ------------------------------------------------------------------------- */
|
||||
#include "lcd_draw.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef DEBUG
|
||||
#include <stdio.h>
|
||||
@@ -170,6 +171,81 @@ int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wW
|
||||
return LCD_OK;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数名: Lcd_Line
|
||||
* 功能:
|
||||
* 绘制任意方向线段(可设置线宽)。
|
||||
* 说明:
|
||||
* - 水平/垂直线走 Lcd_LineH/Lcd_LineV 快速路径。
|
||||
* - 斜线采用 Bresenham 风格误差迭代,并做对称双端绘制。
|
||||
* ------------------------------------------------------------------------- */
|
||||
int8_t Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color)
|
||||
{
|
||||
uint16_t wDX, wDY, wSignY, wSignX, wDecision;
|
||||
uint16_t wCurx, wCury, wNextx, wNexty, wPY, wPX;
|
||||
|
||||
if (Lcd_ColorCheck(color) != LCD_OK) return LCD_ERR_INVALID_COLOR;
|
||||
if (wYstart == wYend) return Lcd_LineH(wXstart, wXend, wYstart, wWidth, color);
|
||||
if (wXstart == wXend) return Lcd_LineV(wYstart, wYend, wXstart, wWidth, color);
|
||||
|
||||
wDX = (uint16_t)abs((int)wXstart - (int)wXend);
|
||||
wDY = (uint16_t)abs((int)wYstart - (int)wYend);
|
||||
if (((wDX >= wDY && (wXstart > wXend)) || ((wDY > wDX) && (wYstart > wYend)))) {
|
||||
M_GuiSwap(wXend, wXstart);
|
||||
M_GuiSwap(wYend, wYstart);
|
||||
}
|
||||
wSignY = (wYend - wYstart) / wDY;
|
||||
wSignX = (wXend - wXstart) / wDX;
|
||||
|
||||
if (wDX >= wDY) {
|
||||
wCurx = wXstart;
|
||||
wCury = wYstart;
|
||||
wNextx = wXend;
|
||||
wNexty = wYend;
|
||||
wDecision = (wDX >> 1);
|
||||
for (; wCurx <= wNextx;) {
|
||||
if (wDecision >= wDX) {
|
||||
wDecision -= wDX;
|
||||
wCury += wSignY;
|
||||
wNexty -= wSignY;
|
||||
}
|
||||
for (wPY = wCury - wWidth / 2; wPY <= wCury + wWidth / 2; wPY++) {
|
||||
Lcd_SetPixel(wCurx, wPY, color);
|
||||
}
|
||||
for (wPY = wNexty - wWidth / 2; wPY <= wNexty + wWidth / 2; wPY++) {
|
||||
Lcd_SetPixel(wNextx, wPY, color);
|
||||
}
|
||||
wCurx++;
|
||||
wNextx--;
|
||||
wDecision += wDY;
|
||||
}
|
||||
} else {
|
||||
wCurx = wXstart;
|
||||
wCury = wYstart;
|
||||
wNextx = wXend;
|
||||
wNexty = wYend;
|
||||
wDecision = (wDY >> 1);
|
||||
for (; wCury <= wNexty;) {
|
||||
if (wDecision >= wDY) {
|
||||
wDecision -= wDY;
|
||||
wCurx += wSignX;
|
||||
wNextx -= wSignX;
|
||||
}
|
||||
for (wPX = wCurx - wWidth / 2; wPX <= wCurx + wWidth / 2; wPX++) {
|
||||
Lcd_SetPixel(wPX, wCury, color);
|
||||
}
|
||||
for (wPX = wNextx - wWidth / 2; wPX <= wNextx + wWidth / 2; wPX++) {
|
||||
Lcd_SetPixel(wPX, wNexty, color);
|
||||
}
|
||||
wCury++;
|
||||
wNexty--;
|
||||
wDecision += wDX;
|
||||
}
|
||||
}
|
||||
|
||||
return LCD_OK;
|
||||
}
|
||||
|
||||
/* 宏: M_Max / M_Min
|
||||
* 作用:
|
||||
* 计算两个值的较大/较小值,用于将任意方向输入规范化为最小/最大边界。
|
||||
|
||||
@@ -9,5 +9,6 @@ int8_t Lcd_FillRect(uint16_t left_x, uint16_t top_y, uint16_t right_x, uint16_t
|
||||
int8_t Lcd_Invert(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend);
|
||||
int8_t Lcd_LineH(uint16_t wXStart, uint16_t wXEnd, uint16_t wYStart, uint16_t wWidth, uint8_t color);
|
||||
int8_t Lcd_LineV(uint16_t wYStart, uint16_t wYEnd, uint16_t wXStart, uint16_t wWidth, uint8_t color);
|
||||
int8_t Lcd_Line(uint16_t wXstart, uint16_t wYstart, uint16_t wXend, uint16_t wYend, uint16_t wWidth, uint8_t color);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* -------------------------------------------------------------------------
|
||||
* 文件名: lcd_text.c
|
||||
* 模块职责:
|
||||
* 提供文本渲染与 UTF-8 解析能力,包含:
|
||||
* 1) UTF-8 单字符解码(utf8_next)
|
||||
* 2) ASCII 字符绘制(内部函数 Lcd_Pub_Ascii)
|
||||
* 3) 中文位图绘制(内部函数 Lcd_Pub_UTF8)
|
||||
* 4) 字符串渲染入口(Lcd_ShowStr)
|
||||
* 提供文本渲染能力,包含:
|
||||
* 1) ASCII 字符绘制(内部函数 Lcd_Pub_Ascii)
|
||||
* 2) 中文位图绘制(内部函数 Lcd_Pub_UTF8)
|
||||
* 3) 字符串渲染入口(Lcd_ShowStr)
|
||||
* 4) 依赖 common/utf8 的 utf8_next 进行 UTF-8 解析
|
||||
*
|
||||
* 设计说明:
|
||||
* - 显示像素最终通过 Lcd_SetPixel 写入显存。
|
||||
@@ -30,50 +30,6 @@ static textConfig text_cfg = {
|
||||
.rowSpace = 2
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数名: utf8_next
|
||||
* 功能:
|
||||
* 从 UTF-8 字节流当前位置解析“一个字符”,输出 Unicode 码点与字节长度。
|
||||
*
|
||||
* 参数:
|
||||
* utf8 - 指向当前待解析字节
|
||||
* out_unicode - 输出解析得到的 Unicode 码点
|
||||
*
|
||||
* 返回值:
|
||||
* 0 : 解析失败/字符串结束
|
||||
* 1 : ASCII 单字节
|
||||
* 2 : 两字节 UTF-8
|
||||
* 3 : 三字节 UTF-8
|
||||
*
|
||||
* 注意:
|
||||
* - 当前实现不支持 4 字节 UTF-8(如 emoji)。
|
||||
* - 对截断序列(缺少后续字节)返回 0。
|
||||
* ------------------------------------------------------------------------- */
|
||||
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode)
|
||||
{
|
||||
unsigned char c = utf8[0];
|
||||
if (c == 0) {
|
||||
*out_unicode = 0;
|
||||
return 0;
|
||||
}
|
||||
if (c < 0x80) {
|
||||
*out_unicode = c;
|
||||
return 1;
|
||||
}
|
||||
if ((c & 0xE0) == 0xC0) {
|
||||
if (utf8[1] == 0) return 0;
|
||||
*out_unicode = (uint32_t)((c & 0x1F) << 6 | (utf8[1] & 0x3F));
|
||||
return 2;
|
||||
}
|
||||
if ((c & 0xF0) == 0xE0) {
|
||||
if (utf8[1] == 0 || utf8[2] == 0) return 0;
|
||||
*out_unicode = (uint32_t)((c & 0x0F) << 12 | (utf8[1] & 0x3F) << 6 | (utf8[2] & 0x3F));
|
||||
return 3;
|
||||
}
|
||||
*out_unicode = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数名: Lcd_Pub_Ascii(内部)
|
||||
* 功能:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define __LCD_TEXT_H__
|
||||
|
||||
#include "lcd.h"
|
||||
#include "utf8.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t wGBFontWidth;
|
||||
@@ -12,7 +13,6 @@ typedef struct {
|
||||
uint16_t rowSpace;
|
||||
} textConfig;
|
||||
|
||||
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode);
|
||||
int8_t Lcd_ShowStr(uint16_t x, uint16_t y, uint8_t *pcString);
|
||||
int8_t Lcd_ShowTest(uint16_t x, uint16_t y, uint8_t *pcString);
|
||||
|
||||
|
||||
1239
src/Drv/menu.c
1239
src/Drv/menu.c
File diff suppressed because it is too large
Load Diff
191
src/Drv/menu/app/menu.c
Normal file
191
src/Drv/menu/app/menu.c
Normal file
@@ -0,0 +1,191 @@
|
||||
#include "menu.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "../model/display.h"
|
||||
#include "../../key.h"
|
||||
#include "../view/menu_layout.h"
|
||||
#include "../presenter/menu_presenter.h"
|
||||
#include "../view/menu_render_port.h"
|
||||
#include "../common/menu_state_types.h"
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 运行态状态收口(App 层私有)
|
||||
*
|
||||
* 设计意图:
|
||||
* 1) 避免全局变量四处散落,统一由 app/menu.c 托管生命周期;
|
||||
* 2) 外部模块只能经由 MenuApp_* 接口访问,降低耦合;
|
||||
* 3) Presenter/Model/View 依赖都在初始化时一次性装配。
|
||||
*
|
||||
* 变量职责:
|
||||
* - s_dspCtrl:
|
||||
* 显示刷新控制状态(例如首帧标记 bFirst)。
|
||||
* - s_menuCtrl:
|
||||
* 导航与路由控制状态(当前节点、各级 route、0 级入口等)。
|
||||
* - s_menuItems[300]:
|
||||
* 运行时菜单节点池(树构建后常驻内存),由 Model 初始化、View 读取布局/渲染。
|
||||
* - g_menuPresenter:
|
||||
* MVP 调度中枢实例,封装输入处理与刷新协同。
|
||||
* - g_menuLayoutConfig:
|
||||
* 布局参数快照(屏幕尺寸、行高、边距、菜单宽度等),初始化时写入一次。
|
||||
* ------------------------------------------------------------------------- */
|
||||
static tagDspCtrl s_dspCtrl;
|
||||
static tagMenuCtrl s_menuCtrl;
|
||||
static tagMenuItem s_menuItems[300];
|
||||
|
||||
static MenuPresenter g_menuPresenter;
|
||||
static MenuLayoutConfig g_menuLayoutConfig;
|
||||
|
||||
void MenuApp_Render(void)
|
||||
{
|
||||
/* 统一渲染入口:由 Presenter 决定全量/增量刷新策略。 */
|
||||
MenuPresenter_Refresh(&g_menuPresenter);
|
||||
}
|
||||
|
||||
void MenuApp_HandleInput(uint8_t keyVal)
|
||||
{
|
||||
/* 单次输入事件入口:按键解释与状态迁移都在 Presenter 内完成。 */
|
||||
MenuPresenter_HandleInput(&g_menuPresenter, keyVal);
|
||||
}
|
||||
|
||||
void MenuApp_PollInput(void)
|
||||
{
|
||||
/* 轮询输入设备(键盘/按键),再复用事件处理入口。 */
|
||||
MenuApp_HandleInput(Key_Read());
|
||||
}
|
||||
|
||||
void MenuApp_Init(void)
|
||||
{
|
||||
const MenuRenderPort *renderPort = MenuRenderPort_Lcd();
|
||||
uint16_t lcdSizeX;
|
||||
uint16_t lcdSizeY;
|
||||
|
||||
/* 1) 初始化底层渲染设备(LCD/模拟器具体实现由 renderPort 决定)。 */
|
||||
renderPort->init();
|
||||
|
||||
/* 2) 通过渲染端口查询设备能力,避免 app 层直接依赖 lcd.h 宏。 */
|
||||
lcdSizeX = renderPort->get_size_x();
|
||||
lcdSizeY = renderPort->get_size_y();
|
||||
|
||||
/* 3) 组装布局配置(后续由 View 布局计算与渲染阶段复用)。 */
|
||||
g_menuLayoutConfig.lcdSizeX = lcdSizeX;
|
||||
g_menuLayoutConfig.lcdSizeY = lcdSizeY;
|
||||
g_menuLayoutConfig.menuYMin = MENU_YMIN;
|
||||
g_menuLayoutConfig.menuYMax = MENU_YMAX_FROM_LCD(lcdSizeY);
|
||||
g_menuLayoutConfig.lineHeight = LINE_HEIGHT;
|
||||
g_menuLayoutConfig.menuWidth = MENU_WITDTH;
|
||||
g_menuLayoutConfig.menuXAdd = MENU_XADD;
|
||||
g_menuLayoutConfig.menuYAdd = MENU_YADD;
|
||||
|
||||
/* 4) 装配 Presenter:注入控制状态、菜单池、渲染端口与布局配置。 */
|
||||
MenuPresenter_Setup(&g_menuPresenter,
|
||||
&s_dspCtrl,
|
||||
&s_menuCtrl,
|
||||
s_menuItems,
|
||||
renderPort,
|
||||
&g_menuLayoutConfig);
|
||||
|
||||
/* 5) 初始化模型数据并建立初始路由/布局。 */
|
||||
MenuPresenter_InitModel(&g_menuPresenter, g_tMenuModelTab, MENU_MAX_ITEM);
|
||||
}
|
||||
|
||||
const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count)
|
||||
{
|
||||
/* 诊断/测试只读访问接口:
|
||||
* - 返回内部菜单池首地址;
|
||||
* - 若 count 非空,同步返回数组容量。 */
|
||||
if (count != NULL)
|
||||
{
|
||||
*count = (uint16_t)(sizeof(s_menuItems) / sizeof(s_menuItems[0]));
|
||||
}
|
||||
return s_menuItems;
|
||||
}
|
||||
|
||||
void MenuProc_See_AppInfo()
|
||||
{
|
||||
printf("MenuProc_See_AppInfo\n");
|
||||
}
|
||||
|
||||
void MenuProc_See_YC()
|
||||
{
|
||||
printf("MenuProc_See_YC\n");
|
||||
}
|
||||
void MenuProc_Set_Value()
|
||||
{
|
||||
printf("MenuProc_Set_Value\n");
|
||||
}
|
||||
void MenuProc_Set_Soft()
|
||||
{
|
||||
printf("MenuProc_Set_Soft\n");
|
||||
}
|
||||
void MenuProc_Cfg_Time()
|
||||
{
|
||||
printf("MenuProc_Cfg_Time\n");
|
||||
}
|
||||
void MenuProc_Cfg_RevEvent()
|
||||
{
|
||||
printf("MenuProc_Cfg_RevEvent\n");
|
||||
}
|
||||
void MenuProc_Cfg_ManualWave()
|
||||
{
|
||||
printf("MenuProc_Cfg_ManualWave\n");
|
||||
}
|
||||
void MenuProc_See_Input()
|
||||
{
|
||||
printf("MenuProc_See_Input\n");
|
||||
}
|
||||
|
||||
void MenuProc_See_Set()
|
||||
{
|
||||
printf("MenuProc_See_Set\n");
|
||||
}
|
||||
|
||||
void MenuProc_See_Soft()
|
||||
{
|
||||
printf("MenuProc_See_Soft\n");
|
||||
}
|
||||
void MenuProc_YX_SetCommType()
|
||||
{
|
||||
printf("MenuProc_YX_SetCommType\n");
|
||||
}
|
||||
|
||||
void MenuProc_YX_SetWidth()
|
||||
{
|
||||
printf("MenuProc_YX_SetWidth\n");
|
||||
}
|
||||
|
||||
void MenuProc_YX_SetTwin()
|
||||
{
|
||||
printf("MenuProc_YX_SetTwin\n");
|
||||
}
|
||||
|
||||
/* ---------- 以下为 display.c 菜单表引用、暂无具体实现的桩函数,避免链接未定义符号 ---------- */
|
||||
void Menu_NonPfunc(void) { printf("Menu_NonPfunc\n"); }
|
||||
void MenuProc_AllInf_Default(void) { printf("MenuProc_AllInf_Default\n"); }
|
||||
void MenuProc_Para_Default(void) { printf("MenuProc_Para_Default\n"); }
|
||||
void MenuProc_Set_Default(void) { printf("MenuProc_Set_Default\n"); }
|
||||
void MenuProc_Resume_Soft(void) { printf("MenuProc_Resume_Soft\n"); }
|
||||
void MenuProc_Cfg_CellDef(void) { printf("MenuProc_Cfg_CellDef\n"); }
|
||||
void MenuProc_Cfg_ShowAnaType(void) { printf("MenuProc_Cfg_ShowAnaType\n"); }
|
||||
void MenuProc_Dbg_XuYX(void) { printf("MenuProc_Dbg_XuYX\n"); }
|
||||
void MenuProc_Dbg_XuYC(void) { printf("MenuProc_Dbg_XuYC\n"); }
|
||||
void MenuProc_Dbg_XuEvent(void) { printf("MenuProc_Dbg_XuEvent\n"); }
|
||||
void MenuProc_Dbg_Relay(void) { printf("MenuProc_Dbg_Relay\n"); }
|
||||
void MenuProc_See_VersionBoard(void) { printf("MenuProc_See_VersionBoard\n"); }
|
||||
void MenuProc_Cfg_ClrRec(void) { printf("MenuProc_Cfg_ClrRec\n"); }
|
||||
void MenuProc_Cfg_ComPara(void) { printf("MenuProc_Cfg_ComPara\n"); }
|
||||
void MenuProc_Cfg_EditIP(void) { printf("MenuProc_Cfg_EditIP\n"); }
|
||||
void MenuProc_Cfg_EditSntp(void) { printf("MenuProc_Cfg_EditSntp\n"); }
|
||||
void MenuProc_Cfg_CellConf(void) { printf("MenuProc_Cfg_CellConf\n"); }
|
||||
void MenuProc_See_RecSOE(void) { printf("MenuProc_See_RecSOE\n"); }
|
||||
void MenuProc_See_RecAct(void) { printf("MenuProc_See_RecAct\n"); }
|
||||
void MenuProc_See_RecOpt(void) { printf("MenuProc_See_RecOpt\n"); }
|
||||
void MenuProc_See_RecAlm(void) { printf("MenuProc_See_RecAlm\n"); }
|
||||
void MenuProc_See_RecStart(void) { printf("MenuProc_See_RecStart\n"); }
|
||||
void MenuProc_See_RecYK(void) { printf("MenuProc_See_RecYK\n"); }
|
||||
void MenuProc_See_RecChk(void) { printf("MenuProc_See_RecChk\n"); }
|
||||
void MenuProc_See_RecRun(void) { printf("MenuProc_See_RecRun\n"); }
|
||||
void MenuProc_See_RecFault(void) { printf("MenuProc_See_RecFault\n"); }
|
||||
void MenuProc_YC_SetSqValue(void) { printf("MenuProc_YC_SetSqValue\n"); }
|
||||
void MenuProc_YC_SetAdjCoe(void) { printf("MenuProc_YC_SetAdjCoe\n"); }
|
||||
@@ -7,30 +7,16 @@
|
||||
* 目标:
|
||||
* - 用纯 C 语言在控制台模拟嵌入式 HMI 的菜单逻辑
|
||||
* - 键值映射:W/S/A/D/Enter/Esc → 上/下/左/右/确认/退出
|
||||
* - 结构设计尽量贴近原工程的 g_tMenuCtrl / Menu_Route / Menu_Show_Proc
|
||||
* - 结构采用 MVP:Model(状态) / View(渲染) / Presenter(调度)
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "../common/menu_item_types.h"
|
||||
|
||||
#include "lcd/lcd.h" /* MENU_YMAX 依赖 LCD_SIZE_Y */
|
||||
|
||||
#define CN_HEIGHT 12 /* 菜单汉字高 */
|
||||
#define CN_ROWSPACE 2 // 菜单字符行间隔
|
||||
#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE) // 字符行间隔
|
||||
#define MENU_XADD 4 // 菜单框横坐标增加
|
||||
#define MENU_YADD 4 // 菜单框纵坐标增加
|
||||
#define MENU_WITDTH 7 /*ASCII字体宽度 7,因此单个字节显示的宽度是 7 个像素点*/
|
||||
|
||||
|
||||
|
||||
#define MENU_YMIN 0 // 菜单 Y 坐标的最小值 顶部边界 0
|
||||
#define MENU_YMAX (LCD_SIZE_Y - 12) // 菜单 Y 坐标的最大值 底部边界 LCD_SIZE_Y - 16
|
||||
|
||||
|
||||
/* 初始化一棵简单的测试菜单树 */
|
||||
void Menu_Init();
|
||||
void Menu_Route(void);
|
||||
void Menu_Show_Proc(void);
|
||||
void MenuApp_Init(void);
|
||||
void MenuApp_HandleInput(uint8_t keyVal);
|
||||
void MenuApp_PollInput(void);
|
||||
void MenuApp_Render(void);
|
||||
const tagMenuItem *MenuApp_GetMenuItems(uint16_t *count);
|
||||
|
||||
/* 非功能键处理 */
|
||||
void Menu_NonPfunc();
|
||||
@@ -46,8 +32,6 @@ void MenuProc_Dbg_XuEvent();
|
||||
void MenuProc_Dbg_Relay();
|
||||
void MenuProc_See_VersionBoard();
|
||||
|
||||
|
||||
|
||||
void MenuProc_See_AppInfo();
|
||||
void MenuProc_Set_Value();
|
||||
void MenuProc_Set_Soft();
|
||||
@@ -81,4 +65,3 @@ void MenuProc_YX_SetWidth();
|
||||
void MenuProc_YX_SetTwin();
|
||||
|
||||
#endif
|
||||
|
||||
52
src/Drv/menu/common/menu_item_types.h
Normal file
52
src/Drv/menu/common/menu_item_types.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef MENU_ITEM_TYPES_H
|
||||
#define MENU_ITEM_TYPES_H
|
||||
|
||||
#include "types.h"
|
||||
|
||||
typedef struct MenuItem MenuItem;
|
||||
typedef MenuItem *tagPMenuItem;
|
||||
typedef MenuItem tagMenuItem;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
MenuItem *higher;
|
||||
MenuItem *lower;
|
||||
MenuItem *before;
|
||||
MenuItem *behind;
|
||||
} MenuLinks;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t wSPosX;
|
||||
uint16_t wSPosY;
|
||||
uint16_t wEPosX;
|
||||
uint16_t wEPosY;
|
||||
} MenuRect;
|
||||
|
||||
struct MenuItem
|
||||
{
|
||||
/* 导航关系 */
|
||||
MenuItem *ptHigher;
|
||||
MenuItem *ptLower;
|
||||
MenuItem *ptBefore;
|
||||
MenuItem *ptBehind;
|
||||
|
||||
/* 菜单元数据 */
|
||||
uint8_t byClass;
|
||||
uint8_t byName[50];
|
||||
uint8_t byTip[50];
|
||||
uint8_t byAttrib;
|
||||
uint16_t wPassword;
|
||||
uint16_t wPara;
|
||||
FUNCPTR pfnWinProc;
|
||||
|
||||
/* 视图与布局 */
|
||||
uint16_t wPos;
|
||||
uint16_t wNum;
|
||||
uint16_t wSPosX;
|
||||
uint16_t wSPosY;
|
||||
uint16_t wEPosX;
|
||||
uint16_t wEPosY;
|
||||
};
|
||||
|
||||
#endif
|
||||
23
src/Drv/menu/common/menu_state_types.h
Normal file
23
src/Drv/menu/common/menu_state_types.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef MENU_STATE_TYPES_H
|
||||
#define MENU_STATE_TYPES_H
|
||||
|
||||
#include "menu_item_types.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
/* 当前菜单显示只依赖首帧刷新标记 */
|
||||
unsigned bFirst : 1;
|
||||
} tagDspCtrl;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t by0LevelNum;
|
||||
|
||||
tagPMenuItem ptHead;
|
||||
tagPMenuItem ptCurrent;
|
||||
tagPMenuItem ptRoute[4];
|
||||
tagPMenuItem ptCurBak;
|
||||
tagPMenuItem pt0Level;
|
||||
} tagMenuCtrl;
|
||||
|
||||
#endif
|
||||
@@ -1,8 +1,6 @@
|
||||
#include "menu.h"
|
||||
#include "../app/menu.h"
|
||||
#include "display.h"
|
||||
|
||||
|
||||
|
||||
//========================================== //
|
||||
// 字符串代号常量定义
|
||||
enum _STR_FLAG_ {
|
||||
@@ -74,7 +72,7 @@ enum _STR_FLAG_ {
|
||||
EN_STR_FLAG_HARMOTYPE,
|
||||
EN_STR_FLAG_YXSIN_DI, //设置双点遥信
|
||||
EN_STR_FLAG_SHOWANATYPE,
|
||||
EN_STR_FLAG_ANAPOLARTYPE, // 交流通道极性
|
||||
EN_STR_FLAG_ANAPOLARTYPE, // 交流通道极性
|
||||
EN_STR_FLAG_WIRDIAGRAMTYPE // 增加主接线图类型识别.modified by zhanggl 111010
|
||||
|
||||
} enumStrType;
|
||||
@@ -84,7 +82,7 @@ enum _STR_FLAG_ {
|
||||
*/
|
||||
const tagMenuModel g_tMenuModelTab[] =
|
||||
{
|
||||
{ 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
|
||||
{ 0, "主界面", "", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
|
||||
{ 1, "装置信息", "查看装置信息", 0, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_AppInfo },
|
||||
{ 1, "实时数据", "装置实时数据", 0, 0x0000, 0, (FUNCPTR)Menu_NonPfunc },
|
||||
{ 2, "交流量", "查看遥测交流量", EN_MEA_AC, 0x0000, EN_ANA_0, (FUNCPTR)MenuProc_See_YC },
|
||||
52
src/Drv/menu/model/menu_model.c
Normal file
52
src/Drv/menu/model/menu_model.c
Normal file
@@ -0,0 +1,52 @@
|
||||
#include "menu_model.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "menu_tree_builder.h"
|
||||
|
||||
/* 初始化阶段一次性修饰菜单名称:对有子项的节点追加 '\x10' 指示符。 */
|
||||
static void MenuModel_DecorateDisplayNames(tagMenuItem *items, uint32_t maxItem)
|
||||
{
|
||||
for (uint32_t i = 0; i < maxItem; i++)
|
||||
{
|
||||
uint16_t nameCapacity = (uint16_t)sizeof(items[i].byName);
|
||||
uint16_t maxTextLenWithoutNull = (uint16_t)(nameCapacity - 1);
|
||||
uint16_t len = 0;
|
||||
if (items[i].ptLower == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
while ((len < maxTextLenWithoutNull) && (items[i].byName[len] != '\0'))
|
||||
{
|
||||
len++;
|
||||
}
|
||||
if ((len == 0) || (items[i].byName[len - 1] != '\x10'))
|
||||
{
|
||||
if (len < maxTextLenWithoutNull)
|
||||
{
|
||||
items[i].byName[len] = '\x10';
|
||||
items[i].byName[len + 1] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl,
|
||||
tagMenuItem *menuItems,
|
||||
const tagMenuModel *modelTab,
|
||||
uint32_t modelCount)
|
||||
{
|
||||
MenuTree_0LevelNumCal(menuCtrl, modelTab, modelCount);
|
||||
MenuTree_MainCreate(menuItems, modelTab, modelCount);
|
||||
MenuModel_DecorateDisplayNames(menuItems, modelCount);
|
||||
|
||||
menuCtrl->ptHead = &menuItems[0];
|
||||
|
||||
menuCtrl->ptRoute[0] = &menuItems[0];
|
||||
menuCtrl->pt0Level = menuCtrl->ptRoute[0];
|
||||
menuCtrl->ptCurrent = menuCtrl->ptHead->ptLower;
|
||||
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
|
||||
menuCtrl->ptRoute[1] = menuCtrl->ptCurrent;
|
||||
menuCtrl->ptRoute[2] = menuCtrl->ptCurrent;
|
||||
menuCtrl->ptRoute[3] = menuCtrl->ptCurrent;
|
||||
}
|
||||
12
src/Drv/menu/model/menu_model.h
Normal file
12
src/Drv/menu/model/menu_model.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#ifndef MENU_MODEL_H
|
||||
#define MENU_MODEL_H
|
||||
|
||||
#include "display.h"
|
||||
#include "../common/menu_state_types.h"
|
||||
|
||||
void MenuModel_Bootstrap(tagMenuCtrl *menuCtrl,
|
||||
tagMenuItem *menuItems,
|
||||
const tagMenuModel *modelTab,
|
||||
uint32_t modelCount);
|
||||
|
||||
#endif
|
||||
119
src/Drv/menu/model/menu_tree_builder.c
Normal file
119
src/Drv/menu/model/menu_tree_builder.c
Normal file
@@ -0,0 +1,119 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "menu_tree_builder.h"
|
||||
|
||||
void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem)
|
||||
{
|
||||
uint8_t by0LevelNum = 0;
|
||||
uint32_t wLoop;
|
||||
|
||||
for (wLoop = 0; wLoop < maxItem; wLoop++)
|
||||
{
|
||||
if (modelTab[wLoop].byClass == 0)
|
||||
{
|
||||
by0LevelNum++;
|
||||
}
|
||||
}
|
||||
menuCtrl->by0LevelNum = by0LevelNum;
|
||||
}
|
||||
|
||||
void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem)
|
||||
{
|
||||
tagPMenuItem ptFirst[4];
|
||||
tagPMenuItem ptLast[4];
|
||||
tagPMenuItem ptCurrent;
|
||||
tagPMenuItem ptNextNode;
|
||||
uint32_t wLoop1;
|
||||
uint8_t byCurClass;
|
||||
uint8_t byNextClass;
|
||||
uint8_t wLoop2;
|
||||
|
||||
ptFirst[0] = &menuItems[0];
|
||||
ptLast[0] = ptFirst[0];
|
||||
|
||||
ptCurrent = &menuItems[0];
|
||||
ptCurrent->wPos = 0;
|
||||
ptCurrent->wNum = 0;
|
||||
ptCurrent->wSPosX = 0;
|
||||
ptCurrent->wSPosY = 0;
|
||||
ptCurrent->wEPosX = 0;
|
||||
ptCurrent->wEPosY = 0;
|
||||
|
||||
ptCurrent->ptHigher = NULL;
|
||||
ptCurrent->ptLower = NULL;
|
||||
ptCurrent->ptBefore = NULL;
|
||||
ptCurrent->ptBehind = NULL;
|
||||
|
||||
ptCurrent->byClass = modelTab[0].byClass;
|
||||
ptCurrent->byAttrib = modelTab[0].byAttrib;
|
||||
ptCurrent->wPassword = modelTab[0].wPassword;
|
||||
ptCurrent->wPara = modelTab[0].wPara;
|
||||
ptCurrent->pfnWinProc = modelTab[0].pfnWinProc;
|
||||
memcpy(ptCurrent->byName, modelTab[0].byName, 50);
|
||||
memcpy(ptCurrent->byTip, modelTab[0].byTip, 50);
|
||||
|
||||
for (wLoop1 = 1; wLoop1 < maxItem; wLoop1++)
|
||||
{
|
||||
ptNextNode = &menuItems[wLoop1];
|
||||
|
||||
ptNextNode->wPos = 0;
|
||||
ptNextNode->wNum = 0;
|
||||
ptNextNode->wSPosX = 0;
|
||||
ptNextNode->wSPosY = 0;
|
||||
ptNextNode->wEPosX = 0;
|
||||
ptNextNode->wEPosY = 0;
|
||||
|
||||
ptNextNode->ptLower = NULL;
|
||||
ptNextNode->ptBehind = NULL;
|
||||
ptNextNode->ptBefore = NULL;
|
||||
ptNextNode->ptHigher = NULL;
|
||||
|
||||
ptNextNode->byClass = modelTab[wLoop1].byClass;
|
||||
ptNextNode->byAttrib = modelTab[wLoop1].byAttrib;
|
||||
ptNextNode->wPassword = modelTab[wLoop1].wPassword;
|
||||
ptNextNode->wPara = modelTab[wLoop1].wPara;
|
||||
ptNextNode->pfnWinProc = modelTab[wLoop1].pfnWinProc;
|
||||
memcpy(ptNextNode->byName, modelTab[wLoop1].byName, 50);
|
||||
memcpy(ptNextNode->byTip, modelTab[wLoop1].byTip, 50);
|
||||
|
||||
byCurClass = ptCurrent->byClass;
|
||||
byNextClass = ptNextNode->byClass;
|
||||
|
||||
if (byCurClass < byNextClass)
|
||||
{
|
||||
ptCurrent->ptLower = ptNextNode;
|
||||
ptNextNode->ptHigher = ptCurrent;
|
||||
ptFirst[byNextClass] = ptNextNode;
|
||||
ptLast[byNextClass] = ptNextNode;
|
||||
}
|
||||
else if (byCurClass == byNextClass)
|
||||
{
|
||||
ptNextNode->ptBefore = ptCurrent;
|
||||
ptNextNode->ptHigher = ptCurrent->ptHigher;
|
||||
ptCurrent->ptBehind = ptNextNode;
|
||||
ptLast[byNextClass] = ptNextNode;
|
||||
}
|
||||
else if (byCurClass > byNextClass)
|
||||
{
|
||||
ptNextNode->ptBefore = ptLast[byNextClass];
|
||||
ptNextNode->ptHigher = ptFirst[byNextClass]->ptHigher;
|
||||
ptLast[byNextClass]->ptBehind = ptNextNode;
|
||||
ptLast[byNextClass] = ptNextNode;
|
||||
|
||||
for (wLoop2 = byCurClass; wLoop2 > byNextClass; wLoop2--)
|
||||
{
|
||||
ptLast[wLoop2]->ptBehind = ptFirst[wLoop2];
|
||||
ptFirst[wLoop2]->ptBefore = ptLast[wLoop2];
|
||||
}
|
||||
}
|
||||
|
||||
ptCurrent = ptNextNode;
|
||||
}
|
||||
|
||||
byCurClass = ptCurrent->byClass;
|
||||
for (wLoop1 = 0; wLoop1 <= byCurClass; wLoop1++)
|
||||
{
|
||||
ptLast[wLoop1]->ptBehind = ptFirst[wLoop1];
|
||||
ptFirst[wLoop1]->ptBefore = ptLast[wLoop1];
|
||||
}
|
||||
}
|
||||
10
src/Drv/menu/model/menu_tree_builder.h
Normal file
10
src/Drv/menu/model/menu_tree_builder.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef MENU_TREE_BUILDER_H
|
||||
#define MENU_TREE_BUILDER_H
|
||||
|
||||
#include "display.h"
|
||||
#include "../common/menu_state_types.h"
|
||||
|
||||
void MenuTree_0LevelNumCal(tagMenuCtrl *menuCtrl, const tagMenuModel *modelTab, uint32_t maxItem);
|
||||
void MenuTree_MainCreate(tagMenuItem *menuItems, const tagMenuModel *modelTab, uint32_t maxItem);
|
||||
|
||||
#endif
|
||||
15
src/Drv/menu/presenter/menu_nav_types.h
Normal file
15
src/Drv/menu/presenter/menu_nav_types.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef MENU_NAV_TYPES_H
|
||||
#define MENU_NAV_TYPES_H
|
||||
|
||||
#include "../common/menu_item_types.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
tagPMenuItem ptHead;
|
||||
tagPMenuItem ptCurrent;
|
||||
tagPMenuItem ptRoute[4];
|
||||
tagPMenuItem ptCurBak;
|
||||
tagPMenuItem pt0Level;
|
||||
} MenuNavState;
|
||||
|
||||
#endif
|
||||
111
src/Drv/menu/presenter/menu_navigator.c
Normal file
111
src/Drv/menu/presenter/menu_navigator.c
Normal file
@@ -0,0 +1,111 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#include "menu_navigator.h"
|
||||
|
||||
#include "../../key.h"
|
||||
|
||||
MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal)
|
||||
{
|
||||
MenuNavResult result;
|
||||
tagPMenuItem ptCurrent;
|
||||
tagPMenuItem ptHead;
|
||||
tagPMenuItem *ptRoute;
|
||||
|
||||
result.needRefresh = 0;
|
||||
result.skipRenderThisRound = 0;
|
||||
|
||||
ptCurrent = navState->ptCurrent;
|
||||
ptHead = navState->ptHead;
|
||||
ptRoute = navState->ptRoute;
|
||||
|
||||
switch (keyVal)
|
||||
{
|
||||
case KEY_F1:
|
||||
case KEY_F2:
|
||||
break;
|
||||
case KEY_U:
|
||||
ptCurrent = ptCurrent->ptBefore;
|
||||
result.needRefresh = 1;
|
||||
break;
|
||||
case KEY_D:
|
||||
ptCurrent = ptCurrent->ptBehind;
|
||||
result.needRefresh = 1;
|
||||
break;
|
||||
case KEY_L:
|
||||
ptCurrent = ptRoute[ptCurrent->byClass - 1];
|
||||
if (ptCurrent->byClass == 0)
|
||||
{
|
||||
if (ptCurrent->wPos == 1)
|
||||
{
|
||||
ptCurrent = ptCurrent->ptBefore->ptBefore;
|
||||
}
|
||||
else
|
||||
{
|
||||
ptCurrent = ptCurrent->ptBefore;
|
||||
}
|
||||
ptCurrent = ptCurrent->ptLower;
|
||||
}
|
||||
result.needRefresh = 1;
|
||||
break;
|
||||
case KEY_R:
|
||||
case KEY_ENT:
|
||||
if (ptCurrent->ptLower != NULL)
|
||||
{
|
||||
ptCurrent = ptCurrent->ptLower;
|
||||
result.needRefresh = 1;
|
||||
}
|
||||
else if (ptCurrent->pfnWinProc != NULL)
|
||||
{
|
||||
ptCurrent->pfnWinProc();
|
||||
}
|
||||
break;
|
||||
case KEY_ESC:
|
||||
if (ptCurrent->byClass == 1)
|
||||
{
|
||||
navState->pt0Level = ptHead;
|
||||
navState->ptRoute[0] = ptHead;
|
||||
navState->ptCurrent = ptHead->ptLower;
|
||||
navState->ptCurBak = navState->ptCurrent;
|
||||
navState->ptRoute[1] = navState->ptCurrent;
|
||||
result.skipRenderThisRound = 1;
|
||||
return result;
|
||||
}
|
||||
ptCurrent = ptRoute[ptCurrent->byClass - 1];
|
||||
result.needRefresh = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
navState->ptCurrent = ptCurrent;
|
||||
return result;
|
||||
}
|
||||
|
||||
void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem)
|
||||
{
|
||||
tagPMenuItem ptIndex;
|
||||
|
||||
if (navState->ptCurBak == navState->ptCurrent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ptIndex = navState->ptCurrent;
|
||||
for (uint32_t index = 0; index < maxItem; index++)
|
||||
{
|
||||
if (ptIndex->ptHigher == NULL)
|
||||
{
|
||||
ptIndex = ptIndex->ptBefore;
|
||||
}
|
||||
else
|
||||
{
|
||||
ptIndex = ptIndex->ptHigher;
|
||||
navState->ptRoute[ptIndex->byClass] = ptIndex;
|
||||
}
|
||||
if (ptIndex->byClass == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
navState->ptRoute[navState->ptCurrent->byClass] = navState->ptCurrent;
|
||||
}
|
||||
15
src/Drv/menu/presenter/menu_navigator.h
Normal file
15
src/Drv/menu/presenter/menu_navigator.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef MENU_NAVIGATOR_H
|
||||
#define MENU_NAVIGATOR_H
|
||||
|
||||
#include "menu_nav_types.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t needRefresh;
|
||||
uint8_t skipRenderThisRound;
|
||||
} MenuNavResult;
|
||||
|
||||
MenuNavResult MenuNavigator_ProcessKey(MenuNavState *navState, uint8_t keyVal);
|
||||
void MenuNavigator_RebuildRoute(MenuNavState *navState, uint32_t maxItem);
|
||||
|
||||
#endif
|
||||
176
src/Drv/menu/presenter/menu_presenter.c
Normal file
176
src/Drv/menu/presenter/menu_presenter.c
Normal file
@@ -0,0 +1,176 @@
|
||||
#include "menu_presenter.h"
|
||||
|
||||
#include "../model/menu_model.h"
|
||||
#include "menu_navigator.h"
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数: MenuPresenter_FillNavState
|
||||
* 作用: 将“运行时控制结构 tagMenuCtrl”中的导航相关字段复制到
|
||||
* “导航临时结构 MenuNavState”。
|
||||
*
|
||||
* 设计原因:
|
||||
* - MenuNavigator_* 仅关心导航字段,不应直接操作完整控制结构;
|
||||
* - 通过拷贝到轻量结构,可在 Presenter 内控制读写边界。
|
||||
* ------------------------------------------------------------------------- */
|
||||
static void MenuPresenter_FillNavState(const tagMenuCtrl *ctrl, MenuNavState *nav)
|
||||
{
|
||||
/* 根节点指针:用于 ESC 回到顶层时复位。 */
|
||||
nav->ptHead = ctrl->ptHead;
|
||||
/* 当前焦点节点:导航计算的主要输入。 */
|
||||
nav->ptCurrent = ctrl->ptCurrent;
|
||||
/* 路径第 0 级:顶层菜单路径缓存。 */
|
||||
nav->ptRoute[0] = ctrl->ptRoute[0];
|
||||
/* 路径第 1 级:一级菜单路径缓存。 */
|
||||
nav->ptRoute[1] = ctrl->ptRoute[1];
|
||||
/* 路径第 2 级:二级菜单路径缓存。 */
|
||||
nav->ptRoute[2] = ctrl->ptRoute[2];
|
||||
/* 路径第 3 级:三级菜单路径缓存。 */
|
||||
nav->ptRoute[3] = ctrl->ptRoute[3];
|
||||
/* 上一次焦点备份:用于判断是否发生焦点迁移。 */
|
||||
nav->ptCurBak = ctrl->ptCurBak;
|
||||
/* 当前可见的 0 级入口:用于判断是否需要全屏刷新。 */
|
||||
nav->pt0Level = ctrl->pt0Level;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数: MenuPresenter_ApplyNavState
|
||||
* 作用: 将导航临时结构 MenuNavState 的结果回写到 tagMenuCtrl。
|
||||
*
|
||||
* 说明:
|
||||
* - FillNavState + Navigator + ApplyNavState 构成一次输入处理闭环;
|
||||
* - 这样 Presenter 明确“读输入状态 -> 计算 -> 写回状态”三个阶段。
|
||||
* ------------------------------------------------------------------------- */
|
||||
static void MenuPresenter_ApplyNavState(tagMenuCtrl *ctrl, const MenuNavState *nav)
|
||||
{
|
||||
/* 回写根节点(通常不变,但保持对称拷贝便于维护)。 */
|
||||
ctrl->ptHead = nav->ptHead;
|
||||
/* 回写当前焦点。 */
|
||||
ctrl->ptCurrent = nav->ptCurrent;
|
||||
/* 回写 0 级路径缓存。 */
|
||||
ctrl->ptRoute[0] = nav->ptRoute[0];
|
||||
/* 回写 1 级路径缓存。 */
|
||||
ctrl->ptRoute[1] = nav->ptRoute[1];
|
||||
/* 回写 2 级路径缓存。 */
|
||||
ctrl->ptRoute[2] = nav->ptRoute[2];
|
||||
/* 回写 3 级路径缓存。 */
|
||||
ctrl->ptRoute[3] = nav->ptRoute[3];
|
||||
/* 回写焦点备份。 */
|
||||
ctrl->ptCurBak = nav->ptCurBak;
|
||||
/* 回写 0 级入口指针。 */
|
||||
ctrl->pt0Level = nav->pt0Level;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数: MenuPresenter_Setup
|
||||
* 作用: 完成 Presenter 的依赖注入和 View 绑定。
|
||||
*
|
||||
* 输入:
|
||||
* - dspCtrl/menuCtrl/menuItems: 运行态状态与数据池
|
||||
* - renderPort: 渲染端口抽象(LCD/模拟器实现)
|
||||
* - layoutConfig: 布局参数
|
||||
* ------------------------------------------------------------------------- */
|
||||
void MenuPresenter_Setup(MenuPresenter *presenter,
|
||||
tagDspCtrl *dspCtrl,
|
||||
tagMenuCtrl *menuCtrl,
|
||||
tagMenuItem *menuItems,
|
||||
const MenuRenderPort *renderPort,
|
||||
const MenuLayoutConfig *layoutConfig)
|
||||
{
|
||||
/* 保存显示控制状态句柄。 */
|
||||
presenter->dspCtrl = dspCtrl;
|
||||
/* 保存菜单控制状态句柄。 */
|
||||
presenter->menuCtrl = menuCtrl;
|
||||
/* 保存菜单节点池句柄。 */
|
||||
presenter->menuItems = menuItems;
|
||||
/* 保存布局配置句柄。 */
|
||||
presenter->layoutConfig = layoutConfig;
|
||||
/* 初始化 View,并注入具体渲染端口。 */
|
||||
MenuView_Init(&presenter->view, renderPort);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数: MenuPresenter_InitModel
|
||||
* 作用: 启动时初始化模型与布局。
|
||||
*
|
||||
* 步骤:
|
||||
* 1) 设置首帧标志,确保首次进入能触发完整绘制;
|
||||
* 2) 通过 Model 完成菜单树构建与初始路由;
|
||||
* 3) 通知 View 计算每个节点的布局坐标。
|
||||
* ------------------------------------------------------------------------- */
|
||||
void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount)
|
||||
{
|
||||
/* 标记“首帧未绘制”,后续输入循环会先触发一次刷新。 */
|
||||
presenter->dspCtrl->bFirst = 1;
|
||||
/* 构建运行时菜单树并初始化控制状态。 */
|
||||
MenuModel_Bootstrap(presenter->menuCtrl,
|
||||
presenter->menuItems,
|
||||
modelTab,
|
||||
modelCount);
|
||||
/* 根据模型结果计算显示矩形位置。 */
|
||||
MenuView_Layout(&presenter->view, presenter->menuCtrl, presenter->layoutConfig);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数: MenuPresenter_Refresh
|
||||
* 作用: 统一刷新入口,把“当前状态”交给 View 决定具体渲染策略。
|
||||
* ------------------------------------------------------------------------- */
|
||||
void MenuPresenter_Refresh(MenuPresenter *presenter)
|
||||
{
|
||||
/* View 根据 bFirst / ptCurBak / route 差异决定增量或全量刷新。 */
|
||||
MenuView_RenderByState(&presenter->view, presenter->menuCtrl, presenter->dspCtrl);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 函数: MenuPresenter_HandleInput
|
||||
* 作用: 处理一次输入事件(按键),驱动导航状态迁移与视图刷新。
|
||||
*
|
||||
* 核心流程:
|
||||
* 1) 首帧短路:先渲染再返回;
|
||||
* 2) 拷贝导航状态 -> 调用导航器 -> 回写结果;
|
||||
* 3) 按导航结果决定是否跳过渲染、是否重建路径并刷新。
|
||||
* ------------------------------------------------------------------------- */
|
||||
void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal)
|
||||
{
|
||||
/* 本轮输入使用的导航状态快照。 */
|
||||
MenuNavState navState;
|
||||
/* 导航器输出:是否需要刷新/是否跳过本轮渲染。 */
|
||||
MenuNavResult navResult;
|
||||
|
||||
/* 若仍是首帧阶段:优先做一次渲染,不消费导航逻辑。 */
|
||||
if (presenter->dspCtrl->bFirst)
|
||||
{
|
||||
/* 执行首帧刷新。 */
|
||||
MenuPresenter_Refresh(presenter);
|
||||
/* 首帧路径到此结束。 */
|
||||
return;
|
||||
}
|
||||
|
||||
/* 从控制结构提取导航子集,作为导航器输入。 */
|
||||
MenuPresenter_FillNavState(presenter->menuCtrl, &navState);
|
||||
/* 基于按键执行导航状态机,得到状态变化与渲染建议。 */
|
||||
navResult = MenuNavigator_ProcessKey(&navState, keyVal);
|
||||
/* 将导航器结果回写到运行时控制结构。 */
|
||||
MenuPresenter_ApplyNavState(presenter->menuCtrl, &navState);
|
||||
|
||||
/* 某些按键(如特定 ESC 场景)要求本轮跳过渲染。 */
|
||||
if (navResult.skipRenderThisRound)
|
||||
{
|
||||
/* 按导航器要求直接返回。 */
|
||||
return;
|
||||
}
|
||||
|
||||
/* 只有导航器明确要求刷新时,才进行路径重建与渲染。 */
|
||||
if (navResult.needRefresh)
|
||||
{
|
||||
/* 用于“自底向上”重建 route[] 的临时状态。 */
|
||||
MenuNavState rebuildState;
|
||||
/* 以最新控制状态为基准生成重建输入。 */
|
||||
MenuPresenter_FillNavState(presenter->menuCtrl, &rebuildState);
|
||||
/* 重建各级 route 缓存,保证后续渲染路径一致。 */
|
||||
MenuNavigator_RebuildRoute(&rebuildState, MENU_MAX_ITEM);
|
||||
/* 回写重建后的 route 与相关导航状态。 */
|
||||
MenuPresenter_ApplyNavState(presenter->menuCtrl, &rebuildState);
|
||||
/* 触发刷新,让界面体现本次输入结果。 */
|
||||
MenuPresenter_Refresh(presenter);
|
||||
}
|
||||
}
|
||||
30
src/Drv/menu/presenter/menu_presenter.h
Normal file
30
src/Drv/menu/presenter/menu_presenter.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef MENU_PRESENTER_H
|
||||
#define MENU_PRESENTER_H
|
||||
|
||||
#include "../model/display.h"
|
||||
#include "../view/menu_layout.h"
|
||||
#include "../view/menu_render_port.h"
|
||||
#include "../common/menu_state_types.h"
|
||||
#include "../view/menu_view.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
tagDspCtrl *dspCtrl;
|
||||
tagMenuCtrl *menuCtrl;
|
||||
tagMenuItem *menuItems;
|
||||
MenuView view;
|
||||
const MenuLayoutConfig *layoutConfig;
|
||||
} MenuPresenter;
|
||||
|
||||
void MenuPresenter_Setup(MenuPresenter *presenter,
|
||||
tagDspCtrl *dspCtrl,
|
||||
tagMenuCtrl *menuCtrl,
|
||||
tagMenuItem *menuItems,
|
||||
const MenuRenderPort *renderPort,
|
||||
const MenuLayoutConfig *layoutConfig);
|
||||
|
||||
void MenuPresenter_InitModel(MenuPresenter *presenter, const tagMenuModel *modelTab, uint32_t modelCount);
|
||||
void MenuPresenter_HandleInput(MenuPresenter *presenter, uint8_t keyVal);
|
||||
void MenuPresenter_Refresh(MenuPresenter *presenter);
|
||||
|
||||
#endif
|
||||
447
src/Drv/menu/view/menu_layout.c
Normal file
447
src/Drv/menu/view/menu_layout.c
Normal file
@@ -0,0 +1,447 @@
|
||||
#include <stddef.h>
|
||||
#include "menu_layout.h"
|
||||
#include "utf8.h"
|
||||
|
||||
/******************************************************************************
|
||||
* 模块: menu_layout.c
|
||||
* 职责: 根据菜单树结构计算各级菜单在 LCD 上的显示矩形区域
|
||||
*
|
||||
* 设计要点:
|
||||
* 1) 采用“先算当前层,再递归/级联算下一层”的方式布点;
|
||||
* 2) 每级菜单都维护:
|
||||
* - ptFirst[level]: 当前层循环链表的起点;
|
||||
* - ptIndex[level]: 当前层遍历游标;
|
||||
* - byMenuNum[level]: 当前层项数量(或当前位置计数);
|
||||
* 3) 对 Y 方向越界提供多级回退策略(向上翻转/贴顶重排),尽量保证菜单可见。
|
||||
*****************************************************************************/
|
||||
|
||||
/* 读取菜单节点导航指针集合(本文件局部辅助,避免 core 泄漏行为函数)。 */
|
||||
static MenuLinks MenuLayout_GetLinks(const MenuItem *item)
|
||||
{
|
||||
MenuLinks links;
|
||||
links.higher = item->ptHigher;
|
||||
links.lower = item->ptLower;
|
||||
links.before = item->ptBefore;
|
||||
links.behind = item->ptBehind;
|
||||
return links;
|
||||
}
|
||||
|
||||
/* 读取菜单节点矩形(本文件局部辅助)。 */
|
||||
static MenuRect MenuLayout_GetRect(const MenuItem *item)
|
||||
{
|
||||
MenuRect rect;
|
||||
rect.wSPosX = item->wSPosX;
|
||||
rect.wSPosY = item->wSPosY;
|
||||
rect.wEPosX = item->wEPosX;
|
||||
rect.wEPosY = item->wEPosY;
|
||||
return rect;
|
||||
}
|
||||
|
||||
/* 给菜单项写入矩形坐标的轻量封装,避免到处重复构造 MenuRect。 */
|
||||
static void MenuLayout_SetRect(MenuItem *item, uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey)
|
||||
{
|
||||
item->wSPosX = sx;
|
||||
item->wSPosY = sy;
|
||||
item->wEPosX = ex;
|
||||
item->wEPosY = ey;
|
||||
}
|
||||
|
||||
/* 将理论挂接行压缩映射到可视区,避免超出可显示行数。 */
|
||||
static uint16_t MenuLayout_MapMenuPos(uint16_t menuPos, uint8_t itemNum, uint8_t maxNum)
|
||||
{
|
||||
if ((itemNum > maxNum) && (menuPos >= maxNum))
|
||||
{
|
||||
return (uint16_t)(maxNum - (itemNum - menuPos));
|
||||
}
|
||||
return menuPos;
|
||||
}
|
||||
|
||||
/* 子菜单 X 方向统一规则:从父框右边展开。 */
|
||||
static void MenuLayout_CalcSubMenuX(const MenuRect *parentRect,
|
||||
uint8_t maxLen,
|
||||
const MenuLayoutConfig *config,
|
||||
uint16_t *startX,
|
||||
uint16_t *endX)
|
||||
{
|
||||
*startX = parentRect->wEPosX;
|
||||
*endX = (uint16_t)(*startX + maxLen * config->menuWidth + config->menuXAdd);
|
||||
}
|
||||
|
||||
/* 尝试将菜单框整体翻转到上方显示;成功返回 1,失败返回 0。 */
|
||||
static uint8_t MenuLayout_TryFlipUp(uint16_t *startY,
|
||||
uint16_t *endY,
|
||||
uint8_t itemNum,
|
||||
uint16_t flipMinY,
|
||||
const MenuLayoutConfig *config)
|
||||
{
|
||||
uint16_t candidateEndY;
|
||||
uint16_t temp;
|
||||
|
||||
candidateEndY = (uint16_t)(*startY - (itemNum - 1) * config->lineHeight - config->menuYAdd);
|
||||
if ((candidateEndY > flipMinY) && (candidateEndY < config->menuYMax))
|
||||
{
|
||||
temp = *startY;
|
||||
*startY = candidateEndY;
|
||||
*endY = (uint16_t)(temp + config->lineHeight);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Sub2 兜底:回退到顶层起始 Y,并在底部做截断。 */
|
||||
static void MenuLayout_FallbackClampToTop(const tagPMenuItem *ptIndex,
|
||||
uint8_t itemNum,
|
||||
uint16_t *startY,
|
||||
uint16_t *endY,
|
||||
const MenuLayoutConfig *config)
|
||||
{
|
||||
*startY = ptIndex[0]->wSPosY;
|
||||
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
|
||||
if (*endY > config->menuYMax)
|
||||
{
|
||||
*endY = config->menuYMax;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sub1 兜底:从顶层起始 Y 逐行上探,直到菜单框不越界。 */
|
||||
static void MenuLayout_FallbackProbeUp(const tagPMenuItem *ptIndex,
|
||||
uint8_t itemNum,
|
||||
uint16_t *startY,
|
||||
uint16_t *endY,
|
||||
const MenuLayoutConfig *config)
|
||||
{
|
||||
*startY = ptIndex[0]->wSPosY;
|
||||
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
|
||||
for (uint16_t i = 1; i < (uint16_t)(ptIndex[0]->wSPosY / config->lineHeight + 1); i++)
|
||||
{
|
||||
if (*endY > config->menuYMax)
|
||||
{
|
||||
*startY = (uint16_t)(ptIndex[0]->wSPosY - config->lineHeight * i);
|
||||
if (*startY < config->lineHeight)
|
||||
{
|
||||
*startY = 0;
|
||||
}
|
||||
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 计算单个菜单项“显示宽度”:
|
||||
* - 中文等多字节字符按 2 列计宽,ASCII 按 1 列;
|
||||
* - 若该项有下级菜单且名称末尾未带特殊标记 '\x10',额外预留 1 列用于层级指示。 */
|
||||
static uint8_t MenuLayout_ItemDisplayLen(const MenuItem *item)
|
||||
{
|
||||
uint8_t displayLen;
|
||||
displayLen = MenuLayout_Utf8LenCal((uint8_t *)item->byName);
|
||||
if (item->ptLower != NULL)
|
||||
{
|
||||
uint8_t byteLen = 0;
|
||||
while ((byteLen < 50) && (item->byName[byteLen] != '\0'))
|
||||
{
|
||||
byteLen++;
|
||||
}
|
||||
if ((byteLen == 0) || (item->byName[byteLen - 1] != '\x10'))
|
||||
{
|
||||
displayLen += 1;
|
||||
}
|
||||
}
|
||||
return displayLen;
|
||||
}
|
||||
|
||||
/* UTF-8 显示长度统计:
|
||||
* utf8_next() 每次返回一个 Unicode 字符占用的字节数 n,
|
||||
* 约定 n>1 视为“宽字符”(占 2 列),否则占 1 列。 */
|
||||
uint8_t MenuLayout_Utf8LenCal(uint8_t *str)
|
||||
{
|
||||
uint8_t strLen = 0;
|
||||
uint32_t unicode;
|
||||
uint8_t index = 0;
|
||||
uint8_t n = 0;
|
||||
|
||||
while (str[index] != '\0')
|
||||
{
|
||||
n = utf8_next(str + index, &unicode);
|
||||
strLen += (n > 1) ? 2 : 1;
|
||||
index += n;
|
||||
}
|
||||
return strLen;
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_CharLenCal
|
||||
* 作用:
|
||||
* 1) 统计 bylevel 下一层(bylevel+1)菜单项总数;
|
||||
* 2) 计算该层中最长显示宽度(用于决定菜单框宽度);
|
||||
* 3) 为该层每个节点写 wPos(从 1 开始的位置序号);
|
||||
* 4) 把“下一层项数”写回父节点 wNum。
|
||||
*
|
||||
* 变量字典:
|
||||
* - bylevel: 输入为父层级,函数内部会自增到“子层级”;
|
||||
* - byMenuNum[level]: level 层累计项数(同时作为 wPos 的来源);
|
||||
* - ptFirst[level]: level 层循环链表的起点节点;
|
||||
* - ptIndex[level]: level 层当前遍历节点;
|
||||
* - byMaxLen: 本层所有项显示宽度的最大值(单位: 字符列)。
|
||||
*****************************************************************************/
|
||||
uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex)
|
||||
{
|
||||
uint8_t displayLen;
|
||||
uint8_t byMaxLen = 0;
|
||||
MenuLinks links;
|
||||
|
||||
/* 进入下一层,从父节点的 lower 作为该层环形链表起点。 */
|
||||
ptFirst[bylevel + 1] = ptIndex[bylevel]->ptLower;
|
||||
bylevel = bylevel + 1;
|
||||
ptIndex[bylevel] = ptFirst[bylevel];
|
||||
|
||||
byMenuNum[bylevel] = 1;
|
||||
ptIndex[bylevel]->wPos = 1;
|
||||
|
||||
/* 固定上限 300 作为保护,避免链表异常导致死循环。 */
|
||||
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[bylevel]);
|
||||
displayLen = MenuLayout_ItemDisplayLen(ptIndex[bylevel]);
|
||||
|
||||
if (byMaxLen < displayLen)
|
||||
{
|
||||
byMaxLen = displayLen;
|
||||
}
|
||||
|
||||
ptIndex[bylevel] = links.behind;
|
||||
if (ptIndex[bylevel] == ptFirst[bylevel])
|
||||
{
|
||||
break;
|
||||
}
|
||||
byMenuNum[bylevel]++;
|
||||
ptIndex[bylevel]->wPos = byMenuNum[bylevel];
|
||||
}
|
||||
|
||||
/* 父节点记录子项数量,供分页/显示逻辑复用。 */
|
||||
ptIndex[bylevel - 1]->wNum = byMenuNum[bylevel];
|
||||
return byMaxLen;
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_Sub2PosCal
|
||||
* 作用: 计算二级子菜单(及其同层兄弟)的显示矩形
|
||||
* 策略:
|
||||
* - 先按“父项对齐”得到理想 Y;
|
||||
* - 若底部越界,尝试向上翻转;
|
||||
* - 仍不满足则使用贴近顶层菜单的兜底摆放。
|
||||
*
|
||||
* 变量字典:
|
||||
* - byMaxNum: 当前配置下可完整显示的最大行数;
|
||||
* - byMenuPos: 当前菜单相对父菜单的挂接行(理论位置);
|
||||
* - byItemNum: 当前层或下一层项数(代码中按阶段复用);
|
||||
* - wSPosY/wEPosY: 菜单框起止 Y 坐标(含间距边界);
|
||||
* - parentRect: 父菜单矩形,用于对子菜单做相对定位。
|
||||
*
|
||||
* 关键公式:
|
||||
* - X 起点 = parentRect.wEPosX(总是从父框右侧展开);
|
||||
* - X 终点 = X 起点 + byMaxLen * menuWidth + menuXAdd;
|
||||
* - Y 起点 = parentRect.wSPosY + (byMenuPos - 1) * lineHeight;
|
||||
* - Y 终点 = Y 起点 + 子项数 * lineHeight + menuYAdd。
|
||||
*****************************************************************************/
|
||||
void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config)
|
||||
{
|
||||
uint16_t wSPosY;
|
||||
uint16_t wEPosY;
|
||||
uint8_t byMaxLen;
|
||||
uint8_t byMaxNum;
|
||||
uint16_t byMenuPos;
|
||||
uint8_t byItemNum;
|
||||
uint8_t byMenuNum[4];
|
||||
MenuLinks links;
|
||||
MenuRect parentRect;
|
||||
|
||||
/* 当前可完整容纳的最大行数(预留边框/间距)。 */
|
||||
byMaxNum = (config->menuYMax - config->menuYMin - 6) / config->lineHeight;
|
||||
ptIndex[bylevel] = ptFirst[bylevel];
|
||||
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[bylevel]);
|
||||
if (links.lower != NULL)
|
||||
{
|
||||
byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex);
|
||||
parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]);
|
||||
MenuLayout_CalcSubMenuX(&parentRect,
|
||||
byMaxLen,
|
||||
config,
|
||||
&ptIndex[bylevel]->wSPosX,
|
||||
&ptIndex[bylevel]->wEPosX);
|
||||
|
||||
/* byMenuPos: 当前项在同层中的理论挂接位置(必要时压缩映射)。 */
|
||||
byMenuPos = ptIndex[bylevel]->wPos;
|
||||
byItemNum = byMenuNum[bylevel];
|
||||
byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum);
|
||||
|
||||
wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight;
|
||||
byItemNum = byMenuNum[bylevel + 1];
|
||||
wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd;
|
||||
|
||||
if (wEPosY < config->menuYMax)
|
||||
{
|
||||
/* 理想位置不越界,直接应用。 */
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* 底部越界:先尝试“向上翻转”放置整个菜单框。 */
|
||||
if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->menuYMin, config))
|
||||
{
|
||||
/* 仍不满足:回退到顶层菜单起始 Y,再做截断兜底。 */
|
||||
MenuLayout_FallbackClampToTop(ptIndex, byItemNum, &wSPosY, &wEPosY, config);
|
||||
}
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
}
|
||||
ptIndex[bylevel] = links.behind;
|
||||
if (ptIndex[bylevel] == ptFirst[bylevel])
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_Sub1PosCal
|
||||
* 作用: 计算一级子菜单(通常是主菜单下拉)的位置,并联动计算二级子菜单
|
||||
* 说明: 与 Sub2 逻辑相近,但在兜底时会逐步向上平移,直到菜单框可容纳。
|
||||
*
|
||||
* 变量字典:
|
||||
* - byMaxNum: 屏幕当前可显示的最大菜单行数;
|
||||
* - byMenuPos: 当前节点理论挂接行(可能被映射压缩);
|
||||
* - byItemNum: 当前下拉框条目数;
|
||||
* - wTemp: 用于“翻转布局”时交换起止 Y;
|
||||
* - byMenuNum[4]: 分层统计数组,索引与菜单层级一一对应。
|
||||
*
|
||||
* 与 Sub2 的核心差异:
|
||||
* - Sub1 兜底策略增加“逐行上探”,尝试找到不越界的最靠下位置,
|
||||
* 视觉上更适合主菜单下拉场景。
|
||||
*****************************************************************************/
|
||||
void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config)
|
||||
{
|
||||
uint16_t wSPosY;
|
||||
uint16_t wEPosY;
|
||||
uint8_t byMenuNum[4];
|
||||
uint8_t byMaxLen;
|
||||
uint8_t byMaxNum;
|
||||
uint16_t byMenuPos;
|
||||
uint8_t byItemNum;
|
||||
MenuLinks links;
|
||||
MenuRect parentRect;
|
||||
|
||||
byMaxNum = config->menuYMax / config->lineHeight;
|
||||
ptIndex[bylevel] = ptFirst[bylevel];
|
||||
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[bylevel]);
|
||||
if (links.lower != NULL)
|
||||
{
|
||||
byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex);
|
||||
parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]);
|
||||
MenuLayout_CalcSubMenuX(&parentRect,
|
||||
byMaxLen,
|
||||
config,
|
||||
&ptIndex[bylevel]->wSPosX,
|
||||
&ptIndex[bylevel]->wEPosX);
|
||||
|
||||
byMenuPos = ptIndex[bylevel]->wPos;
|
||||
byItemNum = byMenuNum[bylevel];
|
||||
byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum);
|
||||
|
||||
wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight;
|
||||
byItemNum = byMenuNum[bylevel + 1];
|
||||
wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd;
|
||||
|
||||
if (wEPosY < config->menuYMax)
|
||||
{
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* 底部越界:优先尝试一次整体翻转到上方。 */
|
||||
if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->lineHeight, config))
|
||||
{
|
||||
/* 二次兜底:从顶层菜单起始 Y 往上逐行试探,直到不越界。 */
|
||||
MenuLayout_FallbackProbeUp(ptIndex, byItemNum, &wSPosY, &wEPosY, config);
|
||||
}
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
/* 一级菜单定位完成后,继续计算二级菜单。 */
|
||||
MenuLayout_Sub2PosCal(2, ptFirst, ptIndex, config);
|
||||
}
|
||||
ptIndex[bylevel] = links.behind;
|
||||
if (ptIndex[bylevel] == ptFirst[bylevel])
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_PositionCal
|
||||
* 作用: 入口函数,计算从 0 级到 2 级菜单的显示坐标
|
||||
* 流程:
|
||||
* 1) 先给 0 级菜单按屏宽等分;
|
||||
* 2) 每个 0 级项若有下级,则计算其下拉框;
|
||||
* 3) 再级联计算 1/2 级子菜单。
|
||||
*
|
||||
* 变量字典:
|
||||
* - by0LevelNum: 顶层菜单数量;
|
||||
* - byInterval: 顶层菜单等分后的列宽;
|
||||
* - byMenuNum[0]: 当前顶层节点在同层中的序号(从 1 开始);
|
||||
* - byMenuNum[1]: 顶层节点的直接子项数量(由 CharLenCal 写入);
|
||||
* - ptFirst/ptIndex: 各层遍历上下文,在本函数中作为全流程共享状态。
|
||||
*****************************************************************************/
|
||||
void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config)
|
||||
{
|
||||
tagPMenuItem ptFirst[4];
|
||||
tagPMenuItem ptIndex[4];
|
||||
uint8_t byMenuNum[4];
|
||||
uint8_t byMaxLen;
|
||||
uint8_t byInterval;
|
||||
MenuLinks links;
|
||||
|
||||
/* 输入保护:头指针/配置为空,或 0 级菜单数量为 0 时直接返回。 */
|
||||
if ((ptMenuHead == NULL) || (config == NULL) || (by0LevelNum == 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ptFirst[0] = ptMenuHead;
|
||||
ptIndex[0] = ptFirst[0];
|
||||
byMenuNum[0] = 1;
|
||||
ptIndex[0]->wPos = 1;
|
||||
/* 顶层菜单按等分宽度布局。 */
|
||||
byInterval = config->lcdSizeX / by0LevelNum;
|
||||
|
||||
for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[0]);
|
||||
if (links.lower != NULL)
|
||||
{
|
||||
/* 先统计该分支子项宽高需求,再写入 0 级菜单框。 */
|
||||
byMaxLen = MenuLayout_CharLenCal(0, byMenuNum, ptFirst, ptIndex);
|
||||
MenuLayout_SetRect(
|
||||
ptIndex[0],
|
||||
(byMenuNum[0] - 1) * byInterval,
|
||||
config->lcdSizeY - config->lineHeight - byMenuNum[1] * config->lineHeight - config->menuYAdd,
|
||||
(byMenuNum[0] - 1) * byInterval + byMaxLen * config->menuWidth + config->menuXAdd,
|
||||
config->lcdSizeY - config->lineHeight);
|
||||
/* 0 级完成后继续展开 1/2 级。 */
|
||||
MenuLayout_Sub1PosCal(1, ptFirst, ptIndex, config);
|
||||
}
|
||||
ptIndex[0] = links.behind;
|
||||
if (ptIndex[0] == ptFirst[0])
|
||||
{
|
||||
break;
|
||||
}
|
||||
byMenuNum[0]++;
|
||||
ptIndex[0]->wPos = byMenuNum[0];
|
||||
}
|
||||
}
|
||||
34
src/Drv/menu/view/menu_layout.h
Normal file
34
src/Drv/menu/view/menu_layout.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef MENU_LAYOUT_H
|
||||
#define MENU_LAYOUT_H
|
||||
|
||||
#include "../common/menu_item_types.h"
|
||||
|
||||
#define CN_HEIGHT 12
|
||||
#define CN_ROWSPACE 2
|
||||
#define LINE_HEIGHT (CN_HEIGHT + CN_ROWSPACE)
|
||||
#define MENU_XADD 4
|
||||
#define MENU_YADD 4
|
||||
#define MENU_WITDTH 7
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t lcdSizeX;
|
||||
uint16_t lcdSizeY;
|
||||
uint16_t menuYMin;
|
||||
uint16_t menuYMax;
|
||||
uint16_t lineHeight;
|
||||
uint16_t menuWidth;
|
||||
uint16_t menuXAdd;
|
||||
uint16_t menuYAdd;
|
||||
} MenuLayoutConfig;
|
||||
|
||||
#define MENU_YMIN 0
|
||||
#define MENU_YMAX_FROM_LCD(ySize) ((uint16_t)((ySize) - 12U))
|
||||
|
||||
uint8_t MenuLayout_Utf8LenCal(uint8_t *str);
|
||||
uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex);
|
||||
void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config);
|
||||
void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config);
|
||||
void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config);
|
||||
|
||||
#endif
|
||||
22
src/Drv/menu/view/menu_render_port.h
Normal file
22
src/Drv/menu/view/menu_render_port.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef MENU_RENDER_PORT_H
|
||||
#define MENU_RENDER_PORT_H
|
||||
|
||||
typedef struct
|
||||
{
|
||||
void (*init)(void);
|
||||
unsigned short (*get_size_x)(void);
|
||||
unsigned short (*get_size_y)(void);
|
||||
unsigned char (*get_color_font)(void);
|
||||
unsigned char (*get_color_back)(void);
|
||||
signed char (*fill_rect)(unsigned short left_x, unsigned short top_y, unsigned short right_x, unsigned short bottom_y, unsigned int color);
|
||||
signed char (*line_h)(unsigned short x_start, unsigned short x_end, unsigned short y, unsigned short width, unsigned char color);
|
||||
signed char (*line_v)(unsigned short y_start, unsigned short y_end, unsigned short x, unsigned short width, unsigned char color);
|
||||
signed char (*line)(unsigned short x_start, unsigned short y_start, unsigned short x_end, unsigned short y_end, unsigned short width, unsigned char color);
|
||||
signed char (*set_pixel)(unsigned short x, unsigned short y, unsigned char color);
|
||||
signed char (*invert)(unsigned short left_x, unsigned short top_y, unsigned short right_x, unsigned short bottom_y);
|
||||
signed char (*show_str)(unsigned short x, unsigned short y, unsigned char *text);
|
||||
} MenuRenderPort;
|
||||
|
||||
const MenuRenderPort *MenuRenderPort_Lcd(void);
|
||||
|
||||
#endif
|
||||
45
src/Drv/menu/view/menu_renderer_lcd.c
Normal file
45
src/Drv/menu/view/menu_renderer_lcd.c
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "menu_render_port.h"
|
||||
|
||||
#include "lcd.h"
|
||||
#include "lcd_draw.h"
|
||||
#include "lcd_text.h"
|
||||
|
||||
static unsigned short MenuRenderPort_LcdSizeX(void)
|
||||
{
|
||||
return LCD_SIZE_X;
|
||||
}
|
||||
|
||||
static unsigned short MenuRenderPort_LcdSizeY(void)
|
||||
{
|
||||
return LCD_SIZE_Y;
|
||||
}
|
||||
|
||||
static unsigned char MenuRenderPort_LcdColorFont(void)
|
||||
{
|
||||
return LCD_FONT;
|
||||
}
|
||||
|
||||
static unsigned char MenuRenderPort_LcdColorBack(void)
|
||||
{
|
||||
return LCD_BACK;
|
||||
}
|
||||
|
||||
static const MenuRenderPort g_lcd_port = {
|
||||
.init = Lcd_Init,
|
||||
.get_size_x = MenuRenderPort_LcdSizeX,
|
||||
.get_size_y = MenuRenderPort_LcdSizeY,
|
||||
.get_color_font = MenuRenderPort_LcdColorFont,
|
||||
.get_color_back = MenuRenderPort_LcdColorBack,
|
||||
.fill_rect = Lcd_FillRect,
|
||||
.line_h = Lcd_LineH,
|
||||
.line_v = Lcd_LineV,
|
||||
.line = Lcd_Line,
|
||||
.set_pixel = Lcd_SetPixel,
|
||||
.invert = Lcd_Invert,
|
||||
.show_str = Lcd_ShowStr,
|
||||
};
|
||||
|
||||
const MenuRenderPort *MenuRenderPort_Lcd(void)
|
||||
{
|
||||
return &g_lcd_port;
|
||||
}
|
||||
114
src/Drv/menu/view/menu_view.c
Normal file
114
src/Drv/menu/view/menu_view.c
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "menu_view.h"
|
||||
|
||||
static void MenuView_DrawMeitou(MenuView *view, uint16_t yStart, uint16_t width)
|
||||
{
|
||||
uint8_t fontColor = view->port->get_color_font();
|
||||
view->port->line_h(16, 144, yStart, width, fontColor);
|
||||
view->port->line(8, yStart - 8, 16, yStart, width, fontColor);
|
||||
view->port->line(144, yStart, 152, yStart - 8, width, fontColor);
|
||||
}
|
||||
|
||||
static void MenuView_DrawBoundaryBox(MenuView *view, uint16_t leftX, uint16_t topY, uint16_t rightX, uint16_t bottomY)
|
||||
{
|
||||
uint8_t backColor = view->port->get_color_back();
|
||||
uint8_t fontColor = view->port->get_color_font();
|
||||
for (uint16_t y = topY; y < bottomY; y++)
|
||||
{
|
||||
for (uint16_t x = leftX; x < rightX; x++)
|
||||
{
|
||||
view->port->set_pixel(x, y, backColor);
|
||||
}
|
||||
}
|
||||
|
||||
view->port->line_h(leftX, rightX, topY, 1, fontColor);
|
||||
view->port->line_v(topY, bottomY, leftX, 1, fontColor);
|
||||
view->port->line_h(leftX, rightX + 1, bottomY, 1, fontColor);
|
||||
view->port->line_v(topY, bottomY, rightX, 1, fontColor);
|
||||
}
|
||||
|
||||
static void MenuView_ShowOtherLevel(MenuView *view, const tagMenuCtrl *menuCtrl, uint8_t level)
|
||||
{
|
||||
tagPMenuItem ptIndex = menuCtrl->ptRoute[level];
|
||||
tagPMenuItem ptRoute;
|
||||
uint16_t wPosX;
|
||||
uint16_t wPosY;
|
||||
|
||||
MenuView_DrawBoundaryBox(view, ptIndex->wSPosX, ptIndex->wSPosY, ptIndex->wEPosX, ptIndex->wEPosY);
|
||||
|
||||
ptRoute = menuCtrl->ptRoute[level + 1];
|
||||
ptIndex = ptRoute;
|
||||
wPosX = menuCtrl->ptRoute[level]->wSPosX + 4;
|
||||
for (uint16_t index = 0; index < menuCtrl->ptRoute[level]->wNum; index++)
|
||||
{
|
||||
wPosY = menuCtrl->ptRoute[level]->wSPosY + (ptIndex->wPos - 1) * LINE_HEIGHT + 3;
|
||||
view->port->show_str(wPosX, wPosY, ptIndex->byName);
|
||||
if (ptRoute == ptIndex)
|
||||
{
|
||||
view->port->invert(menuCtrl->ptRoute[level]->wSPosX + 2, wPosY - 1, menuCtrl->ptRoute[level]->wEPosX - 2, wPosY + 14);
|
||||
}
|
||||
ptIndex = ptIndex->ptBehind;
|
||||
}
|
||||
}
|
||||
|
||||
static void MenuView_ShowTopLevel(MenuView *view)
|
||||
{
|
||||
uint16_t lcdSizeX = view->port->get_size_x();
|
||||
view->port->fill_rect(0, 0, lcdSizeX - 1, 32, view->port->get_color_back());
|
||||
MenuView_DrawMeitou(view, 16, 2);
|
||||
view->port->show_str(16, 20, (uint8_t *)"当前模式: 无模式");
|
||||
}
|
||||
|
||||
void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort)
|
||||
{
|
||||
view->port = renderPort;
|
||||
}
|
||||
|
||||
void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config)
|
||||
{
|
||||
(void)view;
|
||||
MenuLayout_PositionCal(menuCtrl->ptHead, menuCtrl->by0LevelNum, config);
|
||||
}
|
||||
|
||||
void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl)
|
||||
{
|
||||
uint8_t needFullRefresh = 0;
|
||||
uint8_t backColor = view->port->get_color_back();
|
||||
uint16_t lcdSizeX = view->port->get_size_x();
|
||||
uint16_t menuYMax = MENU_YMAX_FROM_LCD(view->port->get_size_y());
|
||||
|
||||
if (dspCtrl->bFirst)
|
||||
{
|
||||
dspCtrl->bFirst = 0;
|
||||
needFullRefresh = 1;
|
||||
}
|
||||
|
||||
if (menuCtrl->pt0Level != menuCtrl->ptRoute[0])
|
||||
{
|
||||
needFullRefresh = 1;
|
||||
view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor);
|
||||
menuCtrl->pt0Level = menuCtrl->ptRoute[0];
|
||||
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
|
||||
}
|
||||
else if (menuCtrl->ptCurBak != menuCtrl->ptCurrent)
|
||||
{
|
||||
if (menuCtrl->ptCurrent->byClass >= menuCtrl->ptCurBak->byClass)
|
||||
{
|
||||
MenuView_ShowOtherLevel(view, menuCtrl, menuCtrl->ptCurrent->byClass - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
view->port->fill_rect(0, MENU_YMIN, lcdSizeX - 1, menuYMax, backColor);
|
||||
needFullRefresh = 1;
|
||||
}
|
||||
menuCtrl->ptCurBak = menuCtrl->ptCurrent;
|
||||
}
|
||||
|
||||
if (needFullRefresh > 0)
|
||||
{
|
||||
MenuView_ShowTopLevel(view);
|
||||
for (uint8_t index = 0; index < menuCtrl->ptCurrent->byClass; index++)
|
||||
{
|
||||
MenuView_ShowOtherLevel(view, menuCtrl, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Drv/menu/view/menu_view.h
Normal file
17
src/Drv/menu/view/menu_view.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef MENU_VIEW_H
|
||||
#define MENU_VIEW_H
|
||||
|
||||
#include "menu_layout.h"
|
||||
#include "menu_render_port.h"
|
||||
#include "../common/menu_state_types.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const MenuRenderPort *port;
|
||||
} MenuView;
|
||||
|
||||
void MenuView_Init(MenuView *view, const MenuRenderPort *renderPort);
|
||||
void MenuView_Layout(MenuView *view, tagMenuCtrl *menuCtrl, const MenuLayoutConfig *config);
|
||||
void MenuView_RenderByState(MenuView *view, tagMenuCtrl *menuCtrl, tagDspCtrl *dspCtrl);
|
||||
|
||||
#endif
|
||||
36
src/common/utf8.c
Normal file
36
src/common/utf8.c
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "utf8.h"
|
||||
|
||||
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode)
|
||||
{
|
||||
unsigned char c = utf8[0];
|
||||
if (c == 0)
|
||||
{
|
||||
*out_unicode = 0;
|
||||
return 0;
|
||||
}
|
||||
if (c < 0x80)
|
||||
{
|
||||
*out_unicode = c;
|
||||
return 1;
|
||||
}
|
||||
if ((c & 0xE0) == 0xC0)
|
||||
{
|
||||
if (utf8[1] == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
*out_unicode = (uint32_t)((c & 0x1F) << 6 | (utf8[1] & 0x3F));
|
||||
return 2;
|
||||
}
|
||||
if ((c & 0xF0) == 0xE0)
|
||||
{
|
||||
if (utf8[1] == 0 || utf8[2] == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
*out_unicode = (uint32_t)((c & 0x0F) << 12 | (utf8[1] & 0x3F) << 6 | (utf8[2] & 0x3F));
|
||||
return 3;
|
||||
}
|
||||
*out_unicode = 0;
|
||||
return 0;
|
||||
}
|
||||
9
src/common/utf8.h
Normal file
9
src/common/utf8.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef UTF8_H
|
||||
#define UTF8_H
|
||||
|
||||
#include "types.h"
|
||||
|
||||
/* 解析 UTF-8 字节流中的一个字符,返回占用字节数(0 表示失败/结束)。 */
|
||||
uint8_t utf8_next(const unsigned char *utf8, uint32_t *out_unicode);
|
||||
|
||||
#endif
|
||||
10
src/main.c
10
src/main.c
@@ -1,4 +1,4 @@
|
||||
/* ============================================================================
|
||||
/* ============================================================================
|
||||
* main.c - PC 端 HMI 菜单主程序
|
||||
* 功能:菜单交互(主线程)+ TCP 服务器(独立线程),按 Q 退出
|
||||
* ========================================================================== */
|
||||
@@ -30,7 +30,7 @@ static int getch(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
#include "Drv/menu.h"
|
||||
#include "Drv/menu/app/menu.h"
|
||||
#include "TCP/tcp.h"
|
||||
#include "remoteDisplay.h"
|
||||
#include "Drv/key.h"
|
||||
@@ -63,7 +63,7 @@ int main(void)
|
||||
|
||||
printf("开始初始化菜单树...\n");
|
||||
|
||||
Menu_Init(); /* 初始化菜单树(运行界面/定值设置/查看数据等) */
|
||||
MenuApp_Init(); /* 初始化菜单应用(Model + View + Presenter) */
|
||||
Key_Init(); /* 初始化按键 */
|
||||
printf("PC 端 HMI 菜单模拟启动(TCP 服务在单独线程,端口 7003)。\n");
|
||||
/* 7003 为 RemoDispBus 默认端口,与 remo_disp_server.py 一致 */
|
||||
@@ -73,13 +73,13 @@ int main(void)
|
||||
}
|
||||
while(1)
|
||||
{
|
||||
Menu_Route();
|
||||
MenuApp_PollInput();
|
||||
Sleep(20);
|
||||
count++;
|
||||
if(count > 50)
|
||||
{
|
||||
count = 0;
|
||||
Menu_Show_Proc();
|
||||
MenuApp_Render();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ set(DTU_TEST_COMMON_SOURCES
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/display.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/remoteDisplay.c"
|
||||
@@ -20,12 +20,17 @@ set(DTU_TEST_COMMON_SOURCES
|
||||
# - add_test: 注册到 CTest,支持 ctest 统一执行
|
||||
function(add_dtu_test test_name)
|
||||
add_executable(${test_name} ${ARGN})
|
||||
target_sources(${test_name} PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/src/common/utf8.c"
|
||||
)
|
||||
# 测试目标可见的头文件目录
|
||||
target_include_directories(${test_name} PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/include"
|
||||
"${CMAKE_SOURCE_DIR}/src"
|
||||
"${CMAKE_SOURCE_DIR}/src/TCP"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd"
|
||||
"${CMAKE_SOURCE_DIR}/src/common"
|
||||
)
|
||||
# 平台差异:Windows 需要 ws2_32,非 Windows 使用 pthread
|
||||
if(WIN32)
|
||||
@@ -56,14 +61,6 @@ add_dtu_test(
|
||||
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
|
||||
)
|
||||
add_dtu_test(
|
||||
test_p0_utf8_next
|
||||
test_p0_utf8_next.c
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
|
||||
)
|
||||
add_dtu_test(
|
||||
test_p0_utf8_hz12_get
|
||||
test_p0_utf8_hz12_get.c
|
||||
@@ -89,12 +86,35 @@ add_dtu_test(
|
||||
add_dtu_test(
|
||||
test_p1_menu
|
||||
test_p1_menu.c
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/display.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
|
||||
)
|
||||
add_dtu_test(
|
||||
test_p1_menu_nav_legacy
|
||||
test_p1_menu_nav_legacy.c
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
|
||||
)
|
||||
add_dtu_test(
|
||||
test_p1_menu_navigator
|
||||
test_p1_menu_navigator.c
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
|
||||
)
|
||||
add_dtu_test(
|
||||
test_p1_menu_tree_builder
|
||||
test_p1_menu_tree_builder.c
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
|
||||
)
|
||||
add_dtu_test(
|
||||
test_p1_menu_layout
|
||||
test_p1_menu_layout.c
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
@@ -106,3 +126,21 @@ add_dtu_test(
|
||||
"${CMAKE_SOURCE_DIR}/src/TCP/tcp.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/thread_utils.c"
|
||||
)
|
||||
add_dtu_test(
|
||||
test_p2_menu_runtime_startup
|
||||
test_p2_menu_runtime_startup.c
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/app/menu.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/display.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_model.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_view.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_presenter.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/model/menu_tree_builder.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_layout.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/presenter/menu_navigator.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/menu/view/menu_renderer_lcd.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_draw.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/lcd_text.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/lcd/ascii.c"
|
||||
"${CMAKE_SOURCE_DIR}/src/Drv/key.c"
|
||||
)
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
/* -------------------------------------------------------------------------
|
||||
* 文件名: test_p0_utf8_hz12_get.c
|
||||
* 作用:
|
||||
* 验证中文点阵查询接口 utf8_hz12_get() 的基础行为是否正确。
|
||||
*
|
||||
* 被测接口:
|
||||
* const uint8_t* utf8_hz12_get(uint32_t unicode);
|
||||
*
|
||||
* 测试目标:
|
||||
* 1) 对于字库中存在的码点,应返回非 NULL 位图指针。
|
||||
* 2) 对于字库范围外或不存在的码点,应返回 NULL。
|
||||
*
|
||||
* 说明:
|
||||
* - 本测试只校验“是否命中”这一契约,不校验位图内容本身。
|
||||
* - 若后续需要,可新增逐字节位图校验测试。
|
||||
* ------------------------------------------------------------------------- */
|
||||
#include "../src/Drv/lcd/ascii.h"
|
||||
#include "test_common.h"
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* 测试入口:
|
||||
* 按“命中/低位未命中/高位未命中”三个场景依次断言。
|
||||
*
|
||||
* 返回值约定:
|
||||
* 0 -> 测试通过
|
||||
* 非0 -> 断言失败(由 ASSERT_* 宏返回)
|
||||
* ------------------------------------------------------------------------- */
|
||||
int main(void)
|
||||
{
|
||||
/* 命中用例:U+4F60(“你”)通常在当前字库中存在 */
|
||||
const uint8_t* hit = utf8_hz12_get(0x4F60u); /* 你 */
|
||||
/* 低位未命中:非常低的控制区码点,预期不在中文字库 */
|
||||
const uint8_t* miss_low = utf8_hz12_get(0x0001u);
|
||||
/* 高位未命中:超高码点,预期不在当前 12 点阵中文表中 */
|
||||
const uint8_t* miss_high = utf8_hz12_get(0x10FFFFu);
|
||||
|
||||
/* 命中必须返回有效指针 */
|
||||
ASSERT_TRUE(hit != NULL);
|
||||
/* 非命中必须返回 NULL */
|
||||
ASSERT_TRUE(miss_low == NULL);
|
||||
ASSERT_TRUE(miss_high == NULL);
|
||||
return 0;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#include "../src/Drv/lcd/lcd_text.h"
|
||||
#include "test_common.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint32_t unicode = 0;
|
||||
const unsigned char ascii[] = "A";
|
||||
const unsigned char u2[] = {0xC2, 0xA2, 0x00}; /* U+00A2 */
|
||||
const unsigned char u3[] = {0xE4, 0xBD, 0xA0, 0x00}; /* U+4F60 */
|
||||
const unsigned char invalid[] = {0xF0, 0x00}; /* 4-byte start not supported */
|
||||
const unsigned char truncated2[] = {0xC2, 0x00};
|
||||
const unsigned char truncated3[] = {0xE4, 0xBD, 0x00};
|
||||
|
||||
ASSERT_EQ_INT(1, utf8_next(ascii, &unicode));
|
||||
ASSERT_EQ_U32(0x41u, unicode);
|
||||
|
||||
ASSERT_EQ_INT(2, utf8_next(u2, &unicode));
|
||||
ASSERT_EQ_U32(0x00A2u, unicode);
|
||||
|
||||
ASSERT_EQ_INT(3, utf8_next(u3, &unicode));
|
||||
ASSERT_EQ_U32(0x4F60u, unicode);
|
||||
|
||||
ASSERT_EQ_INT(0, utf8_next(invalid, &unicode));
|
||||
ASSERT_EQ_INT(0, utf8_next(truncated2, &unicode));
|
||||
ASSERT_EQ_INT(0, utf8_next(truncated3, &unicode));
|
||||
return 0;
|
||||
}
|
||||
@@ -1,28 +1,47 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "test_common.h"
|
||||
#include "../src/Drv/menu.c"
|
||||
|
||||
#include "../src/Drv/menu/view/menu_layout.h"
|
||||
#include "../src/Drv/menu/model/menu_tree_builder.h"
|
||||
|
||||
static int noop_proc(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
tagMenuCtrl ctrl;
|
||||
tagMenuItem items[4];
|
||||
uint8_t menu_num[4] = {0};
|
||||
tagPMenuItem first[4] = {0};
|
||||
tagPMenuItem index[4] = {0};
|
||||
uint8_t max_len;
|
||||
|
||||
Menu_Init();
|
||||
ASSERT_TRUE(g_tMenuCtrl.by0LevelNum > 0);
|
||||
ASSERT_TRUE(g_tMenuCtrl.ptHead != NULL);
|
||||
ASSERT_TRUE(g_tMenuCtrl.ptCurrent != NULL);
|
||||
const tagMenuModel model[4] = {
|
||||
{0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
{1, "设置", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
{2, "子项", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
{1, "查看", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
};
|
||||
|
||||
ASSERT_EQ_INT(3, utf8_len_cal((uint8_t*)"ABC"));
|
||||
ASSERT_EQ_INT(2, utf8_len_cal((uint8_t*)"你"));
|
||||
memset(&ctrl, 0, sizeof(ctrl));
|
||||
memset(items, 0, sizeof(items));
|
||||
|
||||
first[0] = g_tMenuCtrl.ptHead;
|
||||
index[0] = g_tMenuCtrl.ptHead;
|
||||
max_len = Menu_charLenCal(0, menu_num, first, index);
|
||||
MenuTree_0LevelNumCal(&ctrl, model, 4);
|
||||
ASSERT_EQ_INT(1, ctrl.by0LevelNum);
|
||||
MenuTree_MainCreate(items, model, 4);
|
||||
|
||||
ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC"));
|
||||
ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)"你"));
|
||||
|
||||
first[0] = &items[0];
|
||||
index[0] = &items[0];
|
||||
max_len = MenuLayout_CharLenCal(0, menu_num, first, index);
|
||||
ASSERT_TRUE(max_len > 0);
|
||||
ASSERT_TRUE(menu_num[1] > 0);
|
||||
ASSERT_STREQ("设置", (const char *)items[1].byName);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
47
tests/test_p1_menu_layout.c
Normal file
47
tests/test_p1_menu_layout.c
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "test_common.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "../src/Drv/lcd/lcd.h"
|
||||
#include "../src/Drv/menu/app/menu.h"
|
||||
#include "../src/Drv/menu/view/menu_layout.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
tagMenuItem root;
|
||||
tagMenuItem child;
|
||||
MenuLayoutConfig config = {
|
||||
LCD_SIZE_X,
|
||||
LCD_SIZE_Y,
|
||||
MENU_YMIN,
|
||||
MENU_YMAX_FROM_LCD(LCD_SIZE_Y),
|
||||
LINE_HEIGHT,
|
||||
MENU_WITDTH,
|
||||
MENU_XADD,
|
||||
MENU_YADD};
|
||||
|
||||
memset(&root, 0, sizeof(root));
|
||||
memset(&child, 0, sizeof(child));
|
||||
|
||||
memcpy(root.byName, "Root", 5);
|
||||
memcpy(child.byName, "Child", 6);
|
||||
root.byClass = 0;
|
||||
child.byClass = 1;
|
||||
|
||||
root.ptLower = &child;
|
||||
root.ptBehind = &root;
|
||||
root.ptBefore = &root;
|
||||
child.ptHigher = &root;
|
||||
child.ptBehind = &child;
|
||||
child.ptBefore = &child;
|
||||
|
||||
ASSERT_EQ_INT(3, MenuLayout_Utf8LenCal((uint8_t *)"ABC"));
|
||||
ASSERT_EQ_INT(2, MenuLayout_Utf8LenCal((uint8_t *)"你"));
|
||||
|
||||
MenuLayout_PositionCal(&root, 1, &config);
|
||||
ASSERT_TRUE(root.wEPosX > root.wSPosX);
|
||||
ASSERT_TRUE(root.wEPosY > root.wSPosY);
|
||||
ASSERT_TRUE(root.wEPosY <= LCD_SIZE_Y);
|
||||
|
||||
return 0;
|
||||
}
|
||||
93
tests/test_p1_menu_nav_legacy.c
Normal file
93
tests/test_p1_menu_nav_legacy.c
Normal file
@@ -0,0 +1,93 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "test_common.h"
|
||||
|
||||
#include "../src/Drv/key.h"
|
||||
#include "../src/Drv/menu/presenter/menu_navigator.h"
|
||||
|
||||
static int g_exec_count = 0;
|
||||
|
||||
static int on_exec(void)
|
||||
{
|
||||
g_exec_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void build_legacy_like_tree(MenuNavState *nav, tagMenuItem *root, tagMenuItem *m1, tagMenuItem *m2, tagMenuItem *m1_sub)
|
||||
{
|
||||
memset(nav, 0, sizeof(*nav));
|
||||
memset(root, 0, sizeof(*root));
|
||||
memset(m1, 0, sizeof(*m1));
|
||||
memset(m2, 0, sizeof(*m2));
|
||||
memset(m1_sub, 0, sizeof(*m1_sub));
|
||||
|
||||
root->byClass = 0;
|
||||
root->ptLower = m1;
|
||||
root->ptBefore = root;
|
||||
root->ptBehind = root;
|
||||
root->wPos = 1;
|
||||
|
||||
m1->byClass = 1;
|
||||
m1->wPos = 1;
|
||||
m1->ptHigher = root;
|
||||
m1->ptBefore = m2;
|
||||
m1->ptBehind = m2;
|
||||
m1->ptLower = m1_sub;
|
||||
m1->pfnWinProc = on_exec;
|
||||
|
||||
m2->byClass = 1;
|
||||
m2->wPos = 2;
|
||||
m2->ptHigher = root;
|
||||
m2->ptBefore = m1;
|
||||
m2->ptBehind = m1;
|
||||
m2->pfnWinProc = on_exec;
|
||||
|
||||
m1_sub->byClass = 2;
|
||||
m1_sub->wPos = 1;
|
||||
m1_sub->ptHigher = m1;
|
||||
m1_sub->ptBefore = m1_sub;
|
||||
m1_sub->ptBehind = m1_sub;
|
||||
m1_sub->pfnWinProc = on_exec;
|
||||
|
||||
nav->ptHead = root;
|
||||
nav->ptCurrent = m1;
|
||||
nav->ptCurBak = m1;
|
||||
nav->ptRoute[0] = root;
|
||||
nav->ptRoute[1] = m1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
MenuNavState nav;
|
||||
tagMenuItem root;
|
||||
tagMenuItem m1;
|
||||
tagMenuItem m2;
|
||||
tagMenuItem m1_sub;
|
||||
MenuNavResult result;
|
||||
|
||||
build_legacy_like_tree(&nav, &root, &m1, &m2, &m1_sub);
|
||||
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_D);
|
||||
ASSERT_EQ_INT(1, result.needRefresh);
|
||||
ASSERT_TRUE(nav.ptCurrent == &m2);
|
||||
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_U);
|
||||
ASSERT_EQ_INT(1, result.needRefresh);
|
||||
ASSERT_TRUE(nav.ptCurrent == &m1);
|
||||
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
|
||||
ASSERT_EQ_INT(1, result.needRefresh);
|
||||
ASSERT_TRUE(nav.ptCurrent == &m1_sub);
|
||||
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_ESC);
|
||||
ASSERT_EQ_INT(1, result.needRefresh);
|
||||
ASSERT_TRUE(nav.ptCurrent == &m1);
|
||||
|
||||
g_exec_count = 0;
|
||||
nav.ptCurrent = &m2;
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
|
||||
ASSERT_EQ_INT(0, result.needRefresh);
|
||||
ASSERT_EQ_INT(1, g_exec_count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
79
tests/test_p1_menu_navigator.c
Normal file
79
tests/test_p1_menu_navigator.c
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "test_common.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "../src/Drv/key.h"
|
||||
#include "../src/Drv/menu/presenter/menu_navigator.h"
|
||||
|
||||
static int g_exec_count = 0;
|
||||
|
||||
static int on_exec(void)
|
||||
{
|
||||
g_exec_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void build_two_level(MenuNavState *nav, tagMenuItem *root, tagMenuItem *child_a, tagMenuItem *child_b)
|
||||
{
|
||||
memset(nav, 0, sizeof(*nav));
|
||||
memset(root, 0, sizeof(*root));
|
||||
memset(child_a, 0, sizeof(*child_a));
|
||||
memset(child_b, 0, sizeof(*child_b));
|
||||
|
||||
root->byClass = 0;
|
||||
root->wPos = 1;
|
||||
root->ptLower = child_a;
|
||||
root->ptBefore = root;
|
||||
root->ptBehind = root;
|
||||
|
||||
child_a->byClass = 1;
|
||||
child_a->wPos = 1;
|
||||
child_a->ptHigher = root;
|
||||
child_a->ptBefore = child_b;
|
||||
child_a->ptBehind = child_b;
|
||||
child_a->pfnWinProc = on_exec;
|
||||
|
||||
child_b->byClass = 1;
|
||||
child_b->wPos = 2;
|
||||
child_b->ptHigher = root;
|
||||
child_b->ptBefore = child_a;
|
||||
child_b->ptBehind = child_a;
|
||||
child_b->pfnWinProc = on_exec;
|
||||
|
||||
nav->ptHead = root;
|
||||
nav->ptCurrent = child_a;
|
||||
nav->ptCurBak = child_a;
|
||||
nav->ptRoute[0] = root;
|
||||
nav->ptRoute[1] = child_a;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
MenuNavState nav;
|
||||
tagMenuItem root;
|
||||
tagMenuItem a;
|
||||
tagMenuItem b;
|
||||
MenuNavResult result;
|
||||
|
||||
build_two_level(&nav, &root, &a, &b);
|
||||
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_D);
|
||||
ASSERT_EQ_INT(1, result.needRefresh);
|
||||
ASSERT_TRUE(nav.ptCurrent == &b);
|
||||
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_U);
|
||||
ASSERT_EQ_INT(1, result.needRefresh);
|
||||
ASSERT_TRUE(nav.ptCurrent == &a);
|
||||
|
||||
g_exec_count = 0;
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_ENT);
|
||||
ASSERT_EQ_INT(0, result.needRefresh);
|
||||
ASSERT_EQ_INT(1, g_exec_count);
|
||||
|
||||
result = MenuNavigator_ProcessKey(&nav, KEY_ESC);
|
||||
ASSERT_EQ_INT(1, result.skipRenderThisRound);
|
||||
ASSERT_TRUE(nav.ptCurrent == root.ptLower);
|
||||
ASSERT_TRUE(nav.ptRoute[0] == &root);
|
||||
|
||||
return 0;
|
||||
}
|
||||
37
tests/test_p1_menu_tree_builder.c
Normal file
37
tests/test_p1_menu_tree_builder.c
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "test_common.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "../src/Drv/menu/model/menu_tree_builder.h"
|
||||
|
||||
static int noop_proc(void)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
tagMenuCtrl ctrl;
|
||||
tagMenuItem items[4];
|
||||
const tagMenuModel model[4] = {
|
||||
{0, "Root", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
{1, "A", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
{1, "B", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
{2, "C", "", 0, 0, 0, (FUNCPTR)noop_proc},
|
||||
};
|
||||
|
||||
memset(&ctrl, 0, sizeof(ctrl));
|
||||
memset(items, 0, sizeof(items));
|
||||
|
||||
MenuTree_0LevelNumCal(&ctrl, model, 4);
|
||||
ASSERT_EQ_INT(1, ctrl.by0LevelNum);
|
||||
|
||||
MenuTree_MainCreate(items, model, 4);
|
||||
ASSERT_TRUE(items[0].ptLower == &items[1]);
|
||||
ASSERT_TRUE(items[1].ptBehind == &items[2]);
|
||||
ASSERT_TRUE(items[2].ptBefore == &items[1]);
|
||||
ASSERT_TRUE(items[2].ptLower == &items[3]);
|
||||
ASSERT_TRUE(items[3].ptHigher == &items[2]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
39
tests/test_p2_menu_runtime_startup.c
Normal file
39
tests/test_p2_menu_runtime_startup.c
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "test_common.h"
|
||||
|
||||
#include "../src/Drv/key.h"
|
||||
#include "../src/Drv/menu/app/menu.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int decorated_found = 0;
|
||||
uint16_t itemCount = 0;
|
||||
const tagMenuItem *menuItems;
|
||||
|
||||
MenuApp_Init();
|
||||
Key_Init();
|
||||
menuItems = MenuApp_GetMenuItems(&itemCount);
|
||||
|
||||
for (uint16_t i = 0; i < itemCount; i++)
|
||||
{
|
||||
if (menuItems[i].ptLower != NULL)
|
||||
{
|
||||
uint8_t len = 0;
|
||||
while ((len < 50) && (menuItems[i].byName[len] != '\0'))
|
||||
{
|
||||
len++;
|
||||
}
|
||||
ASSERT_TRUE(len > 0);
|
||||
ASSERT_EQ_INT('\x10', menuItems[i].byName[len - 1]);
|
||||
decorated_found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(decorated_found == 1);
|
||||
|
||||
/* 首次路由应仅触发首帧绘制,不应崩溃 */
|
||||
MenuApp_PollInput();
|
||||
|
||||
/* 二次刷新路径也不应崩溃 */
|
||||
MenuApp_Render();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user