#include #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]; } }