修复五项交互缺陷:折起后滚动条 / 单点信号样式 / 线宽实时生效 / 网格菜单限高滚动 / 点选信号滚动绘图区
1) 分组折起后再展开,信号列表的滚动条消失(筛选态与常规态均可复现)
- 根因:AdvanceGroupAnimations 只在“折起完成”时重建行集合;而“展开完成”那一帧
m_contentHeight 从收起态跳回展开态,却没有任何路径重算滚动条可见性 ⇒ 滚动条停在隐藏。
- 修法:任一方向动画结束都 m_rowsDirty = true + BuildRows(),并每帧 RefreshScrollBar(false)。
⚠️ 动画期间不夹取滚动偏移,避免打断与滚动同步的折起动画(scrollSynced)。
2) 单点信号(finiteCount == 1)默认绘制方式由「点线」改为「散点」
- ApplySinglePointDefaultStyle → DrawMode::SCATTER(仍取最大线宽)。
- 新增 SignalPanel::ModeOptionEnabled:单点信号的「线段」「点线」置灰、不可点、
且不参与悬停高亮(避免“看着能点其实点不动”);样式区的绘制与命中都用它。
- PlotView::UpdateAutoScatterStyles 跳过单点信号,否则“放大自动点线”会把它又改回「点线」。
3) 拖动线宽滑块完全不生效(拖动时线宽不变、松手瞬间变了、再移动鼠标又变回去)
- 根因一:AcquirePolyline 的折线缓存键没有 lineWidth,而 BuildPolyline 把线宽烘进
包络带几何(每个像素列的上下包络各向外扩 max(1,线宽)*scale*0.5,「梳状」判定也依赖它)
⇒ 稠密信号(采样数 > 绘图区宽度)永远复用旧线宽的顶点与包络带。
- 根因二:样式补丁只更新“静态场景缓存 + 后备缓冲区”,而每网格图层(宽基底)仍是旧样式像素,
下一帧命中“宽基底快路径”时把新样式覆盖回旧像素。
- 修法:PolylineCacheEntry 增加 lineWidth 并进比较;
样式补丁之后 QueueLaneRebuild(受影响网格) + RenderPendingLaneRebuilds(1)(沿用每帧至多 1 个的预算)。
4) 「目标」行的 GX 标签复用「所属网格」菜单,并先给 PopupMenu 增加「限高 + 条目区滚动 + 尾部固定项」
- MenuLayoutConstraint::maxHeight(OpenLaneMenu 传客户区高度的 62%);
- 固定区 = 最后一个分隔线及其之后的条目(即「新建网格」)始终钉在菜单底部,其余条目可滚动;
- 滚轮滚动条目区(Application 在菜单内滚动时不再关闭菜单,其余情形保持既有行为);
- 键盘移动悬停项自动滚入可视区(EnsureItemVisible);ItemAtPoint 要求条目同时命中滚动视口;
- 滚动区文本用 UiTextClipped(GDI 文本不遵循 GDI+ SetClip),并跳过完全不可见的条目(性能);
- BuildLayout 不再无条件清零 m_scrollOffset(否则 RebuildLayout 会把滚轮位置吃掉);
- 「目标」行 GX 标签点击**不参与**联动滚动(悬停联动保留)。
5) 点选信号名 ⇒ 绘图区自动把目标网格滚动到可视区内(上百个独立网格时旧有高亮无法定位)
- SignalPanel::RequestPlotLaneScroll / TakePlotLaneScrollRequest;
Application::ApplyPendingPlotLaneScroll(WM_LBUTTONDOWN 的面板分支);
PlotView::ScrollLaneIntoView(在可视区上方就上滚、在下方就下滚、已完整可见则不动)。
- 只有“点击信号名”触发;悬停绝不改变视图;勾选框 / GX 徽标 / 移除 ✕ 也不触发。
验证(全部离屏执行,窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- 新增 .preflight/check(不入库)68/68 通过;
- A/B:五处修复全部回退 ⇒ 17 项如实失败(B 4 / C 4 / D 7 / E 2);“折起后滚动条”需要
逐帧重绘驱动动画才能复现,单独回退时 ⇒ 5 项如实失败;恢复后 68/68;
- make release / make debug 零警告。
其它:
- 按要求删除工作区 Tools/(structcheck 脚本)并移除 Makefile 的 structcheck 目标;
- Documents/agent_mem.md 同步:新增 §9.38、§11.2 第 97–103 条与变更日志一行。
This commit is contained in:
1 parent
9cf6b2fedc
commit
2904699eae
19 files changed
+743
-350
No files matched your search
@@ -38,7 +38,9 @@ namespace jdi
|
||||
/**
|
||||
* @brief **只含一个有效采样点**的信号的初始线宽(取样式允许的最大线宽)。
|
||||
*
|
||||
* 单个数据点在「点线」模式下就是一个圆点,半径与线宽成正比;取最大值才能看清。
|
||||
* 单个数据点的初始绘制方式是「散点」,半径随线宽线性增长;取最大值才能看清那个点。
|
||||
* ⚠️ 用户 2026-10-16 要求:初始绘制方式由「点线」改为「**散点**」,
|
||||
* 同时信号面板「样式」行的「线段」/「点线」两个按钮对该信号置灰。
|
||||
* ⚠️ 曾经还对「有效采样点 2…16」的信号也设了「点线 + 2px」的默认样式,
|
||||
* 该需求已**作废**(用户 2026-10-15 明确取消):放大后自动切「点线」的机制
|
||||
* 已经覆盖了那个场景,无需再改默认样式。
|
||||
|
||||
@@ -79,6 +79,24 @@ namespace jdi
|
||||
bool keepOpen = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 打开菜单时的附加布局约束。
|
||||
*/
|
||||
struct MenuLayoutConstraint
|
||||
{
|
||||
/**
|
||||
* @brief 菜单允许的最大高度(**客户区像素**,0 = 不限制)。
|
||||
*
|
||||
* 超过该高度时菜单不再变高:**最后一个分隔线及其之后的条目固定在菜单底部**
|
||||
* (典型用法:把「新建网格」钉在底部),其上方的内容成为可滚动的条目区
|
||||
*(鼠标滚轮 / 键盘方向键滚动)。
|
||||
*
|
||||
* 用于「条目数不定」的菜单:例如加载含上百个信号的 MF4 并把每个信号都放进独立网格时,
|
||||
* 「所属网格」菜单会有上百个条目(既超出屏幕、绘制开销也很大)。
|
||||
*/
|
||||
float maxHeight = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 菜单选择结果。
|
||||
*/
|
||||
@@ -127,9 +145,11 @@ namespace jdi
|
||||
* @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 std::vector<MenuItem>& items, bool expandUp = false,
|
||||
const MenuLayoutConstraint& constraint = MenuLayoutConstraint());
|
||||
|
||||
/**
|
||||
* @brief 关闭菜单。
|
||||
@@ -196,6 +216,19 @@ namespace jdi
|
||||
*/
|
||||
bool ContainsPoint(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 菜单是否因内容过长而启用了滚动(条目区可滚动、尾部分隔线之后固定到底部)。
|
||||
* @return 启用滚动返回 true。
|
||||
*/
|
||||
bool IsScrollable() const { return m_open && m_scrollEnabled; }
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标滚轮:滚动可滚动菜单的条目区。
|
||||
* @param [IN] delta 滚轮增量(`WM_MOUSEWHEEL` 的 `GET_WHEEL_DELTA_WPARAM`)。
|
||||
* @return 滚动位置发生变化返回 true。
|
||||
*/
|
||||
bool OnMouseWheel(int delta);
|
||||
|
||||
/**
|
||||
* @brief 设置剪贴板所有者窗口句柄(供内联输入框复制/粘贴)。
|
||||
* @param [IN] host 宿主窗口句柄,允许为空。
|
||||
@@ -392,6 +425,13 @@ namespace jdi
|
||||
*/
|
||||
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 缩放比例。 */
|
||||
@@ -427,6 +467,20 @@ namespace jdi
|
||||
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
|
||||
bool m_buildExpandUp = false; /**< 最近一次排布是否为“向上展开(锚点为左下角)”。 */
|
||||
|
||||
/** 最近一次使用的布局约束(`RebuildLayout` 复用)。 */
|
||||
MenuLayoutConstraint m_constraint;
|
||||
/**
|
||||
* @brief 尾部固定区(钉在菜单底部)的**首个条目下标**;-1 表示没有固定项。
|
||||
*
|
||||
* 由 `BuildLayout` 在启用滚动时设为最后一个分隔线的下标:该分隔线及其之后的条目
|
||||
* (典型:「新建网格」)始终画在菜单底部,其上方的内容在滚动区内。
|
||||
*/
|
||||
int m_pinnedIndex = -1;
|
||||
bool m_scrollEnabled = false; /**< 条目区是否可滚动(内容超过 `maxHeight` 时启用)。 */
|
||||
float m_scrollOffset = 0.0f; /**< 滚动区当前的滚动偏移(像素,0 = 顶部)。 */
|
||||
float m_scrollRegionHeight = 0.0f; /**< 滚动区的内容总高(像素)。 */
|
||||
gd::RectF m_scrollViewport; /**< 滚动区的可视矩形(也是滚动区条目的裁剪矩形)。 */
|
||||
|
||||
bool m_subOpen = false; /**< 二级子菜单是否展开。 */
|
||||
int m_subParentIndex = -1; /**< 展开子菜单的父项下标。 */
|
||||
gd::RectF m_subRect; /**< 子菜单整体矩形。 */
|
||||
|
||||
@@ -48,8 +48,12 @@ namespace jdi
|
||||
/**
|
||||
* @brief 为**只有一个有效采样点**的信号设置初始绘制样式。
|
||||
*
|
||||
* 单个数据点在「线段」模式下完全画不出来,因此初始就用「点线」并取**最大线宽**
|
||||
* (点线模式下数据点是一个圆点,半径与线宽成正比)。
|
||||
* 单个数据点在「线段」模式下完全画不出来,因此初始就用「散点」并取**最大线宽**
|
||||
* (散点半径随线宽线性增长,取最大值才能看清那个点)。
|
||||
*
|
||||
* ⚠️ 用户 2026-10-16 要求:初始绘制方式由「点线」改为「**散点**」,并且信号面板
|
||||
* 「样式」行里的「线段」/「点线」两个按钮对该信号**置灰**
|
||||
* (见 `SignalPanel::ModeOptionEnabled`)。
|
||||
*
|
||||
* ⚠️ 曾经还对「有效采样点 2…16」的信号也改用「点线 + 2px」—— 该需求已**作废**
|
||||
*(用户 2026-10-15 明确取消:放大后自动切点线的机制已覆盖那个场景)。
|
||||
@@ -63,7 +67,7 @@ namespace jdi
|
||||
{
|
||||
if (finiteCount == 1)
|
||||
{
|
||||
style.mode = DrawMode::LINE_SCATTER;
|
||||
style.mode = DrawMode::SCATTER;
|
||||
style.lineWidth = kSinglePointSignalLineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
+235
-29
@@ -125,10 +125,12 @@ namespace jdi
|
||||
float contentWidth = kMinWidth * m_scale;
|
||||
float totalHeight = kPadV * 2.0f * m_scale;
|
||||
|
||||
std::vector<float> heights(m_items.size(), 0.0f);
|
||||
for (std::size_t index = 0; index < m_items.size(); ++index)
|
||||
{
|
||||
const MenuItem& item = m_items[index];
|
||||
totalHeight += ItemHeight(item);
|
||||
heights[index] = ItemHeight(item);
|
||||
totalHeight += heights[index];
|
||||
|
||||
if (item.kind == MenuItemKind::SWATCHES)
|
||||
{
|
||||
@@ -168,7 +170,76 @@ namespace jdi
|
||||
|
||||
contentWidth = std::min(contentWidth, kMaxWidth * m_scale);
|
||||
const float width = contentWidth;
|
||||
const float height = totalHeight;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 超长菜单:限制高度 + 条目区可滚动 + 尾部固定项(见 `MenuLayoutConstraint`)。
|
||||
// 固定区 = **最后一个分隔线及其之后的条目**(典型:「新建网格」),
|
||||
// 它始终钉在菜单底部,因此滚动不会把“新建网格”飘走。
|
||||
// ------------------------------------------------------------------
|
||||
int pinnedFrom = -1;
|
||||
if (m_constraint.maxHeight > 0.0f)
|
||||
{
|
||||
for (int index = static_cast<int>(m_items.size()) - 1; index > 0; --index)
|
||||
{
|
||||
if (m_items[static_cast<std::size_t>(index)].kind == MenuItemKind::SEPARATOR)
|
||||
{
|
||||
pinnedFrom = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float scrollRegionHeight = 0.0f;
|
||||
float tailHeight = 0.0f;
|
||||
if (pinnedFrom > 0)
|
||||
{
|
||||
for (int index = 0; index < pinnedFrom; ++index)
|
||||
{
|
||||
scrollRegionHeight += heights[static_cast<std::size_t>(index)];
|
||||
}
|
||||
for (int index = pinnedFrom; index < static_cast<int>(m_items.size()); ++index)
|
||||
{
|
||||
tailHeight += heights[static_cast<std::size_t>(index)];
|
||||
}
|
||||
}
|
||||
|
||||
float height = totalHeight;
|
||||
m_scrollEnabled = false;
|
||||
m_scrollRegionHeight = 0.0f;
|
||||
m_pinnedIndex = -1;
|
||||
m_scrollViewport = gd::RectF();
|
||||
// ⚠️ **不要在这里无条件清零 `m_scrollOffset`**:`RebuildLayout()`(滚轮 / 数值回写触发)
|
||||
// 会重新走一遍本函数,清零就等于把滚动位置吃掉(滚轮“滚不动”)。只有在菜单**不再需要**
|
||||
// 滚动时才复位(`Open` 里已经为每次打开重置过)。
|
||||
if (m_constraint.maxHeight > 0.0f)
|
||||
{
|
||||
// 夹到宿主矩形以内(不盖住窗口边框);小到放不下时的上限直接视作“不限制”,
|
||||
// 交给下方的边界收敛处理,避免算出一个高度为 0 的菜单。
|
||||
float limit = std::min(m_constraint.maxHeight, bounds.Height - 4.0f * m_scale);
|
||||
if (limit < 40.0f * m_scale)
|
||||
{
|
||||
limit = 0.0f;
|
||||
}
|
||||
const float viewportHeight = (limit > 0.0f) ? (limit - kPadV * 2.0f * m_scale - tailHeight) : 0.0f;
|
||||
if (pinnedFrom > 0 && limit > 0.0f && totalHeight > limit &&
|
||||
viewportHeight >= kRowHeight * m_scale)
|
||||
{
|
||||
height = limit;
|
||||
m_scrollEnabled = true;
|
||||
m_pinnedIndex = pinnedFrom;
|
||||
m_scrollRegionHeight = scrollRegionHeight;
|
||||
m_scrollOffset = std::max(
|
||||
0.0f, std::min(m_scrollOffset, std::max(0.0f, scrollRegionHeight - viewportHeight)));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_scrollOffset = 0.0f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_scrollOffset = 0.0f;
|
||||
}
|
||||
|
||||
// 带说明文字的 EDIT 项共用**同一个输入框宽度**:取其中最大的自然宽度,
|
||||
// 再按“给最宽的说明文字留位 / 不超过内容区 3/4”夹取。
|
||||
@@ -215,12 +286,38 @@ namespace jdi
|
||||
|
||||
m_rect = gd::RectF(left, top, width, height);
|
||||
|
||||
float cursorY = top + kPadV * m_scale;
|
||||
// 条目排布:滚动区的条目整体上移 `m_scrollOffset`;固定区从菜单底部往上对齐。
|
||||
const float bodyTop = top + kPadV * m_scale;
|
||||
const float pinnedTop = top + height - kPadV * m_scale - tailHeight;
|
||||
m_scrollViewport =
|
||||
m_scrollEnabled
|
||||
? gd::RectF(left, bodyTop, width,
|
||||
std::max(0.0f, height - kPadV * 2.0f * m_scale - tailHeight))
|
||||
: gd::RectF();
|
||||
|
||||
float cursorY = bodyTop;
|
||||
float scrollCursorY = bodyTop - m_scrollOffset;
|
||||
float pinnedCursorY = (pinnedFrom > 0) ? pinnedTop : bodyTop;
|
||||
for (std::size_t index = 0; index < m_items.size(); ++index)
|
||||
{
|
||||
const MenuItem& item = m_items[index];
|
||||
const float itemHeight = ItemHeight(item);
|
||||
m_itemRects[index] = gd::RectF(left + kPadH * m_scale, cursorY,
|
||||
const float itemHeight = heights[index];
|
||||
|
||||
float rectY = cursorY;
|
||||
if (m_scrollEnabled)
|
||||
{
|
||||
if (static_cast<int>(index) < m_pinnedIndex)
|
||||
{
|
||||
rectY = scrollCursorY;
|
||||
scrollCursorY += itemHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
rectY = pinnedCursorY;
|
||||
pinnedCursorY += itemHeight;
|
||||
}
|
||||
}
|
||||
m_itemRects[index] = gd::RectF(left + kPadH * m_scale, rectY,
|
||||
std::max(0.0f, width - kPadH * 2.0f * m_scale), itemHeight);
|
||||
|
||||
if (item.kind == MenuItemKind::SWATCHES)
|
||||
@@ -228,7 +325,7 @@ namespace jdi
|
||||
const int count = std::max(0, std::min(item.swatchCount, 16));
|
||||
std::vector<gd::RectF>& rects = m_swatchRects[index];
|
||||
rects.reserve(static_cast<std::size_t>(count));
|
||||
const float swatchY = cursorY + (itemHeight - kSwatchSize * m_scale) * 0.5f;
|
||||
const float swatchY = rectY + (itemHeight - kSwatchSize * m_scale) * 0.5f;
|
||||
const float startX = left + (kPadH + kItemPad) * m_scale;
|
||||
for (int swatch = 0; swatch < count; ++swatch)
|
||||
{
|
||||
@@ -242,6 +339,47 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
bool PopupMenu::EnsureItemVisible(int index)
|
||||
{
|
||||
if (!m_scrollEnabled || index < 0 || index >= m_pinnedIndex || m_scrollViewport.Height <= 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const gd::RectF& rect = m_itemRects[static_cast<std::size_t>(index)];
|
||||
const float relative = rect.Y - m_scrollViewport.Y;
|
||||
float delta = 0.0f;
|
||||
if (relative < 0.0f)
|
||||
{
|
||||
delta = relative;
|
||||
}
|
||||
else if (relative + rect.Height > m_scrollViewport.Height)
|
||||
{
|
||||
delta = relative + rect.Height - m_scrollViewport.Height;
|
||||
}
|
||||
if (std::fabs(delta) < 0.5f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_scrollOffset += delta;
|
||||
// 重新排布(内部会重新夹取 `m_scrollOffset`)。
|
||||
RebuildLayout();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PopupMenu::OnMouseWheel(int delta)
|
||||
{
|
||||
if (!m_open || !m_scrollEnabled || delta == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const float previous = m_scrollOffset;
|
||||
// 向上滚(`delta > 0`)= 看更靠前的条目 ⇒ 偏移减小。
|
||||
m_scrollOffset -= static_cast<float>(delta) / 120.0f * 54.0f * m_scale;
|
||||
// 重新排布:条目矩形依赖滚动偏移,同时会夹取到合法范围。
|
||||
RebuildLayout();
|
||||
return std::fabs(m_scrollOffset - previous) > 0.01f;
|
||||
}
|
||||
|
||||
float PopupMenu::NaturalEditFieldWidth(const MenuItem& item) const
|
||||
{
|
||||
const std::wstring& widest =
|
||||
@@ -633,7 +771,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
void PopupMenu::Open(MenuScope scope, const gd::RectF& bounds, int x, int y,
|
||||
const std::vector<MenuItem>& items, bool expandUp)
|
||||
const std::vector<MenuItem>& items, bool expandUp,
|
||||
const MenuLayoutConstraint& constraint)
|
||||
{
|
||||
m_items = items;
|
||||
if (m_items.empty())
|
||||
@@ -653,6 +792,9 @@ namespace jdi
|
||||
m_buildX = x;
|
||||
m_buildY = y;
|
||||
m_buildExpandUp = expandUp;
|
||||
// 每次打开都从顶部开始(滚动位置不跨次沿用)。
|
||||
m_constraint = constraint;
|
||||
m_scrollOffset = 0.0f;
|
||||
|
||||
CloseSubmenu();
|
||||
CloseEditState();
|
||||
@@ -671,6 +813,12 @@ namespace jdi
|
||||
m_hoverSwatch = -1;
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
m_constraint = MenuLayoutConstraint();
|
||||
m_pinnedIndex = -1;
|
||||
m_scrollEnabled = false;
|
||||
m_scrollOffset = 0.0f;
|
||||
m_scrollRegionHeight = 0.0f;
|
||||
m_scrollViewport = gd::RectF();
|
||||
CloseSubmenu();
|
||||
CloseEditState();
|
||||
}
|
||||
@@ -720,7 +868,15 @@ namespace jdi
|
||||
}
|
||||
if (!UiPointInRect(m_itemRects[index], fx, fy))
|
||||
{
|
||||
continue; }
|
||||
continue;
|
||||
}
|
||||
// 可滚动菜单:滚动区内的条目还必须落在**滚动视口**内 ——
|
||||
// 夹在视口边界上、只有一部分可见的条目,只有落在视口内的那一部分可以点击。
|
||||
if (m_scrollEnabled && static_cast<int>(index) < m_pinnedIndex &&
|
||||
!UiPointInRect(m_scrollViewport, fx, fy))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.kind == MenuItemKind::SWATCHES)
|
||||
{
|
||||
@@ -772,6 +928,8 @@ namespace jdi
|
||||
{
|
||||
m_hoverIndex = index;
|
||||
m_hoverSwatch = -1;
|
||||
// 键盘移动时把目标条目滚入可视区(长菜单里按方向键不能“跳到看不见的条目”)。
|
||||
EnsureItemVisible(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1204,10 +1362,51 @@ namespace jdi
|
||||
UiFillRoundRect(g, m_rect, kCorner * m_scale, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, m_rect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
|
||||
|
||||
// 条目文字统一走本 lambda:可滚动区里的条目必须夹到**滚动视口** ——
|
||||
// `UiText` 走 GDI `DrawTextW`,**不遵循 GDI+ 的 `SetClip`**,不显式裁剪就会画到固定区上。
|
||||
//(GDI+ 图元 —— 行底色 / 圆角 / 分隔线 / 对勾 / 子菜单三角 —— 受 `SetClip` 约束,无需额外处理。)
|
||||
const auto itemText = [this, &g](const std::wstring& text, HFONT font, const gd::RectF& rect,
|
||||
std::uint32_t argb, TextAlign align, bool clipped)
|
||||
{
|
||||
if (clipped)
|
||||
{
|
||||
UiTextClipped(g, text, font, rect, argb, align, m_scrollViewport);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiText(g, text, font, rect, argb, align);
|
||||
}
|
||||
};
|
||||
|
||||
// 可滚动菜单:条目 [0, pinnedFrom) 在滚动区(裁剪到滚动视口),
|
||||
// 其余(最后一个分隔线及其之后的条目)钉在菜单底部、不参与滚动。
|
||||
const int pinnedFrom = (m_scrollEnabled && m_pinnedIndex >= 0)
|
||||
? m_pinnedIndex
|
||||
: static_cast<int>(m_items.size());
|
||||
const int clipState = m_scrollEnabled ? g.Save() : 0;
|
||||
if (m_scrollEnabled)
|
||||
{
|
||||
g.SetClip(m_scrollViewport, gd::CombineModeIntersect);
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < m_items.size(); ++index)
|
||||
{
|
||||
const MenuItem& item = m_items[index];
|
||||
const gd::RectF& rect = m_itemRects[index];
|
||||
// 进入固定区:撤掉滚动区的裁剪(固定区永远完整可见)。
|
||||
if (m_scrollEnabled && static_cast<int>(index) == pinnedFrom)
|
||||
{
|
||||
g.Restore(clipState);
|
||||
}
|
||||
const bool inScrollRegion = static_cast<int>(index) < pinnedFrom;
|
||||
// 滚动区里完全不可见的条目直接跳过:长菜单(上百条)逐条绘制会明显卡顿。
|
||||
if (m_scrollEnabled && inScrollRegion &&
|
||||
(rect.Y + rect.Height <= m_scrollViewport.Y ||
|
||||
rect.Y >= m_scrollViewport.Y + m_scrollViewport.Height))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const bool clipped = m_scrollEnabled && inScrollRegion;
|
||||
|
||||
if (item.kind == MenuItemKind::SEPARATOR)
|
||||
{
|
||||
@@ -1220,9 +1419,9 @@ namespace jdi
|
||||
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);
|
||||
itemText(item.label, m_fonts->uiSmallBold,
|
||||
gd::RectF(rect.X + kItemPad * m_scale, rect.Y, rect.Width, rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT, clipped);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1232,9 +1431,9 @@ namespace jdi
|
||||
const gd::RectF icon = HeaderIconRect(static_cast<int>(index));
|
||||
const float labelWidth =
|
||||
std::max(8.0f, icon.X - rect.X - kItemPad * m_scale - kHeaderIconGap * m_scale);
|
||||
UiText(g, UiEllipsize(item.label, m_fonts->uiSmallBold, labelWidth), m_fonts->uiSmallBold,
|
||||
gd::RectF(rect.X + kItemPad * m_scale, rect.Y, labelWidth, rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
itemText(UiEllipsize(item.label, m_fonts->uiSmallBold, labelWidth), m_fonts->uiSmallBold,
|
||||
gd::RectF(rect.X + kItemPad * m_scale, rect.Y, labelWidth, rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT, clipped);
|
||||
if (icon.Width > 0.0f)
|
||||
{
|
||||
const bool iconHovered = (static_cast<int>(index) == m_hoverIconIndex);
|
||||
@@ -1266,8 +1465,9 @@ namespace jdi
|
||||
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(item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT);
|
||||
itemText(UiEllipsize(item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||
gd::RectF(labelLeft, rect.Y, labelWidth, rect.Height), labelColor, TextAlign::LEFT,
|
||||
clipped);
|
||||
|
||||
const float suffix = EditSuffixWidth(item);
|
||||
if (static_cast<int>(index) == m_editIndex)
|
||||
@@ -1287,12 +1487,12 @@ namespace jdi
|
||||
if (suffix > 0.0f)
|
||||
{
|
||||
const float suffixLeft = textRect.X + textRect.Width;
|
||||
UiText(g, item.editSuffix, m_fonts->uiSmall,
|
||||
gd::RectF(suffixLeft, fieldRect.Y,
|
||||
std::max(4.0f, fieldRect.X + fieldRect.Width - suffixLeft -
|
||||
4.0f * m_scale),
|
||||
fieldRect.Height),
|
||||
m_theme->textMuted, TextAlign::RIGHT);
|
||||
itemText(item.editSuffix, m_fonts->uiSmall,
|
||||
gd::RectF(suffixLeft, fieldRect.Y,
|
||||
std::max(4.0f, fieldRect.X + fieldRect.Width - suffixLeft -
|
||||
4.0f * m_scale),
|
||||
fieldRect.Height),
|
||||
m_theme->textMuted, TextAlign::RIGHT, clipped);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1301,10 +1501,10 @@ namespace jdi
|
||||
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
|
||||
const std::wstring text =
|
||||
UiEllipsize(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);
|
||||
itemText(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, clipped);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1384,9 +1584,15 @@ namespace jdi
|
||||
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(item.label, m_fonts->uiSmall, textWidth), m_fonts->uiSmall,
|
||||
gd::RectF(textLeft, rect.Y, std::max(8.0f, textWidth), rect.Height), textColor,
|
||||
TextAlign::LEFT);
|
||||
itemText(UiEllipsize(item.label, m_fonts->uiSmall, textWidth), m_fonts->uiSmall,
|
||||
gd::RectF(textLeft, rect.Y, std::max(8.0f, textWidth), rect.Height), textColor,
|
||||
TextAlign::LEFT, clipped);
|
||||
}
|
||||
|
||||
// 恢复滚动区的裁剪(若已因固定区而恢复过则 `Restore` 会成对生效)。
|
||||
if (m_scrollEnabled)
|
||||
{
|
||||
g.Restore(clipState);
|
||||
}
|
||||
|
||||
// 二级子菜单最后绘制,保证覆盖在主菜单之上。
|
||||
|
||||
Reference in new issue
Block a user