Files
Just-DataInspector/Components/Shared/Inc/PopupMenu.hpp
T
DeepSeek V4.1 Flash 71195c4c48 新增主题配色(4 套)+ 信号右键菜单精简与「克隆该信号」+ 布局边界层级根治 + 单点信号吸附修复
## 为什么

用户 2026-10-29 报告/要求五件事:

1. 派生信号的「时间轴偏移保留在样式上、其它调整烘值」维持现状(确认无需改动);
2. 信号名右键菜单里很多功能已被下方样式自定义区覆盖 ⇒ 只保留「仅显示该信号」,
   并在它**上方**新增「克隆该信号」;
3. 设置菜单新增「主题配色」二级菜单(带 √ 状态),顺序 Light / Solarised Light /
   Dark / Solarised Dark,默认主题维持现状;
4. 布局边界:中键平移视界时浮动面板上的边界**闪烁**;光标取值胶囊的**文字**框会**穿透**
   到浮动面板上;并请核对网格内固定构件(图例 / Y 轴标尺 / GX 标签 / 回到最上方 /
   浮动面板)的层级关系;
5. 单数据点信号在拖动光标时的**吸附**功能消失。

## 根因

### 布局边界层级(两个都很隐蔽)

- **文本层号写死**:文本是在绘制过程中由 `UiTextRectSink` 收集的,而宿主把所有文本都记在
  固定层 2;组件几何层却是"组件表下标"(`PlotView` = 0)。面板层 = PlotView 层 + 1 = 1,
  **低于**文本层 2 ⇒ 面板的遮挡矩形挖不到文本 ⇒ 胶囊文字画在面板表面。
- **面板像素缓存**:面板"先画进图层、命中缓存就贴图",贴图那一帧不画字 ⇒ 文本框凭空消失
  ⇒ 面板上的边界闪烁。
- 顺带发现 `PlotView::BuildBoxes` 用 `PushLayer()` 却**从不复位**,会把宿主后续所有组件的
  层号整体推高。

### 单点信号吸附

`MinorCellSparse` 在"把判定范围收窄到数据实际时间跨度"的分支里有
`if (!(dataMax > dataMin)) return false;`。单个有效采样点时 `dataMax == dataMin`,
于是被判定成"不稀疏" ⇒ 不吸附。

## 改了什么

### 1. 布局边界层级

- `UiTextRectSink` 新增 `OnTextLayer(int)` / `TextLayer()`;新增 `UiSetTextLayer` /
  `UiGetTextLayer`;`UiBoxTable` 实现为改 `m_layer`,并暴露 `Layer()`。
- `Application::Render` 在绘制每个组件前下发 `UiSetTextLayer(ComponentLayer(&component))`,
  浮层用 `OverlayLayer(1/2)`;新增 `ComponentLayer()` / `OverlayLayer()` /
  `kLayersPerComponent = 3`,与 `PaintLayoutBounds` 的层号**完全同源**。
- `PaintLayoutBounds` 改为**显式** `SetLayer(下标 × 3)`(不再依赖 `PushLayer` 累加)。
- `PlotView`:`BuildBoxes` 用 `baseLayer + 1` 放浮动面板(并登记遮挡)、
  `baseLayer + 2` 放绘制在面板之上的滚动条 / 回到最上方 / 输入框,结束**复位**层号;
  `paintOverlays` 同步分两次抬高文本层。
- 面板像素图层新增 `PanelSlot::textRects` + 转发型收集器 `PanelTextCollector`:
  渲染时存文本框,命中缓存贴图时回放 ⇒ 几何与缓存命中无关(消除闪烁)。

### 2. 信号右键菜单 + 克隆

- 菜单只剩:标题、**克隆该信号**、**仅显示该信号**;删除显示/隐藏、显示全部/隐藏全部、
  预设色块行、样式三项,以及对应的 `CMD_SIGNAL_TOGGLE/SHOW_ALL/HIDE_ALL`、
  `CMD_COLOR_BASE`、`CMD_MODE_BASE` 及其分支(死代码)。
- 新增 `CMD_SIGNAL_CLONE`(post `WM_APP + 5`)→ `Application::CloneSignal`:
  把「调整」(系数 / 绝对值 / 镜像 / Y 偏移)烘进采样值、**时间轴偏移保留在样式上**,
  名称前置 `*`,插入到源信号**正下方**,并**保留原外观**(颜色/透明度/线宽/绘制样式)。
- 抽出 `Application::InsertSignalBelowSource(...)` 供派生与克隆共用。

### 3. 主题配色

- `ThemeKind{LIGHT, SOLARIZED_LIGHT, DARK, SOLARIZED_DARK}` + `ThemeFor()` / `ThemeName()`;
  Light 取 VS Code 2026 默认浅色、Dark 取 Dark+、Solarized Dark 取 VS Code solarized dark。
  **默认仍是 Solarised Light**。
- 新增 `CurrentTheme()` / `SetCurrentTheme()`:宿主交出自己的 `m_theme` 地址,
  切换主题只改内容;`UiControls` 里没有 `Theme` 形参的原语改用它(原来硬编码
  `SolarizedLight()`,换主题不生效)。
- `Application::ApplyTheme()`:改配色 → `PlotView::OnThemeChanged()`(作废场景/宽基底/
  装饰/光晕/面板图层)+ `SignalPanel::OnThemeChanged()`(作废行位图)→ 整窗重绘。
- 菜单用 `SUBMENU` + 4 个 `keepOpen` 子项;新增 `PopupMenu::UpdateSubmenuItemLabel()`
  回写勾选,并让子菜单分支尊重 `keepOpen`(原为无条件关闭)。

### 4. 单点信号吸附

`MinorCellSparse` 改为只在 `dataMax < dataMin` 时放弃;相等时按"那一个小格"判定
(`need = 5 > finiteCount = 1` ⇒ 稀疏 ⇒ 吸附恢复)。

### 5. 审计

`Makefile` 的 `LAYOUT_BOUNDS` / `JDI_LAYOUT_BOUNDS` **从未被任何 `#if` 引用**
(`LAYOUT_BOUNDS=0` 产物字节数完全相同),属名实不符的死开关 ⇒ 删除并注明原因。

## 怎么验证

新增两支离屏探针(不占鼠标 / 键盘 / 屏幕):

```
.preflight/round15check  28 passed, 0 failed
  单点吸附 7 条(含"远处不吸附")、面板层级 5 条、面板文本框跨帧与中键平移稳定 2 条、
  克隆 14 条(烘值逐点比对 / 名称前缀 / 偏移与外观保留 / 调整复位)
.preflight/themecheck    17 passed, 0 failed
  4 套主题可读性与可区分、默认 = Solarised Light、逐主题换色且无旧底色残留、
  切回默认逐像素还原(diff = 0)、子菜单 4 项 / 顺序 / 唯一勾选 / 命令切换 / 菜单保持打开
```

回归全绿:

```
uicheck 69/69   lbcheck 21/21  lbresid 5/5   lbzorder 4/4
lbcovr   7/7    lbpanel 10/10  derivecheck 16/16
rendercheck: 8 个场景哈希与上一轮逐一相同 + FUNC 7/7, exit=0
  A FAED59DF284153DB  B E6443BFE4E3955F8  C 96A0AA8C402F1729  D E3D5DBD998E43720
  E DA0583CB13C26A6F  F FFB822159F604D1D  G 19F8DE8FE3A8DF18  H DBBDDE4CB2C97C92
```

`make release` / `make debug` 零 error / 零 warning;产物 **791,552 B**
(单一 exe,未拆 DLL,未改 `-Oz`;离 1MB 预警线约 250 KB)。
2026-10-29 20:38:55 +08:00

576 lines
21 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.
#ifndef __POPUP_MENU_HPP__
#define __POPUP_MENU_HPP__
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <cstdint>
#include <string>
#include <vector>
#include "PaintUtil.hpp"
#include "TextEdit.hpp"
#include "Theme.hpp"
namespace jdi
{
class UiBoxTable;
/**
* @brief 弹出菜单的打开来源,用于把选择结果派发回正确的组件。
*/
enum class MenuScope : int
{
NONE = 0, /**< 无来源。 */
SIGNAL_PANEL = 1, /**< 信号面板。 */
TAB_BAR = 2, /**< 标签栏。 */
PLOT_VIEW = 3, /**< 波形绘图区。 */
APP_SETTINGS = 4 /**< 应用设置菜单(工具栏齿轮按钮):由宿主自己处理,不下发到组件。 */
};
/**
* @brief 弹出菜单项的显示类型。
*/
enum class MenuItemKind : int
{
NORMAL = 0, /**< 普通可点击项。 */
SEPARATOR = 1, /**< 分隔线。 */
HEADER = 2, /**< 分组标题(不可点击)。 */
SWATCHES = 3, /**< 颜色色块行。 */
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
SUBMENU = 5, /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
HEADER_ACTION = 6 /**< 分组标题 + **右侧图标按钮**(如「还原」),图标可点击。 */
};
/**
* @brief 单个弹出菜单项。
*/
struct MenuItem
{
MenuItemKind kind = MenuItemKind::NORMAL; /**< 项目类型。 */
std::wstring label; /**< 显示文本;EDIT 项为输入框左侧的说明文字。 */
int id = 0; /**< 命令标识。 */
bool enabled = true; /**< 是否可用。 */
bool checked = false; /**< 是否处于选中(单选)状态。 */
int swatchCount = 0; /**< 色块数量(仅色块行有效)。 */
std::uint32_t swatches[16] = {}; /**< 色块颜色。 */
int checkedSwatch = -1; /**< 被选中的色块下标。 */
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
/**
* EDIT 项**进入编辑时的初始文本**;为空则使用 `value`。
*
* 用于「非编辑态的显示文本」与「编辑态的数值」不一致的场合,例如时间轴偏移:
* 平时显示 `Δt:5s 60ms`,进入编辑后应显示以秒为单位的数值 `5.06`。
*/
std::wstring editValue;
/**
* EDIT 项**编辑态**在输入框最右侧提示的单位文本(如 `s`);为空则不提示。
*
* 非编辑态不显示(那时显示的是 `value` 的完整文案)。
*/
std::wstring editSuffix;
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
/**
* 普通项被点击后是否**保持菜单打开**。
*
* 用于「带复选框的切换项」(如镜像的水平/垂直):可以连续勾选多项,
* 勾选状态通过 `UpdateItemLabel` 回写。默认 false(点击即关):与既有行为一致。
*/
bool keepOpen = false;
};
/**
* @brief 打开菜单时的附加布局约束。
*/
struct MenuLayoutConstraint
{
/**
* @brief 菜单允许的最大高度(**客户区像素**,0 = 不限制)。
*
* 超过该高度时菜单不再变高:**前导标题钉在顶部、最后一个分隔线及其之后的条目固定在底部**,
* 中间的内容成为可滚动的条目区(鼠标滚轮 / 键盘方向键 / 滚动条)。
*
* 用于「条目数不定」的菜单:例如加载含上百个信号的 MF4 并把每个信号都放进独立网格时,
* 「所属网格」菜单会有上百个条目(既超出屏幕、绘制开销也很大)。
*/
float maxHeight = 0.0f;
/**
* @brief 可滚动条目区**最多显示的标准行数**(0 = 不限制)。
*
* 比 `maxHeight` 更直观(用户要求“保留 10 条网格的上限”):无论条目多高,
* 滚动视口高度都不会超过该行数×标准行高。与 `maxHeight` 同时给出时**取更严格的那个**。
*/
int maxScrollRows = 0;
};
/**
* @brief 菜单选择结果。
*/
struct MenuResult
{
bool valid = false; /**< 是否产生了有效选择。 */
MenuScope scope = MenuScope::NONE; /**< 来源组件。 */
int id = 0; /**< 被选中项的命令标识。 */
int swatchIndex = -1; /**< 被选中的色块下标(非色块行为 -1)。 */
gd::RectF itemRect; /**< 被点击条目的矩形(客户区坐标),供就近弹出输入框。 */
bool editCommit = false; /**< 该结果来自菜单内联输入框的提交(菜单保持打开)。 */
std::wstring editText; /**< 提交的输入文本(editCommit 为 true 时有效)。 */
};
/**
* @brief 与界面主题一致的自绘弹出菜单。
*
* 菜单不创建独立窗口,而是作为宿主窗口绘制流程的最上层内容直接绘制到后备缓冲区,
* 因此外观与主界面完全统一,不会出现系统默认菜单样式。
*/
class PopupMenu
{
public:
PopupMenu();
/**
* @brief 绑定字体与主题资源。
* @param [IN] fonts 字体集合,需在菜单生命周期内保持有效。
* @param [IN] theme 主题配色,需在菜单生命周期内保持有效。
*/
void Setup(const UiFonts* fonts, const Theme* theme);
/**
* @brief 设置 DPI 缩放比例。
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
*/
void SetScale(float scale);
/**
* @brief 在指定位置打开菜单。
* @param [IN] scope 来源组件标识。
* @param [IN] bounds 宿主客户区矩形,用于把菜单限制在窗口内。
* @param [IN] x 锚点横坐标(客户区坐标)。
* @param [IN] y 锚点纵坐标(客户区坐标)。
* @param [IN] items 菜单项集合。
* @param [IN] expandUp 为 true 时菜单**向上展开**,且 `(x, y)` 是菜单的**左下角**
* (用于“二级面板贴在按钮左上方”这类锚定);为 false(默认)时向右下展开,
* `(x, y)` 是菜单的左上角。
* @param [IN] constraint 附加布局约束(最大高度 / 超长时可滚动),默认不限制。
*/
void Open(MenuScope scope, const gd::RectF& bounds, int x, int y,
const std::vector<MenuItem>& items, bool expandUp = false,
const MenuLayoutConstraint& constraint = MenuLayoutConstraint());
/**
* @brief 关闭菜单。
*/
void Close();
/**
* @brief 判断菜单是否处于打开状态。
* @return 打开返回 true。
*/
bool IsOpen() const { return m_open; }
/**
* @brief 获取当前菜单的来源组件标识。
* @return 来源标识。
*/
MenuScope Scope() const { return m_scope; }
/**
* @brief 绘制菜单(应在宿主绘制的最后一步调用)。
* @param [IN,OUT] g 绘图上下文。
*/
void Paint(gd::Graphics& g);
/**
* @brief 处理鼠标移动。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 需要重绘返回 true。
*/
bool OnMouseMove(int x, int y);
/**
* @brief 处理鼠标左键按下。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] result 在菜单外按下时返回被取消的选择结果。
* @return 事件已被菜单消费返回 true。
*/
bool OnLButtonDown(int x, int y, MenuResult& result);
/**
* @brief 处理鼠标左键抬起。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] result 选择结果。
* @return 事件已被菜单消费返回 true。
*/
bool OnLButtonUp(int x, int y, MenuResult& result);
/**
* @brief 处理键盘按键。
* @param [IN] key 虚拟键码。
* @param [OUT] result 选择结果。
* @return 事件已被菜单消费返回 true。
*/
bool OnKeyDown(WPARAM key, MenuResult& result);
/**
* @brief 判断坐标是否落在菜单内部。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 落在菜单内返回 true。
*/
bool ContainsPoint(int x, int y) const;
/**
* @brief 菜单整体矩形(客户区坐标;未打开时为空矩形)。
*
* 只读用途:宿主的「显示布局边界」需要把浮层区域也上报出来。
*/
const gd::RectF& Rect() const { return m_rect; }
/**
* @brief 收集菜单与其内部结构的布局边界(调试用)。
*
* 菜单未打开时什么都不登记。条目、滚动区、滚动条与二级子菜单各自成为独立的 box。
* @param [IN,OUT] boxes 收集器。
*/
void BuildBoxes(UiBoxTable& boxes) const;
/**
* @brief 菜单是否因内容过长而启用了滚动(条目区可滚动、前导标题与尾部条目固定)。
* @return 启用滚动返回 true。
*/
bool IsScrollable() const { return m_open && m_scrollEnabled; }
/**
* @brief 滚动条**轨道**矩形(仅可滚动菜单有效,其余返回空矩形)。
*
* 同时也是“可滚动条目区”的依据:条目区内的条目只在该矩形内可点。
*/
const gd::RectF& ScrollBarRect() const { return m_scrollBarRect; }
/**
* @brief 滚动条**滑块**矩形(仅可滚动菜单有效)。
* @return 滑块矩形;无需滚动时返回空矩形。
*/
gd::RectF ScrollBarThumbRect() const;
/**
* @brief 由「滑块的顶部 Y」反推对应的滚动偏移(拖拽滚动条 / 点击轨道时用)。
* @param [IN] thumbTop 期望的滑块顶部 Y(客户区坐标)。
* @return 滚动偏移(已夹到合法范围)。
*/
float ScrollOffsetForThumbTop(float thumbTop) const;
/**
* @brief 条目区当前的滚动偏移(仅可滚动菜单有效)。
* @return 偏移量(像素,0 = 顶部)。
*/
float ScrollOffset() const { return m_scrollOffset; }
/**
* @brief 可滚动条目区在菜单条目中的下标范围(半开区间 `[first, last)`)。
*
* 前导标题(`HEADER` / `HEADER_ACTION`)与末尾分隔线及其之后的条目**不参与滚动**。
* @param [OUT] first 条目区首个条目下标。
* @param [OUT] last 条目区末尾(不含)下标。
* @return 菜单启用滚动返回 true;否则输出全区间。
*/
bool ScrollRegion(int& first, int& last) const;
/**
* @brief 处理鼠标滚轮:滚动可滚动菜单的条目区。
* @param [IN] delta 滚轮增量(`WM_MOUSEWHEEL` 的 `GET_WHEEL_DELTA_WPARAM`)。
* @return 滚动位置发生变化返回 true。
*/
bool OnMouseWheel(int delta);
/**
* @brief 设置剪贴板所有者窗口句柄(供内联输入框复制/粘贴)。
* @param [IN] host 宿主窗口句柄,允许为空。
*/
void SetHostWindow(HWND host) { m_host = host; }
/**
* @brief 判断是否有菜单项正处于内联编辑状态。
* @return 处于编辑状态返回 true。
*/
bool IsEditing() const { return m_editIndex >= 0; }
/**
* @brief 判断坐标是否落在当前正在编辑的输入框内。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 落在输入框内返回 true。
*/
bool IsOnEditField(int x, int y) const;
/**
* @brief 编辑态下按下左键:定位插入符并开始拖拽框选。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中输入框返回 true。
*/
bool OnEditMouseDown(int x, int y);
/**
* @brief 编辑态下拖拽鼠标:扩展选区。
* @param [IN] x 客户区横坐标。
* @return 选区发生变化返回 true。
*/
bool OnEditMouseMove(int x);
/**
* @brief 编辑态下松开左键:结束拖拽框选。
* @return 之前处于拖拽中返回 true。
*/
bool OnEditMouseUp();
/**
* @brief 编辑态下处理字符输入。
* @param [IN] character 字符。
* @param [OUT] result 提交时返回 editCommit 结果。
* @return 需要重绘返回 true。
*/
bool OnChar(wchar_t character, MenuResult& result);
/**
* @brief 更新指定命令项显示的值文本(菜单保持打开,立即重新排布)。
* @param [IN] id 命令标识。
* @param [IN] value 新的值文本(非编辑态显示)。
* @param [IN] editValue 新的**编辑初值**(为空时保持原值)。
* ⚠️ 非编辑态文本与编辑态文本不一致的行(如时间轴偏移:`Δt:1h 1min 2s` ↔ `3662`)
* 必须一并更新,否则下次进入编辑会拿到陈旧的初值。
* @return 找到对应项并更新返回 true。
*/
bool UpdateItemValue(int id, const std::wstring& value,
const std::wstring& editValue = std::wstring());
/**
* @brief 更新指定普通项的文字与可用/选中状态(用于实时反映状态)。
* @param [IN] id 命令标识。
* @param [IN] label 新的文字。
* @param [IN] checked 新的选中状态。
* @param [IN] enabled 新的可用状态。
* @return 找到对应项并更新返回 true。
*/
bool UpdateItemLabel(int id, const std::wstring& label, bool checked = false, bool enabled = true);
/**
* @brief 更新**二级子菜单**里某个普通项的文字与勾选状态。
*
* 子菜单内容是 `MenuItem::submenu` 的快照,改完必须重建子菜单布局才会生效
*(这正是「主题配色」能边点边看到 √ 挪位的原因)。
*
* @param [IN] id 命令标识。
* @param [IN] label 新的文字。
* @param [IN] checked 新的选中状态。
* @param [IN] enabled 新的可用状态。
* @return 找到对应项并更新返回 true。
*/
bool UpdateSubmenuItemLabel(int id, const std::wstring& label, bool checked = false,
bool enabled = true);
private:
/**
* @brief 计算单个菜单项的纵向高度。
* @param [IN] item 菜单项。
* @return 高度(像素)。
*/
float ItemHeight(const MenuItem& item) const;
/**
* @brief 依据当前项目集合计算整体矩形与各项矩形。
* @param [IN] bounds 宿主客户区矩形。
* @param [IN] x 锚点横坐标。
* @param [IN] y 锚点纵坐标。
* @param [IN] expandUp 为 true 时以 `(x, y)` 为**左下角**向上展开。
*/
void BuildLayout(const gd::RectF& bounds, int x, int y, bool expandUp);
/**
* @brief 查询坐标命中的菜单项下标。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] swatchIndex 命中的色块下标(非色块行为 -1)。
* @return 项目下标,未命中返回 -1。
*/
int ItemAtPoint(int x, int y, int& swatchIndex) const;
/**
* @brief 将悬停项移动到下一个可用项。
* @param [IN] step 步进(+1 向下,-1 向上)。
*/
void MoveHover(int step);
/**
* @brief 确认当前悬停项并生成结果。
* @param [OUT] result 选择结果。
* @return 生成了有效选择返回 true。
*/
bool CommitHover(MenuResult& result);
/**
* @brief 计算指定下标项的内联输入框矩形(EDIT 项)。
* @param [IN] index 项目下标。
* @return 输入框矩形,非法下标返回空矩形。
*/
gd::RectF EditFieldRect(int index) const;
/**
* @brief 计算 EDIT 项**编辑态单位后缀**所占的宽度(0 表示无后缀)。
*
* 排版、命中测试与绘制共用:输入框正文区会向右让出这么多宽度,
* 后缀文本绘制在它右侧(靠右对齐)。
*
* @param [IN] item 菜单项。
* @return 后缀宽度(像素)。
*/
float EditSuffixWidth(const MenuItem& item) const;
/**
* @brief 计算 `HEADER_ACTION` 项右侧**图标按钮**(如「还原」)的矩形。
*
* 图标紧贴该行右端(右边距与其它行一致、行内垂直居中),大小按缩放比例推导。
* 鼠标悬停时该图标会高亮,点击即下发该项的命令。
*
* @param [IN] index 项目下标。
* @return 图标矩形;非 `HEADER_ACTION` 项或非法下标返回空矩形。
*/
gd::RectF HeaderIconRect(int index) const;
/**
* @brief 计算 EDIT 项输入框的“自然宽度”(排版与绘制共用同一公式)。
* @param [IN] item 菜单项。
* @return 宽度(像素)。
*/
float NaturalEditFieldWidth(const MenuItem& item) const;
/**
* @brief 进入指定项的内联编辑状态。
* @param [IN] index 项目下标。
* @return 成功进入返回 true。
*/
bool BeginItemEdit(int index);
/**
* @brief 结束内联编辑。
* @param [IN] commit 是否提交(false 为放弃修改)。
* @param [OUT] result 提交时输出 editCommit 结果。
* @return 之前处于编辑状态返回 true。
*/
bool EndItemEdit(bool commit, MenuResult& result);
/**
* @brief 依据当前项集合重新排布(用于值文本变化后)。
*/
void RebuildLayout();
/**
* @brief 退出内联编辑状态(不产生任何结果)。
*/
void CloseEditState();
/**
* @brief 展开指定项的二级子菜单(项不存在或非 SUBMENU 时等价于关闭)。
* @param [IN] parentIndex 父项下标。
*/
void BuildSubLayout(int parentIndex);
/**
* @brief 关闭二级子菜单并清空其状态。
*/
void CloseSubmenu();
/**
* @brief 查询坐标命中的子菜单项下标。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 子菜单项下标,未命中返回 -1。
*/
int SubItemAtPoint(int x, int y) const;
/**
* @brief 绘制二级子菜单(含投影与全部条目)。
* @param [IN,OUT] g 绘图上下文。
*/
void PaintSubmenu(gd::Graphics& g);
/**
* @brief 键盘移动悬停项后,把该项滚动到可滚动区的可视范围内。
* @param [IN] index 目标条目下标。
* @return 滚动偏移发生变化返回 true。
*/
bool EnsureItemVisible(int index);
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
float m_scale = 1.0f; /**< DPI 缩放比例。 */
bool m_open = false; /**< 是否处于打开状态。 */
MenuScope m_scope = MenuScope::NONE; /**< 来源组件标识。 */
std::vector<MenuItem> m_items; /**< 菜单项集合。 */
gd::RectF m_rect; /**< 菜单整体矩形。 */
std::vector<gd::RectF> m_itemRects; /**< 各项矩形。 */
/**
* 同一次排布中,**带说明文字的 EDIT 项**共用的输入框宽度(0 表示没有这类项)。
*
* 宽度取这些项里最宽的自然宽度,并在排布时按“不能挤掉说明文字 / 不超过内容区”夹好。
* 同一面板内的输入框因此一样宽、右边界对齐(用户要求「Y轴」与「时间轴」等宽对齐,
* 而不是各自按自己的文本长度自适应)。
*/
float m_editFieldWidth = 0.0f;
std::vector<std::vector<gd::RectF>> m_swatchRects; /**< 各项内的色块矩形。 */
int m_hoverIndex = -1; /**< 当前悬停项。 */
int m_hoverIconIndex = -1; /**< 当前悬停的 `HEADER_ACTION` 图标所在项下标。 */
int m_hoverSwatch = -1; /**< 当前悬停色块。 */
int m_pressedIndex = -1; /**< 按下时的项目下标。 */
int m_pressedSwatch = -1; /**< 按下时的色块下标。 */
HWND m_host = nullptr; /**< 剪贴板所有者窗口句柄。 */
TextEdit m_edit; /**< 内联输入框编辑模型。 */
int m_editIndex = -1; /**< 正在编辑的项目下标,-1 表示未编辑。 */
gd::RectF m_buildBounds; /**< 最近一次排布使用的宿主矩形。 */
int m_buildX = 0; /**< 最近一次排布使用的锚点横坐标。 */
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
bool m_buildExpandUp = false; /**< 最近一次排布是否为“向上展开(锚点为左下角)”。 */
/** 最近一次使用的布局约束(`RebuildLayout` 复用)。 */
MenuLayoutConstraint m_constraint;
/**
* @brief 可滚动条目区的下标范围(半开区间 `[m_scrollFirst, m_pinnedIndex)`)。
*
* `m_scrollFirst` = 前导固定条目数(**「所属网格」这类标题必须始终留在最上方、不参与滚动**);
* `m_pinnedIndex` = 末尾固定区起点(最后一个分隔线,典型「新建网格」钉在底部)。
*/
int m_scrollFirst = 0;
int m_pinnedIndex = -1;
bool m_scrollEnabled = false; /**< 条目区是否可滚动(内容超过约束上限时启用)。 */
float m_scrollOffset = 0.0f; /**< 滚动区当前的滚动偏移(像素,0 = 顶部)。 */
float m_scrollRegionHeight = 0.0f; /**< 滚动区的内容总高(像素)。 */
gd::RectF m_scrollViewport; /**< 滚动区的可视矩形(也是滚动区条目的裁剪矩形)。 */
gd::RectF m_scrollBarRect; /**< 滚动条轨道矩形(仅可滚动菜单有效)。 */
bool m_scrollBarDragging = false; /**< 是否正在拖拽滚动条滑块。 */
float m_scrollBarGrabOffset = 0.0f; /**< 拖拽时鼠标相对滑块顶部的偏移(像素)。 */
bool m_subOpen = false; /**< 二级子菜单是否展开。 */
int m_subParentIndex = -1; /**< 展开子菜单的父项下标。 */
gd::RectF m_subRect; /**< 子菜单整体矩形。 */
std::vector<MenuItem> m_subItems; /**< 子菜单项集合。 */
std::vector<gd::RectF> m_subItemRects; /**< 子菜单各项矩形。 */
int m_subHoverIndex = -1; /**< 子菜单当前悬停项。 */
int m_subPressedIndex = -1; /**< 子菜单按下时的项下标。 */
};
} // namespace jdi
#endif // __POPUP_MENU_HPP__