Files
DTU-HMI/src/Drv/menu/view/menu_layout.c

448 lines
17 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.
#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];
}
}