界面增强:网格图例 GX 编号、工具栏状态修复、增量括号、实验性边沿计数

- 每个网格图例左侧新增当前网格的 “GX” 编号徽标,与左侧信号列表行末的 G1/G2 徽标
  使用完全一致的样式(同宽高、同圆角、同配色、同字体),并纳入图例底板范围。

- 修复工具栏 toggle 状态被重置的缺陷:Toolbar::Layout() 会重建全部按钮,
  原先会把 checked 全部清零。因此当「光标」按钮因为模式变化(光标x2 → 光标)
  触发文本变更与 Relayout 时,同帧刚设置好的「Y 轴自适应」「选区放大」勾选态被清掉,
  表现为切换光标模式时前面两个开关被一并取消。现在 Layout() 会按 action 继承既有
  checked 状态,重建后状态保持不变。

- 双光标右侧光标的增量显示去掉逗号,改为包裹在圆括号中:`4.9425 (+0.0008)`。

- 网格右键菜单在分隔横线上方新增「实验性功能」(带二级子菜单指示箭头):
  鼠标悬停即向右展开二级菜单,其中包含「边沿计数」切换项(带 √)。
  为此为 PopupMenu 增加了完整的二级子菜单支持:子菜单排版(贴主菜单右缘、
  右侧空间不足时翻到左侧、限制在宿主窗口内)、悬停展开/移开收起、
  点击父项不下发命令、子菜单内按下/抬起配对提交、键盘(← 收起 / → 展开 /
  ↑↓ 在子菜单内移动 / Enter 提交 / Esc 先收起子菜单再关菜单)、
  独立的投影与绘制、ContainsPoint 覆盖子菜单区域、Relayout 后子菜单位置重建。

- 「边沿计数」开启后(按网格独立,状态随 RebuildLanes 按信号继承):
  * 在该网格绘制一条横向光标,风格与纵向时间光标一致(同色同线宽),
    左右两端各带一个尖端朝内的三角把手。
  * 左端显示当前光标的 Y 轴值,读数胶囊纵向居中于横线、靠网格左侧。
  * 横向光标不绘制任何数据点取值标注(与纵向光标的行为不同)。
  * 横向光标可上下拖动改变幅值,拖动时显示上下箭头光标。
  * 生成带 ✕ 关闭按钮的浮动面板:标题栏显示 “GX” 徽标 + “边沿计数”,
    下方按信号列出名称(信号色)与「上升沿 N / 下降沿 M」。
    面板可在整个绘图区内拖动(限制在绘图区内),✕ 仅收起面板、保留横向光标,
    重新勾选菜单项可再次展开。
  * 边沿判定阈值为信号幅值的 (min+max)/2 中点穿越,NaN 视为断点;
    统计结果按 (文件, 信号) 缓存,数据层变化时随折线缓存一并失效。

验证(TestData/VERY_BIG_DATA.CSV,编码器A/B相密集方波,125%、1725x996):
- 图例编号:G1 / G2 徽标与左侧列表徽标样式一致。
- 工具栏:开启「Y 轴自适应」+「选区放大」后,光标由 光标x2 切回 光标,
  两个开关仍保持勾选(截图核对)。
- 增量:右光标显示 `4.9425 (+0.0008)`。
- 二级菜单:悬停「实验性功能」展开「边沿计数」,箭头与子菜单位置正确。
- 边沿计数:横向光标 + 左端读数 `2.54415` + 面板
  `G1 边沿计数 / 编码器A相 / 上升沿 1333422 下降沿 1333421`;
  上下拖动后横线上移到 4.13602(与 Y 轴刻度一致);面板拖到绘图区左下并保持在内;
  点击 ✕ 后面板收起而横向光标保留;
  光标形态探测:横线悬停 SIZENS、面板悬停 SIZEALL。
- 合并网格后面板同时列出网格内两条信号(编码器A相 / 编码器B相)。
- 回归:[1].CSV 双光标正常(光标x2),Δt:151ms 200us;
  make clean && make all 无警告无错误。
- 修复过程中发现:静态场景缓存命中路径(贴图快路径)与整幅重绘路径是两处独立的
  叠加层绘制入口,新增的随鼠标变化内容必须同时加在两边。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-09 09:30:30 +08:00
1 parent a566d98bab
commit cd6e0ed058
8 files changed
+1138 -26

No files matched your search

+23
View File
@@ -347,6 +347,29 @@ namespace jdi
* 用户手动调整幅值(拖拽平移、Y 轴选区放大、直接输入上下限)后会自动关闭。
*/
bool fitYToSelection = false;
/**
* @brief 「实验性功能 → 边沿计数」开关。
*
* 开启后本网格显示一条横向光标(可上下拖动),并在浮动面板中列出该网格各信号的
* 上升沿 / 下降沿数量。与 `fitYToSelection` 一样属于网格自身状态,重建网格时按信号继承。
*/
bool edgeCount = false;
/** 横向光标所在的幅值(数据单位)。 */
double edgeValue = 0.0;
/** `edgeValue` 是否已经初始化(首次开启时取可见幅值范围的中点)。 */
bool edgeValueValid = false;
/** 边沿统计浮动面板是否展开(点击面板上的 ✕ 后置 false)。 */
bool edgePanelOpen = true;
/** 统计面板左上角位置(客户区像素;<0 表示自动定位到绘图区右侧)。 */
double edgePanelX = -1.0;
/** 统计面板左上角纵坐标(<0 表示自动定位)。 */
double edgePanelY = -1.0;
};
/**
+36 -1
View File
@@ -36,7 +36,8 @@ namespace jdi
SEPARATOR = 1, /**< 分隔线。 */
HEADER = 2, /**< 分组标题(不可点击)。 */
SWATCHES = 3, /**< 颜色色块行。 */
EDIT = 4 /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
SUBMENU = 5 /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
};
/**
@@ -54,6 +55,7 @@ namespace jdi
int checkedSwatch = -1; /**< 被选中的色块下标。 */
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
};
/**
@@ -319,6 +321,31 @@ namespace jdi
*/
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);
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
float m_scale = 1.0f; /**< DPI 缩放比例。 */
@@ -343,6 +370,14 @@ namespace jdi
gd::RectF m_buildBounds; /**< 最近一次排布使用的宿主矩形。 */
int m_buildX = 0; /**< 最近一次排布使用的锚点横坐标。 */
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
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
+40 -9
View File
@@ -111,26 +111,57 @@ namespace jdi
void RebuildLanes(const std::vector<std::vector<bool>>& visible, std::vector<Lane>& lanes,
LaneMode mode)
{
// 先记下每个信号原来所在网格的「选区 Y 轴自适应」开关:
// 随网格携带的“网格自身状态”(按信号继承):重建网格时不能丢。
struct LaneCarry
{
bool fitYToSelection = false;
bool edgeCount = false;
double edgeValue = 0.0;
bool edgeValueValid = false;
bool edgePanelOpen = true;
double edgePanelX = -1.0;
double edgePanelY = -1.0;
};
// 先记下每个信号原来所在网格的网格自身状态:
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
std::vector<std::pair<SignalRef, bool>> previousFit;
std::vector<std::pair<SignalRef, LaneCarry>> previousCarry;
for (const Lane& lane : lanes)
{
LaneCarry carry;
carry.fitYToSelection = lane.fitYToSelection;
carry.edgeCount = lane.edgeCount;
carry.edgeValue = lane.edgeValue;
carry.edgeValueValid = lane.edgeValueValid;
carry.edgePanelOpen = lane.edgePanelOpen;
carry.edgePanelX = lane.edgePanelX;
carry.edgePanelY = lane.edgePanelY;
for (const SignalRef& ref : lane.signals)
{
previousFit.emplace_back(ref, lane.fitYToSelection);
previousCarry.emplace_back(ref, carry);
}
}
const auto inheritedFit = [&previousFit](const SignalRef& ref)
const auto inheritedCarry = [&previousCarry](const SignalRef& ref)
{
for (const std::pair<SignalRef, bool>& entry : previousFit)
for (const std::pair<SignalRef, LaneCarry>& entry : previousCarry)
{
if (entry.first == ref)
{
return entry.second;
}
}
return false;
return LaneCarry();
};
const auto applyCarry = [&inheritedCarry](Lane& lane, const SignalRef& ref)
{
const LaneCarry carry = inheritedCarry(ref);
lane.fitYToSelection = carry.fitYToSelection;
lane.edgeCount = carry.edgeCount;
lane.edgeValue = carry.edgeValue;
lane.edgeValueValid = carry.edgeValueValid;
lane.edgePanelOpen = carry.edgePanelOpen;
lane.edgePanelX = carry.edgePanelX;
lane.edgePanelY = carry.edgePanelY;
};
// 收集全部可见信号引用。
@@ -173,7 +204,7 @@ namespace jdi
{
Lane lane;
lane.autoRange = true;
lane.fitYToSelection = inheritedFit(ref);
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
@@ -198,7 +229,7 @@ namespace jdi
lane.autoRange = previousAutoRange;
lane.yMin = previousYMin;
lane.yMax = previousYMax;
lane.fitYToSelection = inheritedFit(visibleRefs.front());
applyCarry(lane, visibleRefs.front());
lane.signals = visibleRefs;
lanes.push_back(std::move(lane));
}
@@ -240,7 +271,7 @@ namespace jdi
Lane lane;
lane.autoRange = true;
lane.fitYToSelection = inheritedFit(ref);
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
+399 -8
View File
@@ -25,6 +25,8 @@ namespace jdi
const float kEditFieldWidth = 88.0f; /**< EDIT 项输入框宽度。 */
const float kEditFieldGap = 6.0f; /**< 说明文字与输入框之间的最小间隔。 */
const float kEditFieldPadV = 5.0f; /**< 输入框上下内缩。 */
const float kSubArrowSize = 4.5f; /**< 子菜单指示箭头尺寸。 */
const float kSubOverlap = 2.0f; /**< 子菜单与主菜单重叠的宽度(避免中间露出缝隙)。 */
}
PopupMenu::PopupMenu() = default;
@@ -165,6 +167,131 @@ namespace jdi
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
}
void PopupMenu::CloseSubmenu()
{
m_subOpen = false;
m_subParentIndex = -1;
m_subRect = gd::RectF();
m_subItems.clear();
m_subItemRects.clear();
m_subHoverIndex = -1;
m_subPressedIndex = -1;
}
void PopupMenu::BuildSubLayout(int parentIndex)
{
CloseSubmenu();
if (parentIndex < 0 || parentIndex >= static_cast<int>(m_items.size()))
{
return;
}
const MenuItem& parent = m_items[static_cast<std::size_t>(parentIndex)];
if (parent.kind != MenuItemKind::SUBMENU || parent.submenu.empty())
{
return;
}
m_subItems = parent.submenu;
// 与主菜单一致:宽度取条目中的最大者并夹在上下限之间。
float contentWidth = kMinWidth * m_scale;
float totalHeight = kPadV * 2.0f * m_scale;
for (const MenuItem& item : m_subItems)
{
totalHeight += ItemHeight(item);
if (item.kind == MenuItemKind::SWATCHES)
{
const int count = std::max(0, std::min(item.swatchCount, 16));
const float width = (kPadH + kItemPad) * 2.0f * m_scale +
static_cast<float>(count) * kSwatchSize * m_scale +
static_cast<float>(std::max(0, count - 1)) * kSwatchGap * m_scale;
contentWidth = std::max(contentWidth, width);
}
else if (item.kind == MenuItemKind::HEADER)
{
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
kMarkerGap * m_scale +
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label);
contentWidth = std::max(contentWidth, width);
}
else if (item.kind == MenuItemKind::EDIT)
{
const float fieldWidth = NaturalEditFieldWidth(item);
const float width = (kPadH + kItemPad) * 2.0f * m_scale +
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale +
fieldWidth;
contentWidth = std::max(contentWidth, width);
}
else if (item.kind == MenuItemKind::NORMAL || item.kind == MenuItemKind::SUBMENU)
{
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
kMarkerGap * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, item.label);
contentWidth = std::max(contentWidth, width);
}
}
contentWidth = std::min(contentWidth, kMaxWidth * m_scale);
const float width = contentWidth;
const float height = totalHeight;
const gd::RectF& parentRect = m_itemRects[static_cast<std::size_t>(parentIndex)];
const gd::RectF& bounds = m_buildBounds;
// 默认贴主菜单右缘并向上与父项顶端对齐;右侧空间不足时翻到左侧。
float left = m_rect.X + m_rect.Width - kSubOverlap * m_scale;
if (left + width > bounds.X + bounds.Width - 2.0f)
{
left = m_rect.X - width + kSubOverlap * m_scale;
}
left = std::max(bounds.X + 2.0f, std::min(left, bounds.X + bounds.Width - width - 2.0f));
float top = parentRect.Y - kPadV * m_scale;
top = std::max(bounds.Y + 2.0f, std::min(top, bounds.Y + bounds.Height - height - 2.0f));
m_subRect = gd::RectF(left, top, width, height);
m_subItemRects.assign(m_subItems.size(), gd::RectF());
float cursorY = top + kPadV * m_scale;
for (std::size_t index = 0; index < m_subItems.size(); ++index)
{
const float itemHeight = ItemHeight(m_subItems[index]);
m_subItemRects[index] = gd::RectF(left + kPadH * m_scale, cursorY,
std::max(0.0f, width - kPadH * 2.0f * m_scale), itemHeight);
cursorY += itemHeight;
}
m_subParentIndex = parentIndex;
m_subOpen = true;
m_subHoverIndex = -1;
m_subPressedIndex = -1;
}
int PopupMenu::SubItemAtPoint(int x, int y) const
{
if (!m_subOpen)
{
return -1;
}
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
for (std::size_t index = 0; index < m_subItems.size(); ++index)
{
const MenuItem& item = m_subItems[index];
if (item.kind == MenuItemKind::SEPARATOR || item.kind == MenuItemKind::HEADER)
{
continue;
}
if (!item.enabled)
{
continue;
}
if (UiPointInRect(m_subItemRects[index], fx, fy))
{
return static_cast<int>(index);
}
}
return -1;
}
gd::RectF PopupMenu::EditFieldRect(int index) const
{
if (index < 0 || index >= static_cast<int>(m_items.size()))
@@ -264,7 +391,13 @@ namespace jdi
{
return;
}
const int subParent = m_subParentIndex;
BuildLayout(m_buildBounds, m_buildX, m_buildY);
// 主菜单重新排布后子菜单位置会失效:若之前是展开的则按新几何重建。
if (subParent >= 0)
{
BuildSubLayout(subParent);
}
}
bool PopupMenu::UpdateItemValue(int id, const std::wstring& value)
@@ -392,6 +525,7 @@ namespace jdi
m_buildX = x;
m_buildY = y;
CloseSubmenu();
CloseEditState();
BuildLayout(bounds, x, y);
}
@@ -407,6 +541,7 @@ namespace jdi
m_hoverSwatch = -1;
m_pressedIndex = -1;
m_pressedSwatch = -1;
CloseSubmenu();
CloseEditState();
}
@@ -425,7 +560,14 @@ namespace jdi
{
return false;
}
return UiPointInRect(m_rect, static_cast<float>(x), static_cast<float>(y));
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
if (UiPointInRect(m_rect, fx, fy))
{
return true;
}
// 展开的二级子菜单同样属于菜单范围(否则在子菜单内点击会被当成“点菜单外”)。
return m_subOpen && UiPointInRect(m_subRect, fx, fy);
}
int PopupMenu::ItemAtPoint(int x, int y, int& swatchIndex) const
@@ -545,16 +687,47 @@ namespace jdi
return false;
}
bool changed = false;
// 鼠标在展开的子菜单内:只更新子菜单的悬停项(主菜单父项保持高亮)。
if (m_subOpen && UiPointInRect(m_subRect, static_cast<float>(x), static_cast<float>(y)))
{
const int sub = SubItemAtPoint(x, y);
if (sub != m_subHoverIndex)
{
m_subHoverIndex = sub;
changed = true;
}
return changed;
}
int swatchIndex = -1;
const int index = ItemAtPoint(x, y, swatchIndex);
if (index == m_hoverIndex && swatchIndex == m_hoverSwatch)
if (index != m_hoverIndex || swatchIndex != m_hoverSwatch)
{
return false;
m_hoverIndex = index;
m_hoverSwatch = swatchIndex;
changed = true;
}
m_hoverIndex = index;
m_hoverSwatch = swatchIndex;
return true;
// 悬停到带子菜单的项 → 向右展开;移开到其它条目 → 收起。
const bool onSubmenuParent =
(index >= 0) && (m_items[static_cast<std::size_t>(index)].kind == MenuItemKind::SUBMENU);
if (onSubmenuParent)
{
if (!m_subOpen || m_subParentIndex != index)
{
BuildSubLayout(index);
changed = true;
}
}
else if (m_subOpen)
{
CloseSubmenu();
changed = true;
}
return changed;
}
bool PopupMenu::OnLButtonDown(int x, int y, MenuResult& result)
@@ -573,6 +746,15 @@ namespace jdi
return true;
}
// 子菜单内的按下:记录待确认项,待抬起时提交。
if (m_subOpen && UiPointInRect(m_subRect, static_cast<float>(x), static_cast<float>(y)))
{
m_subPressedIndex = SubItemAtPoint(x, y);
m_pressedIndex = -1;
m_pressedSwatch = -1;
return true;
}
// 已经处于编辑状态时:点击当前输入框 → 重新定位插入符;
// 点击其它位置 → 先提交本次编辑(菜单不关闭),再按新位置继续处理。
if (m_editIndex >= 0)
@@ -602,6 +784,28 @@ namespace jdi
return false;
}
// 子菜单内的抬起:按下与抬起命中同一项才提交(菜单随之关闭)。
if (m_subOpen && m_subPressedIndex >= 0)
{
const int subPressed = m_subPressedIndex;
m_subPressedIndex = -1;
if (UiPointInRect(m_subRect, static_cast<float>(x), static_cast<float>(y)) &&
SubItemAtPoint(x, y) == subPressed)
{
const MenuItem& item = m_subItems[static_cast<std::size_t>(subPressed)];
if (item.kind == MenuItemKind::NORMAL || item.kind == MenuItemKind::SWATCHES)
{
result.valid = true;
result.scope = m_scope;
result.id = item.id;
result.swatchIndex = -1;
result.itemRect = m_subItemRects[static_cast<std::size_t>(subPressed)];
Close();
}
}
return true;
}
// 编辑态下松开:先结束拖拽框选,不当作普通项点击。
if (m_editIndex >= 0)
{
@@ -633,6 +837,16 @@ namespace jdi
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
// 子菜单父项:单击不下发命令,只保证子菜单处于展开状态。
if (item.kind == MenuItemKind::SUBMENU)
{
if (!m_subOpen || m_subParentIndex != index)
{
BuildSubLayout(index);
}
return true;
}
// 编辑项:单击进入菜单内联编辑,菜单保持打开。
if (item.kind == MenuItemKind::EDIT)
{
@@ -670,8 +884,7 @@ namespace jdi
// 编辑态下按键全部交给输入框(回车提交、Esc 取消本次编辑但菜单保持打开)。
if (m_editIndex >= 0)
{
const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
{ const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
switch (key)
{
case VK_ESCAPE:
@@ -707,16 +920,67 @@ namespace jdi
switch (key)
{
case VK_ESCAPE:
// Esc 先收起展开的二级子菜单,再次按下才关闭整个菜单。
if (m_subOpen)
{
CloseSubmenu();
return true;
}
Close();
return true;
case VK_LEFT:
if (m_subOpen)
{
CloseSubmenu();
return true;
}
break;
case VK_RIGHT:
{
if (m_hoverIndex >= 0 &&
m_items[static_cast<std::size_t>(m_hoverIndex)].kind == MenuItemKind::SUBMENU)
{
BuildSubLayout(m_hoverIndex);
return true;
}
break;
}
case VK_DOWN:
if (m_subOpen && m_subItems.size() > 0)
{
const int count = static_cast<int>(m_subItems.size());
m_subHoverIndex = (m_subHoverIndex + 1 + count) % count;
return true;
}
MoveHover(1);
return true;
case VK_UP:
if (m_subOpen && m_subItems.size() > 0)
{
const int count = static_cast<int>(m_subItems.size());
m_subHoverIndex = (m_subHoverIndex - 1 + count) % count;
return true;
}
MoveHover(-1);
return true;
case VK_RETURN:
{
// 子菜单展开且已有悬停项时,回车提交该项。
if (m_subOpen && m_subHoverIndex >= 0 &&
m_subHoverIndex < static_cast<int>(m_subItems.size()))
{
const MenuItem& item = m_subItems[static_cast<std::size_t>(m_subHoverIndex)];
if (item.kind == MenuItemKind::NORMAL && item.enabled)
{
result.valid = true;
result.scope = m_scope;
result.id = item.id;
result.swatchIndex = -1;
result.itemRect = m_subItemRects[static_cast<std::size_t>(m_subHoverIndex)];
Close();
}
return true;
}
if (CommitHover(result))
{
Close();
@@ -864,6 +1128,133 @@ namespace jdi
cy - 4.0f * m_scale, markerColor, 1.8f * m_scale);
}
// 带子菜单的项:在右侧标记列画一个指向右的三角,提示可以继续展开。
if (item.kind == MenuItemKind::SUBMENU)
{
const float markerRight = rect.X + rect.Width - kItemPad * m_scale;
const float cx = markerRight - kMarkerColumn * m_scale * 0.5f;
const float cy = rect.Y + rect.Height * 0.5f;
const float size = kSubArrowSize * m_scale;
gd::PointF arrow[3];
arrow[0] = gd::PointF(cx - size * 0.5f, cy - size);
arrow[1] = gd::PointF(cx - size * 0.5f, cy + size);
arrow[2] = gd::PointF(cx + size * 0.7f, cy);
gd::SolidBrush brush(ToGdiColor(textColor));
g.FillPolygon(&brush, arrow, 3);
}
const float textLeft = rect.X + kItemPad * m_scale;
const float textWidth =
rect.Width - 2.0f * kItemPad * m_scale - kMarkerColumn * m_scale - kMarkerGap * m_scale;
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, textWidth), m_fonts->uiSmall,
gd::RectF(textLeft, rect.Y, std::max(8.0f, textWidth), rect.Height), textColor,
TextAlign::LEFT);
}
// 二级子菜单最后绘制,保证覆盖在主菜单之上。
PaintSubmenu(g);
}
void PopupMenu::PaintSubmenu(gd::Graphics& g)
{
if (!m_subOpen || m_subRect.Width <= 0.0f || m_subRect.Height <= 0.0f)
{
return;
}
UiFillRoundRect(g, gd::RectF(m_subRect.X + 1.0f * m_scale, m_subRect.Y + 2.0f * m_scale,
m_subRect.Width, m_subRect.Height),
kCorner * m_scale, 0x14000000u);
UiFillRoundRect(g, gd::RectF(m_subRect.X + 2.0f * m_scale, m_subRect.Y + 4.0f * m_scale,
m_subRect.Width, m_subRect.Height),
kCorner * m_scale, 0x10000000u);
UiFillRoundRect(g, m_subRect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_subRect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
for (std::size_t index = 0; index < m_subItems.size(); ++index)
{
const MenuItem& item = m_subItems[index];
const gd::RectF& rect = m_subItemRects[index];
if (item.kind == MenuItemKind::SEPARATOR)
{
const float lineY = rect.Y + rect.Height * 0.5f;
UiDrawLine(g, rect.X + kItemPad * m_scale, lineY,
rect.X + rect.Width - kItemPad * m_scale, lineY, m_theme->border, 1.0f);
continue;
}
if (item.kind == MenuItemKind::HEADER)
{
UiText(g, item.label, m_fonts->uiSmallBold,
gd::RectF(rect.X + kItemPad * m_scale, rect.Y, rect.Width, rect.Height),
m_theme->textMuted, TextAlign::LEFT);
continue;
}
const bool hovered = (static_cast<int>(index) == m_subHoverIndex) && item.enabled;
if (item.kind == MenuItemKind::EDIT)
{
// 子菜单中的编辑项:与主菜单一样就地编辑。
if (hovered && static_cast<int>(index) != m_editIndex)
{
UiFillRoundRect(g, rect, 4.0f * m_scale, WithAlpha(m_theme->accent, 0x22));
}
const std::uint32_t labelColor = item.enabled ? m_theme->text : m_theme->textMuted;
const float labelLeft = rect.X + kItemPad * m_scale;
const gd::RectF fieldRect = EditFieldRect(static_cast<int>(index));
const float labelWidth = std::max(8.0f, fieldRect.X - labelLeft - kEditFieldGap * m_scale);
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
const std::wstring text =
UiEllipsize(g, item.value, m_fonts->uiSmall, fieldRect.Width - 12.0f * m_scale);
UiText(g, text, m_fonts->uiSmall,
gd::RectF(fieldRect.X + 6.0f * m_scale, fieldRect.Y,
std::max(8.0f, fieldRect.Width - 12.0f * m_scale), fieldRect.Height),
item.enabled ? m_theme->textStrong : m_theme->textMuted, TextAlign::LEFT);
continue;
}
if (item.kind == MenuItemKind::SWATCHES)
{
continue;
}
std::uint32_t background = 0;
if (hovered)
{
background = m_theme->accent;
}
else if (static_cast<int>(index) == m_subPressedIndex)
{
background = m_theme->pressed;
}
if (background != 0)
{
UiFillRoundRect(g, rect, 4.0f * m_scale, background);
}
const std::uint32_t textColor = !item.enabled ? m_theme->textMuted
: (hovered ? m_theme->textOnAccent
: m_theme->text);
// 选中标记(对勾):与主菜单一致画在右侧标记列内。
if (item.checked)
{
const float markerRight = rect.X + rect.Width - kItemPad * m_scale;
const float cx = markerRight - kMarkerColumn * m_scale * 0.5f;
const float cy = rect.Y + rect.Height * 0.5f;
const std::uint32_t markerColor = hovered ? m_theme->textOnAccent : m_theme->accent;
UiDrawLine(g, cx - 4.0f * m_scale, cy, cx - 1.0f * m_scale, cy + 3.0f * m_scale,
markerColor, 1.8f * m_scale);
UiDrawLine(g, cx - 1.0f * m_scale, cy + 3.0f * m_scale, cx + 4.5f * m_scale,
cy - 4.0f * m_scale, markerColor, 1.8f * m_scale);
}
const float textLeft = rect.X + kItemPad * m_scale;
const float textWidth =
rect.Width - 2.0f * kItemPad * m_scale - kMarkerColumn * m_scale - kMarkerGap * m_scale;