修复五项交互缺陷:折起后滚动条 / 单点信号样式 / 线宽实时生效 / 网格菜单限高滚动 / 点选信号滚动绘图区

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:
DeepSeek V4.1 Flash committed 2026-10-16 11:06:31 +08:00
1 parent 9cf6b2fedc
commit 2904699eae
19 files changed
+743 -350

No files matched your search

+3 -1
View File
@@ -38,7 +38,9 @@ namespace jdi
/**
* @brief **只含一个有效采样点**的信号的初始线宽(取样式允许的最大线宽)。
*
* 单个数据点在「点线」模式下就是一个圆点,半径与线宽成正比;取最大值才能看清。
* 单个数据点的初始绘制方式是「散点」,半径随线宽线性增长;取最大值才能看清那个点。
* ⚠️ 用户 2026-10-16 要求:初始绘制方式由「点线」改为「**散点**」,
* 同时信号面板「样式」行的「线段」/「点线」两个按钮对该信号置灰。
* ⚠️ 曾经还对「有效采样点 2…16」的信号也设了「点线 + 2px」的默认样式,
* 该需求已**作废**(用户 2026-10-15 明确取消):放大后自动切「点线」的机制
* 已经覆盖了那个场景,无需再改默认样式。
+55 -1
View File
@@ -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; /**< 子菜单整体矩形。 */
+7 -3
View File
@@ -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
View File
@@ -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);
}
// 二级子菜单最后绘制,保证覆盖在主菜单之上。