「所属网格」菜单:标题固定 + 10 行上限 + 滚动条;网格滚动联动改为动态过渡
1) 菜单滚动时最上方的「所属网格」标题不再跟着滚走
- 根因:上一轮只实现了“尾部固定区”(最后一个分隔线 + 「新建网格」钉在底部),
而滚动区是从条目 0 开始的,于是标题也被一起滚掉。
- 修法:BuildLayout 新增 m_scrollFirst(= 前导 HEADER / HEADER_ACTION 的个数),
滚动区收敛为半开区间 [m_scrollFirst, m_pinnedIndex):
· 前导标题从菜单顶向下固定排(不参与滚动);
· 滚动区条目受 m_scrollOffset 影响;
· 尾部固定区从菜单底向上对齐。
- 配套:Paint 的裁剪与“跳过不可见条目”范围、ItemAtPoint 的“必须落在滚动视口内”判定、
EnsureItemVisible 的下界全部改用该区间;新增 ScrollRegion(first, last) 供查询/测试。
2) 菜单长度上限改为「最多 10 条网格」并加滚动条
- MenuLayoutConstraint 新增 maxScrollRows(滚动区最多显示的标准行数;与 maxHeight
同时给出时取更严格者);OpenLaneMenu 传 10(kLaneMenuVisibleRows),
另保留 maxHeight = max(180, 客户区高 × 0.8) 作为矮窗口的硬上限。
- 新增滚动条:轨道贴滚动条目区右内侧(不盖住标题与底部固定项),滑块高度按视口/内容比
(下限 22 逻辑像素);滚动区条目宽度让出 kScrollBarWidth + kScrollBarGap,
文本与对勾不会压在滑块上。
- 滚动条可拖拽:按下滑块 → 拖动(只改偏移、不更新悬停);点击轨道空白 → 滑块中心移过去;
松开结束拖拽且不下发命令、菜单保持打开。新增 ScrollOffsetForThumbTop 反算偏移。
- ⚠️ BuildLayout 绝不能复位 m_scrollBarDragging:拖拽时每帧都会 RebuildLayout(),
在那里清零会让拖拽“拖一下就脱手”(该状态只在 Open / Close 里复位)。
3) 网格滚动联动改为动态过渡(旧实现直接把视界瞬移过去,过于僵硬)
- PlotView::ScrollLaneIntoView 只负责启动平滑动画:与信号列表的“滚动到目标信号”同源
(ease-out cubic;时长 clamp(距离×0.35ms, 130, 300)),新增
m_laneScrollAnimActive/From/To/StartMs/DurationMs。
- 宿主 16ms 定时器(kScrollTimerId)调用 AdvanceLaneScrollAnimation() 逐帧推进,
与信号面板动画共用同一条定时器;Render 的“需要保持定时重绘”判定加入
LaneScrollAnimationActive()。
- 每帧 ClampLaneScroll() + BuildLaneVisuals()(网格滚动量参与场景/图层指纹与各网格 rect);
结束时精确落到目标。已完整可见的网格仍然不启动动画。
- ⚠️ 用户自己滚动网格(滚轮 / 拖滚动条)会 CancelLaneScrollAnimation(),避免与动画抢滚动位置。
验证(全部离屏执行:窗口 SW_HIDE + 只注入合成消息 + 进程与线程 BELOW_NORMAL,
不移动真实光标、不抢焦点、不截屏):
- .preflight/check 扩到 95 项,95/95 通过;新增断言:
· 菜单顶部标题在滚动时不动(Y 坐标不变)、滚动视口高度恰为 10 × 行高;
· 滚动条轨道/滑块存在、随滚动移动、按住滑块拖到底(菜单不关、不下发命令、末条目入视口)、
滚轮回到顶部;
· ScrollLaneIntoView 不瞬移(刚触发时几乎未动)、中间帧单调递进、收尾精确到位、
已可见时不启动动画、滚轮/手动滚动会取消动画。
- A/B:回退三处(标题固定 / 10 行上限 / 滚动动画)⇒ 10 项如实失败;恢复后 95/95。
- make release / make debug 零警告。
文档:Documents/agent_mem.md 新增 §9.39、§11.2 第 104–107 条与变更日志一行。
This commit is contained in:
1 parent
2904699eae
commit
82b5ebe4f9
10 files changed
+496
-78
No files matched your search
+242
-51
@@ -31,6 +31,9 @@ namespace jdi
|
||||
const float kHeaderIconSize = 16.0f; /**< `HEADER_ACTION` 右侧图标按钮的边长。 */
|
||||
const float kHeaderIconGap = 6.0f; /**< 标题文字与图标按钮之间的最小间隔。 */
|
||||
const float kRowFieldWidth = 132.0f; /**< 整行输入框(无说明文字)的默认宽度。 */
|
||||
const float kScrollBarWidth = 6.0f; /**< 可滚动菜单的滚动条宽度。 */
|
||||
const float kScrollBarGap = 4.0f; /**< 滚动条与条目内容之间的间距。 */
|
||||
const float kScrollBarMinThumb = 22.0f; /**< 滚动条滑块的最小高度。 */
|
||||
|
||||
/**
|
||||
* @brief 绘制「还原/重置」图标(逆时针回环箭头)。
|
||||
@@ -172,12 +175,15 @@ namespace jdi
|
||||
const float width = contentWidth;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 超长菜单:限制高度 + 条目区可滚动 + 尾部固定项(见 `MenuLayoutConstraint`)。
|
||||
// 固定区 = **最后一个分隔线及其之后的条目**(典型:「新建网格」),
|
||||
// 它始终钉在菜单底部,因此滚动不会把“新建网格”飘走。
|
||||
// 超长菜单:限制高度 + 条目区可滚动 + 顶部/尾部固定项(见 `MenuLayoutConstraint`)。
|
||||
// * 顶部固定 = **前导的 `HEADER` / `HEADER_ACTION`**(典型:「所属网格」标题)——
|
||||
// 用户 2026-10-16 要求:滚动时标题必须留在最上方不动;
|
||||
// * 尾部固定 = **最后一个分隔线及其之后的条目**(典型:「新建网格」)钉在底部;
|
||||
// * 中间是滚动区(鼠标滚轮 / 键盘 / 滚动条),右侧画一根滚动条。
|
||||
// ------------------------------------------------------------------
|
||||
int pinnedFrom = -1;
|
||||
if (m_constraint.maxHeight > 0.0f)
|
||||
int scrollFirst = 0;
|
||||
if (m_constraint.maxHeight > 0.0f || m_constraint.maxScrollRows > 0)
|
||||
{
|
||||
for (int index = static_cast<int>(m_items.size()) - 1; index > 0; --index)
|
||||
{
|
||||
@@ -187,45 +193,77 @@ namespace jdi
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (scrollFirst < pinnedFrom &&
|
||||
(m_items[static_cast<std::size_t>(scrollFirst)].kind == MenuItemKind::HEADER ||
|
||||
m_items[static_cast<std::size_t>(scrollFirst)].kind == MenuItemKind::HEADER_ACTION))
|
||||
{
|
||||
++scrollFirst; // 前导标题固定在顶部,不参与滚动
|
||||
}
|
||||
}
|
||||
|
||||
float headHeight = 0.0f;
|
||||
float scrollRegionHeight = 0.0f;
|
||||
float tailHeight = 0.0f;
|
||||
if (pinnedFrom > 0)
|
||||
{
|
||||
for (int index = 0; index < pinnedFrom; ++index)
|
||||
for (int index = 0; index < static_cast<int>(m_items.size()); ++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)];
|
||||
if (index >= pinnedFrom)
|
||||
{
|
||||
tailHeight += heights[static_cast<std::size_t>(index)];
|
||||
}
|
||||
else if (index < scrollFirst)
|
||||
{
|
||||
headHeight += heights[static_cast<std::size_t>(index)];
|
||||
}
|
||||
else
|
||||
{
|
||||
scrollRegionHeight += heights[static_cast<std::size_t>(index)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float height = totalHeight;
|
||||
m_scrollEnabled = false;
|
||||
m_scrollRegionHeight = 0.0f;
|
||||
m_scrollFirst = 0;
|
||||
m_pinnedIndex = -1;
|
||||
m_scrollViewport = gd::RectF();
|
||||
// ⚠️ **不要在这里无条件清零 `m_scrollOffset`**:`RebuildLayout()`(滚轮 / 数值回写触发)
|
||||
m_scrollBarRect = gd::RectF();
|
||||
// ⚠️ **不要在这里复位 `m_scrollBarDragging`**:拖拽滚动条时每一帧都会 `RebuildLayout()`,
|
||||
// 在这里清零会把拖拽态当场吃掉(拖一下断一下)。该状态只在 `Open` / `Close` 里复位。
|
||||
// ⚠️ **不要在这里无条件清零 `m_scrollOffset`**:`RebuildLayout()`(滚轮 / 数值回写 / 拖滚动条)
|
||||
// 会重新走一遍本函数,清零就等于把滚动位置吃掉(滚轮“滚不动”)。只有在菜单**不再需要**
|
||||
// 滚动时才复位(`Open` 里已经为每次打开重置过)。
|
||||
if (m_constraint.maxHeight > 0.0f)
|
||||
if (pinnedFrom > 0)
|
||||
{
|
||||
// 夹到宿主矩形以内(不盖住窗口边框);小到放不下时的上限直接视作“不限制”,
|
||||
// 交给下方的边界收敛处理,避免算出一个高度为 0 的菜单。
|
||||
float limit = std::min(m_constraint.maxHeight, bounds.Height - 4.0f * m_scale);
|
||||
if (limit < 40.0f * m_scale)
|
||||
// 滚动视口高度上限:`maxHeight`(客户区像素 → 换算成视口可用高)与
|
||||
// `maxScrollRows`(标准行数)**取更严格的那个**;0 = 该约束不生效。
|
||||
float viewportLimit = 0.0f;
|
||||
if (m_constraint.maxHeight > 0.0f)
|
||||
{
|
||||
limit = 0.0f;
|
||||
// 夹到宿主矩形以内(不盖住窗口边框);小到放不下时的上限直接视作“不限制”,
|
||||
// 交给下方的边界收敛处理,避免算出一个高度为 0 的菜单。
|
||||
const float menuLimit = std::min(m_constraint.maxHeight, bounds.Height - 4.0f * m_scale);
|
||||
if (menuLimit >= 40.0f * m_scale)
|
||||
{
|
||||
viewportLimit = std::max(
|
||||
0.0f, menuLimit - kPadV * 2.0f * m_scale - headHeight - tailHeight);
|
||||
}
|
||||
}
|
||||
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)
|
||||
if (m_constraint.maxScrollRows > 0)
|
||||
{
|
||||
height = limit;
|
||||
const float rows =
|
||||
static_cast<float>(m_constraint.maxScrollRows) * kRowHeight * m_scale;
|
||||
viewportLimit = (viewportLimit > 0.0f) ? std::min(viewportLimit, rows) : rows;
|
||||
}
|
||||
|
||||
if (viewportLimit >= kRowHeight * m_scale && scrollRegionHeight > viewportLimit + 0.5f)
|
||||
{
|
||||
const float viewportHeight = std::min(scrollRegionHeight, viewportLimit);
|
||||
height = kPadV * 2.0f * m_scale + headHeight + viewportHeight + tailHeight;
|
||||
m_scrollEnabled = true;
|
||||
m_scrollFirst = scrollFirst;
|
||||
m_pinnedIndex = pinnedFrom;
|
||||
m_scrollRegionHeight = scrollRegionHeight;
|
||||
m_scrollOffset = std::max(
|
||||
@@ -286,39 +324,52 @@ namespace jdi
|
||||
|
||||
m_rect = gd::RectF(left, top, width, height);
|
||||
|
||||
// 条目排布:滚动区的条目整体上移 `m_scrollOffset`;固定区从菜单底部往上对齐。
|
||||
// 条目排布:头部固定区从菜单顶往下排;滚动区条目整体上移 `m_scrollOffset`;
|
||||
// 尾部固定区从菜单底部往上对齐。
|
||||
const float bodyTop = top + kPadV * m_scale;
|
||||
const float pinnedTop = top + height - kPadV * m_scale - tailHeight;
|
||||
const float tailTop = top + height - kPadV * m_scale - tailHeight;
|
||||
const float scrollBarReserve = m_scrollEnabled ? (kScrollBarWidth + kScrollBarGap) * m_scale : 0.0f;
|
||||
m_scrollViewport =
|
||||
m_scrollEnabled
|
||||
? gd::RectF(left, bodyTop, width,
|
||||
std::max(0.0f, height - kPadV * 2.0f * m_scale - tailHeight))
|
||||
? gd::RectF(left + kPadH * m_scale, bodyTop + headHeight,
|
||||
std::max(0.0f, width - kPadH * 2.0f * m_scale),
|
||||
std::max(0.0f, height - kPadV * 2.0f * m_scale - headHeight - tailHeight))
|
||||
: gd::RectF();
|
||||
|
||||
float cursorY = bodyTop;
|
||||
float scrollCursorY = bodyTop - m_scrollOffset;
|
||||
float pinnedCursorY = (pinnedFrom > 0) ? pinnedTop : bodyTop;
|
||||
float headCursorY = bodyTop;
|
||||
float scrollCursorY = bodyTop + headHeight - m_scrollOffset;
|
||||
float tailCursorY = (m_scrollEnabled) ? tailTop : bodyTop;
|
||||
for (std::size_t index = 0; index < m_items.size(); ++index)
|
||||
{
|
||||
const MenuItem& item = m_items[index];
|
||||
const float itemHeight = heights[index];
|
||||
|
||||
float rectY = cursorY;
|
||||
float rectWidth = std::max(0.0f, width - kPadH * 2.0f * m_scale);
|
||||
if (m_scrollEnabled)
|
||||
{
|
||||
if (static_cast<int>(index) < m_pinnedIndex)
|
||||
const int itemIndex = static_cast<int>(index);
|
||||
if (itemIndex < m_scrollFirst)
|
||||
{
|
||||
rectY = headCursorY;
|
||||
headCursorY += itemHeight;
|
||||
}
|
||||
else if (itemIndex < m_pinnedIndex)
|
||||
{
|
||||
rectY = scrollCursorY;
|
||||
scrollCursorY += itemHeight;
|
||||
// 滚动区条目让出右侧滚动条的位置(对勾/文本不会压在滑块上)。
|
||||
rectWidth = std::max(0.0f, rectWidth - scrollBarReserve);
|
||||
}
|
||||
else
|
||||
{
|
||||
rectY = pinnedCursorY;
|
||||
pinnedCursorY += itemHeight;
|
||||
rectY = tailCursorY;
|
||||
tailCursorY += itemHeight;
|
||||
}
|
||||
}
|
||||
m_itemRects[index] = gd::RectF(left + kPadH * m_scale, rectY,
|
||||
std::max(0.0f, width - kPadH * 2.0f * m_scale), itemHeight);
|
||||
m_itemRects[index] =
|
||||
gd::RectF(left + kPadH * m_scale, rectY, rectWidth, itemHeight);
|
||||
|
||||
if (item.kind == MenuItemKind::SWATCHES)
|
||||
{
|
||||
@@ -337,11 +388,78 @@ namespace jdi
|
||||
|
||||
cursorY += itemHeight;
|
||||
}
|
||||
|
||||
// 滚动条轨道:贴在可滚动条目区的右内侧(只在条目区旁边,不会盖住固定区)。
|
||||
if (m_scrollEnabled)
|
||||
{
|
||||
const float thickness = kScrollBarWidth * m_scale;
|
||||
m_scrollBarRect = gd::RectF(left + width - kPadH * m_scale - thickness,
|
||||
m_scrollViewport.Y, thickness, m_scrollViewport.Height);
|
||||
}
|
||||
}
|
||||
|
||||
bool PopupMenu::ScrollRegion(int& first, int& last) const
|
||||
{
|
||||
if (!m_scrollEnabled || m_pinnedIndex < 0)
|
||||
{
|
||||
first = 0;
|
||||
last = static_cast<int>(m_items.size());
|
||||
return false;
|
||||
}
|
||||
first = m_scrollFirst;
|
||||
last = m_pinnedIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
gd::RectF PopupMenu::ScrollBarThumbRect() const
|
||||
{
|
||||
if (!m_scrollEnabled || m_scrollBarRect.Height <= 0.0f || m_scrollRegionHeight <= 0.0f)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
const float viewport = m_scrollViewport.Height;
|
||||
if (viewport <= 0.0f)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
const float ratio = std::min(1.0f, viewport / m_scrollRegionHeight);
|
||||
const float thumbHeight = std::max(kScrollBarMinThumb * m_scale, m_scrollBarRect.Height * ratio);
|
||||
const float maxScroll = std::max(0.0f, m_scrollRegionHeight - viewport);
|
||||
const float progress = (maxScroll > 0.0f) ? (m_scrollOffset / maxScroll) : 0.0f;
|
||||
const float thumbY =
|
||||
m_scrollBarRect.Y +
|
||||
std::max(0.0f, std::min(1.0f, progress)) * std::max(0.0f, m_scrollBarRect.Height - thumbHeight);
|
||||
return gd::RectF(m_scrollBarRect.X, thumbY, m_scrollBarRect.Width, thumbHeight);
|
||||
}
|
||||
|
||||
float PopupMenu::ScrollOffsetForThumbTop(float thumbTop) const
|
||||
{
|
||||
if (!m_scrollEnabled || m_scrollBarRect.Height <= 0.0f)
|
||||
{
|
||||
return m_scrollOffset;
|
||||
}
|
||||
const float viewport = m_scrollViewport.Height;
|
||||
const float maxScroll = std::max(0.0f, m_scrollRegionHeight - viewport);
|
||||
if (maxScroll <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
const float track = m_scrollBarRect.Height;
|
||||
const float thumbHeight = std::max(kScrollBarMinThumb * m_scale, track * std::min(1.0f, viewport / std::max(1.0f, m_scrollRegionHeight)));
|
||||
const float travel = std::max(0.0f, track - thumbHeight);
|
||||
if (travel <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
const float relative = thumbTop - m_scrollBarRect.Y;
|
||||
const float progress = std::max(0.0f, std::min(1.0f, relative / travel));
|
||||
return progress * maxScroll;
|
||||
}
|
||||
|
||||
bool PopupMenu::EnsureItemVisible(int index)
|
||||
{
|
||||
if (!m_scrollEnabled || index < 0 || index >= m_pinnedIndex || m_scrollViewport.Height <= 0.0f)
|
||||
if (!m_scrollEnabled || index < m_scrollFirst || index >= m_pinnedIndex ||
|
||||
m_scrollViewport.Height <= 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -795,6 +913,7 @@ namespace jdi
|
||||
// 每次打开都从顶部开始(滚动位置不跨次沿用)。
|
||||
m_constraint = constraint;
|
||||
m_scrollOffset = 0.0f;
|
||||
m_scrollBarDragging = false;
|
||||
|
||||
CloseSubmenu();
|
||||
CloseEditState();
|
||||
@@ -814,11 +933,15 @@ namespace jdi
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
m_constraint = MenuLayoutConstraint();
|
||||
m_scrollFirst = 0;
|
||||
m_pinnedIndex = -1;
|
||||
m_scrollEnabled = false;
|
||||
m_scrollOffset = 0.0f;
|
||||
m_scrollRegionHeight = 0.0f;
|
||||
m_scrollViewport = gd::RectF();
|
||||
m_scrollBarRect = gd::RectF();
|
||||
m_scrollBarDragging = false;
|
||||
m_scrollBarGrabOffset = 0.0f;
|
||||
CloseSubmenu();
|
||||
CloseEditState();
|
||||
}
|
||||
@@ -872,8 +995,9 @@ namespace jdi
|
||||
}
|
||||
// 可滚动菜单:滚动区内的条目还必须落在**滚动视口**内 ——
|
||||
// 夹在视口边界上、只有一部分可见的条目,只有落在视口内的那一部分可以点击。
|
||||
if (m_scrollEnabled && static_cast<int>(index) < m_pinnedIndex &&
|
||||
!UiPointInRect(m_scrollViewport, fx, fy))
|
||||
// ⚠️ 前导标题与末尾固定项不在滚动区内,它们始终完整可见(不参与本判定)。
|
||||
if (m_scrollEnabled && static_cast<int>(index) >= m_scrollFirst &&
|
||||
static_cast<int>(index) < m_pinnedIndex && !UiPointInRect(m_scrollViewport, fx, fy))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -978,6 +1102,15 @@ namespace jdi
|
||||
const float fy = static_cast<float>(y);
|
||||
bool changed = false;
|
||||
|
||||
// 正在拖拽滚动条滑块:忽略悬停,只跟随鼠标滚动条目区。
|
||||
if (m_scrollBarDragging)
|
||||
{
|
||||
const float previous = m_scrollOffset;
|
||||
m_scrollOffset = ScrollOffsetForThumbTop(fy - m_scrollBarGrabOffset);
|
||||
RebuildLayout();
|
||||
return std::fabs(m_scrollOffset - previous) > 0.01f;
|
||||
}
|
||||
|
||||
// 鼠标在展开的子菜单内:只更新子菜单的悬停项(主菜单父项保持高亮)。
|
||||
if (m_subOpen && UiPointInRect(m_subRect, fx, fy))
|
||||
{
|
||||
@@ -1098,6 +1231,34 @@ namespace jdi
|
||||
EndItemEdit(true, result);
|
||||
}
|
||||
|
||||
// 可滚动菜单的滚动条:按下即开始拖拽(不是条目点击,菜单保持打开)。
|
||||
if (m_scrollEnabled)
|
||||
{
|
||||
const gd::RectF thumb = ScrollBarThumbRect();
|
||||
if (thumb.Width > 0.0f && UiPointInRect(thumb, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
const float previous = m_scrollOffset;
|
||||
m_scrollBarDragging = true;
|
||||
m_scrollBarGrabOffset = static_cast<float>(y) - thumb.Y;
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
RebuildLayout();
|
||||
(void)previous;
|
||||
return true;
|
||||
}
|
||||
if (UiPointInRect(m_scrollBarRect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
// 点在轨道空白处:把滑块中心移到该处(翻页)。
|
||||
m_scrollBarDragging = true;
|
||||
m_scrollBarGrabOffset = thumb.Height * 0.5f;
|
||||
m_scrollOffset = ScrollOffsetForThumbTop(static_cast<float>(y) - m_scrollBarGrabOffset);
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
RebuildLayout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int swatchIndex = -1;
|
||||
m_pressedIndex = ItemAtPoint(x, y, swatchIndex);
|
||||
m_pressedSwatch = swatchIndex;
|
||||
@@ -1113,6 +1274,15 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 结束滚动条拖拽(不当作条目点击,菜单保持打开)。
|
||||
if (m_scrollBarDragging)
|
||||
{
|
||||
m_scrollBarDragging = false;
|
||||
m_pressedIndex = -1;
|
||||
m_pressedSwatch = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 子菜单内的抬起:按下与抬起命中同一项才提交(菜单随之关闭)。
|
||||
if (m_subOpen && m_subPressedIndex >= 0)
|
||||
{
|
||||
@@ -1378,35 +1548,42 @@ namespace jdi
|
||||
}
|
||||
};
|
||||
|
||||
// 可滚动菜单:条目 [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);
|
||||
}
|
||||
// 可滚动菜单的条目分三段:
|
||||
// * `[0, m_scrollFirst)`:**前导标题**(「所属网格」这类)—— 钉在菜单顶部,**不参与滚动**;
|
||||
// * `[m_scrollFirst, m_pinnedIndex)`:滚动区(裁剪到滚动视口,右侧有滚动条);
|
||||
// * `[m_pinnedIndex, end)`:末尾固定项(最后一个分隔线 + 「新建网格」)钉在菜单底部。
|
||||
const bool scrollable = m_scrollEnabled && m_pinnedIndex > 0;
|
||||
const int scrollFirst = scrollable ? m_scrollFirst : static_cast<int>(m_items.size());
|
||||
const int scrollLast = scrollable ? m_pinnedIndex : static_cast<int>(m_items.size());
|
||||
int clipState = 0;
|
||||
bool clipActive = false;
|
||||
|
||||
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)
|
||||
const int itemIndex = static_cast<int>(index);
|
||||
// 进入/离开滚动区:按需开启/撤销 GDI+ 裁剪(固定区永远完整可见)。
|
||||
if (scrollable && itemIndex == scrollFirst && !clipActive)
|
||||
{
|
||||
clipState = g.Save();
|
||||
g.SetClip(m_scrollViewport, gd::CombineModeIntersect);
|
||||
clipActive = true;
|
||||
}
|
||||
else if (scrollable && itemIndex == scrollLast && clipActive)
|
||||
{
|
||||
g.Restore(clipState);
|
||||
clipActive = false;
|
||||
}
|
||||
const bool inScrollRegion = static_cast<int>(index) < pinnedFrom;
|
||||
const bool inScrollRegion = itemIndex >= scrollFirst && itemIndex < scrollLast;
|
||||
// 滚动区里完全不可见的条目直接跳过:长菜单(上百条)逐条绘制会明显卡顿。
|
||||
if (m_scrollEnabled && inScrollRegion &&
|
||||
if (scrollable && inScrollRegion &&
|
||||
(rect.Y + rect.Height <= m_scrollViewport.Y ||
|
||||
rect.Y >= m_scrollViewport.Y + m_scrollViewport.Height))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const bool clipped = m_scrollEnabled && inScrollRegion;
|
||||
const bool clipped = scrollable && inScrollRegion;
|
||||
|
||||
if (item.kind == MenuItemKind::SEPARATOR)
|
||||
{
|
||||
@@ -1590,9 +1767,23 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 恢复滚动区的裁剪(若已因固定区而恢复过则 `Restore` 会成对生效)。
|
||||
if (m_scrollEnabled)
|
||||
if (clipActive)
|
||||
{
|
||||
g.Restore(clipState);
|
||||
clipActive = false;
|
||||
}
|
||||
|
||||
// 滚动条:轨道 + 滑块(画在条目之上,仅在可滚动菜单里出现)。
|
||||
// 位置限制在可滚动条目区旁边,因此不会盖住顶部标题与底部「新建网格」。
|
||||
if (scrollable && m_scrollBarRect.Width > 0.0f)
|
||||
{
|
||||
const float radius = kScrollBarWidth * m_scale * 0.5f;
|
||||
UiFillRoundRect(g, m_scrollBarRect, radius, m_theme->gridWeak);
|
||||
const gd::RectF thumb = ScrollBarThumbRect();
|
||||
if (thumb.Width > 0.0f)
|
||||
{
|
||||
UiFillRoundRect(g, thumb, radius, m_theme->borderStrong);
|
||||
}
|
||||
}
|
||||
|
||||
// 二级子菜单最后绘制,保证覆盖在主菜单之上。
|
||||
|
||||
Reference in new issue
Block a user