自由标记体验重做 + 全部实验性面板可折起
- 网格内固定点:取值胶囊按已放置矩形做纵向避让(不再互相遮挡);行尾 ✕ 改为与 胶囊同高的正方形;移除固定点的纵向半透明竖线 - 新增「标记透明度」(0–100%),作用于本网格全部固定点的圆点/胶囊/描边/文字/✕; 半透明时文字改走 GDI+(GDI DrawTextW 忽略 alpha),100% 时像素零变化; ≤2% 时不绘制且不登记 ✕ 命中矩形(看不见的按钮不可点) - 面板按信号分组:信号色托底分组栏(展开三角 + 信号名 + N 标记点 + ✕,文字/三角/✕ 一律用自适应对比色),点击分组栏收起/展开;条目五列(值/枚举/增量/时刻/间隔), 首项增列与 Δt 显示 N/A,整个面板没有枚举文本时枚举列整列缺省;行尾仍有 ✕ - 面板内容超高时右侧出现滚动条(分组栏与条目一起滚动),滚轮在内容区翻页; 底部按钮与「标记透明度」滑块固定在底部、不参与滚动 - 底部按钮改为「光标选取模式」切换类按钮(外观与工具栏切换按钮一致,激活高亮), 激活时其上方出现「标记透明度」滑动条(与信号样式区的透明度滑块同款) - 所有实验性功能浮动面板支持展开/折起:标题栏 GX 徽标左侧新增「三角 + 横线」指示器, 单击切换、双击标题栏切换;折起的面板只有标题栏 - 数据层:LanePanelState.collapsed、Lane.freeMarkerOpacity/freeMarkerScroll/ freeMarkerCollapsedGroups,LaneCarry 与 SplitLane 一并继承 - 验证:新增离屏 .preflight/perf/mark2check 69/69(含两组 A/B);回归 datacheck 69/69、 verify 49/49、panelanim 158/158、errcheck 19/19;make release/debug 零警告
This commit is contained in:
1 parent
067128f46c
commit
3400ac7c29
9 files changed
+1225
-202
No files matched your search
@@ -5342,6 +5342,14 @@ namespace jdi
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
// 面板的**折起**状态(只显示标题栏)。折起时仍然算宽度与位置,但内容区全部省略。
|
||||
const bool collapsed = meanFilter ? lane.meanFilterPanel.collapsed
|
||||
: (weakPeak ? lane.weakPeakPanel.collapsed
|
||||
: (freeMarker ? lane.freeMarkerPanel.collapsed
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.collapsed
|
||||
: lane.edgePanel.collapsed)));
|
||||
layout.collapsed = collapsed;
|
||||
|
||||
int rowCount = 0;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
@@ -5415,8 +5423,10 @@ namespace jdi
|
||||
height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal + pad;
|
||||
}
|
||||
|
||||
// 「自由标记」面板:逐信号一行名称(**只列出确实有固定点的信号**),下面跟着它的全部固定点;
|
||||
// 没有任何固定点时给一行灰色提示,最后始终带一行底部切换按钮。
|
||||
// 「自由标记」面板:按信号分组(**分组栏** + 下属固定点条目),
|
||||
// 只列出确实有固定点的信号;没有任何固定点时给一行灰色提示。
|
||||
// 内容区超高时出现滚动条;面板底部固定「标记透明度」滑块(仅光标模式激活时)
|
||||
// 与「光标选取模式」切换按钮(二者**不参与**滚动)。
|
||||
bool markerAny = false;
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
{
|
||||
@@ -5428,9 +5438,11 @@ namespace jdi
|
||||
}
|
||||
if (freeMarker)
|
||||
{
|
||||
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float groupRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
|
||||
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
|
||||
const float sliderRow =
|
||||
lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f;
|
||||
float content = 0.0f;
|
||||
std::vector<std::size_t> order;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
@@ -5440,22 +5452,37 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// ⚠️ 用户明确要求:某个信号没有任何固定点时,它的名称条目**不出现**在面板上。
|
||||
// ⚠️ 用户明确要求:某个信号没有任何固定点时,它的分组栏**不出现**在面板上。
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
if (order.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
content += nameRow;
|
||||
content += static_cast<float>(order.size()) * itemRow;
|
||||
content += groupRow;
|
||||
if (!lane.FreeMarkerGroupCollapsed(ref))
|
||||
{
|
||||
content += static_cast<float>(order.size()) * itemRow;
|
||||
}
|
||||
}
|
||||
if (!markerAny)
|
||||
{
|
||||
// 还没有任何固定点:第一行留作提示文案(绘制时画「在网格内单击以固定数据点」)。
|
||||
content += nameRow;
|
||||
// 还没有任何固定点:留一行提示文案(绘制时画「在网格内单击以固定数据点」)。
|
||||
content += groupRow;
|
||||
}
|
||||
// 底部「启用光标模式 / 关闭光标模式」按钮:上下各留一个 pad(与左右内边距一致)。
|
||||
height = titleHeight + pad + content + pad + markerButtonHeight + pad;
|
||||
// 内容区高度上限:太长的列表不能把面板顶出绘图区(超出部分靠滚动条翻页)。
|
||||
const float budget =
|
||||
m_plotRect.Height - (titleHeight + pad * 3.0f + markerButtonHeight + sliderRow);
|
||||
const float maxContent =
|
||||
std::max(2.0f * itemRow,
|
||||
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(2.0f * itemRow, budget)));
|
||||
const float view = std::min(content, maxContent);
|
||||
height = titleHeight + pad + view + pad + sliderRow + markerButtonHeight + pad;
|
||||
}
|
||||
|
||||
// 折起的面板:只剩标题栏(内容区完全不占高度)。
|
||||
if (collapsed)
|
||||
{
|
||||
height = titleHeight;
|
||||
}
|
||||
|
||||
// 首次使用(位置为负)时自动放到绘图区**右上角**,并按网格在**当前可见网格中的序数**
|
||||
@@ -5546,10 +5573,22 @@ namespace jdi
|
||||
layout.title = gd::RectF(left, top, width, titleHeight);
|
||||
const float closeSize = titleHeight;
|
||||
layout.close = gd::RectF(left + width - closeSize, top, closeSize, closeSize);
|
||||
// 标题栏左侧的「展开 / 折起」三角指示器(位于 GX 徽标左边)。
|
||||
// 折起后面板只剩标题栏,因此这个指示器是唯一的“重新展开”入口(双击标题栏亦可)。
|
||||
layout.disclosure = gd::RectF(left + pad, top + (titleHeight - kEdgePanelDisclosureWidth * m_scale) * 0.5f,
|
||||
kEdgePanelDisclosureWidth * m_scale,
|
||||
kEdgePanelDisclosureWidth * m_scale);
|
||||
// 标题栏左侧的「GX」编号徽标:绘制、悬停提示、手型光标与“点击滚动到该网格”
|
||||
// 全部以这个矩形为准(唯一几何来源,避免“画的”和“点的”漂移)。
|
||||
layout.badge = gd::RectF(left + pad, top + (titleHeight - kLaneBadgeHeight * m_scale) * 0.5f,
|
||||
kLaneBadgeWidth * m_scale, kLaneBadgeHeight * m_scale);
|
||||
layout.badge =
|
||||
gd::RectF(left + pad + kEdgePanelDisclosureWidth * m_scale,
|
||||
top + (titleHeight - kLaneBadgeHeight * m_scale) * 0.5f, kLaneBadgeWidth * m_scale,
|
||||
kLaneBadgeHeight * m_scale);
|
||||
// 折起状态:到此为止(内容区、按钮、滚动条全部为空)。
|
||||
if (collapsed)
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
if (itemized)
|
||||
{
|
||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||
@@ -5688,26 +5727,44 @@ namespace jdi
|
||||
|
||||
if (freeMarker)
|
||||
{
|
||||
// 「自由标记」面板:**只列出确实有固定点的信号**,每个信号一行名称
|
||||
//(右侧带「清除本信号全部固定点」✕),下面逐行列出它的固定点
|
||||
//(组内按时间升序 == 从左到右),行尾再带一个 ✕。
|
||||
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
// 「自由标记」面板:**只列出确实有固定点的信号**,每个信号一个**分组栏**
|
||||
//(信号色托底 + 左侧展开/收起三角 + 信号名 + 右侧「N 标记点」与 ✕),
|
||||
// 下面逐行列出它的固定点(组内按时间升序 == 从上到下),行尾再带一个 ✕。
|
||||
// 内容区超高时右侧出现滚动条;底部按钮与「标记透明度」滑块**固定**不滚动。
|
||||
const float groupRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = 4.0f * m_scale;
|
||||
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
|
||||
const float sliderRow = lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f;
|
||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||
|
||||
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)从左往右**固定列宽**排布:
|
||||
// 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。
|
||||
float columnX = left + pad;
|
||||
for (int column = 0; column < 4; ++column)
|
||||
// 底部固定区:切换按钮(整行宽度、与面板下边界留一个 pad)。
|
||||
layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
|
||||
std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
|
||||
if (sliderRow > 0.0f)
|
||||
{
|
||||
layout.markerColumns[column].left = columnX;
|
||||
layout.markerColumns[column].width = kFreeMarkerColumnWidths[column] * m_scale;
|
||||
columnX += layout.markerColumns[column].width + kFreeMarkerColumnGap * m_scale;
|
||||
// 「标记透明度」行:与信号样式区的「透明度」行同一套排版
|
||||
//(左侧标签 + 中间轨道 + 右侧百分比),只是位置固定在按钮上方。
|
||||
const float rowTop = layout.button.Y - sliderRow;
|
||||
const float valueWidth =
|
||||
std::max(36.0f * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"100%") + 6.0f * m_scale);
|
||||
const float labelWidth =
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kFreeMarkerOpacityLabel) + 6.0f * m_scale;
|
||||
const float trackLeft = left + pad + labelWidth;
|
||||
const float trackRight = left + width - pad - valueWidth - 6.0f * m_scale;
|
||||
const float trackHeight = kWeakPeakTrackHeight * m_scale;
|
||||
layout.opacityTrack =
|
||||
gd::RectF(trackLeft, rowTop + (sliderRow - trackHeight) * 0.5f,
|
||||
std::max(20.0f * m_scale, trackRight - trackLeft), trackHeight);
|
||||
layout.opacityValue =
|
||||
gd::RectF(trackRight + 6.0f * m_scale, rowTop, valueWidth, sliderRow);
|
||||
}
|
||||
|
||||
float rowY = top + titleHeight + pad;
|
||||
// 内容区高度(面板高度算式里的 `view` 部分)与"内容总高"决定是否需要滚动条。
|
||||
const float viewHeight = std::max(
|
||||
0.0f, height - titleHeight - pad * 3.0f - markerButtonHeight - sliderRow);
|
||||
float totalContent = 0.0f;
|
||||
std::vector<std::size_t> order;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
@@ -5716,35 +5773,124 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 该信号没有任何固定点 ⇒ 名称条目与数据行都**不出现**(用户明确要求)。
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
if (order.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
layout.rowRefs.push_back(ref);
|
||||
layout.rowNameRects.push_back(gd::RectF(left + pad, rowY,
|
||||
std::max(0.0f, width - pad * 2.0f - closeSize - gap),
|
||||
nameRow));
|
||||
layout.rowNameCloses.push_back(
|
||||
gd::RectF(left + width - pad - closeSize, rowY + (nameRow - closeSize) * 0.5f,
|
||||
closeSize, closeSize));
|
||||
rowY += nameRow;
|
||||
totalContent += groupRow;
|
||||
if (!lane.FreeMarkerGroupCollapsed(ref))
|
||||
{
|
||||
totalContent += static_cast<float>(order.size()) * itemRow;
|
||||
}
|
||||
}
|
||||
if (!markerAny)
|
||||
{
|
||||
totalContent += groupRow;
|
||||
}
|
||||
const bool scrollable = totalContent > viewHeight + 0.5f;
|
||||
const float maxScroll = std::max(0.0f, totalContent - viewHeight);
|
||||
const float scroll = std::max(0.0f, std::min(lane.freeMarkerScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
layout.content =
|
||||
gd::RectF(left + pad, top + titleHeight + pad, innerWidth, viewHeight);
|
||||
layout.markerContentHeight = totalContent;
|
||||
if (scrollable)
|
||||
{
|
||||
layout.scrollTrack =
|
||||
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||
viewHeight);
|
||||
const float thumbHeight =
|
||||
std::max(18.0f * m_scale, viewHeight * (viewHeight / totalContent));
|
||||
const float thumbTop =
|
||||
layout.content.Y +
|
||||
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||
}
|
||||
|
||||
// 五列(① 取值 ② 枚举 ③ 增量 ④ 时刻 ⑤ 间隔)从左往右**固定列宽**排布:
|
||||
// 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。
|
||||
// ⚠️ 整个面板里没有任何枚举文本时,② 枚举列**整列缺省**(列宽记 0,后面的列左移)。
|
||||
// 这里只查"该信号是否带枚举表"(O(1)),**不**逐条采样(布局函数被高频调用)。
|
||||
bool hasEnum = false;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data != nullptr && resolved.visible && !resolved.data->enumText.Empty() &&
|
||||
lane.HasFreeMarkers(ref))
|
||||
{
|
||||
hasEnum = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
layout.markerHasEnumColumn = hasEnum;
|
||||
float columnX = left + pad;
|
||||
for (int column = 0; column < 5; ++column)
|
||||
{
|
||||
const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum;
|
||||
const float columnWidth =
|
||||
omitted ? 0.0f : (kFreeMarkerColumnWidths[column] * m_scale);
|
||||
layout.markerColumns[column].left = columnX;
|
||||
layout.markerColumns[column].width = columnWidth;
|
||||
columnX += columnWidth + (omitted ? 0.0f : kFreeMarkerColumnGap * m_scale);
|
||||
}
|
||||
|
||||
const float contentBottom = layout.content.Y + layout.content.Height;
|
||||
float rowY = layout.content.Y - scroll;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 该信号没有任何固定点 ⇒ 分组栏与数据行都**不出现**(用户明确要求)。
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
if (order.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EdgePanelLayout::MarkerGroup group;
|
||||
group.ref = ref;
|
||||
group.collapsed = lane.FreeMarkerGroupCollapsed(ref);
|
||||
group.header = gd::RectF(layout.content.X, rowY, innerWidth, groupRow);
|
||||
group.triangle = gd::RectF(layout.content.X + 3.0f * m_scale,
|
||||
rowY + (groupRow - 12.0f * m_scale) * 0.5f,
|
||||
12.0f * m_scale, 12.0f * m_scale);
|
||||
group.close = gd::RectF(layout.content.X + innerWidth - closeSize - 2.0f * m_scale,
|
||||
rowY + (groupRow - closeSize) * 0.5f, closeSize, closeSize);
|
||||
if (group.header.Y + group.header.Height > layout.content.Y &&
|
||||
group.header.Y < contentBottom)
|
||||
{
|
||||
// 只登记"在可视内容区里"的分组栏与条目(滚出视界的条目不得抢走
|
||||
// 底部按钮 / 滑块的点击)。
|
||||
layout.markerGroups.push_back(group);
|
||||
layout.rowRefs.push_back(ref);
|
||||
layout.rowNameRects.push_back(group.header);
|
||||
layout.rowNameCloses.push_back(group.close);
|
||||
}
|
||||
rowY += groupRow;
|
||||
if (group.collapsed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const std::size_t itemIndex : order)
|
||||
{
|
||||
EdgePanelLayout::MarkerRow markerRow;
|
||||
markerRow.itemIndex = itemIndex;
|
||||
markerRow.rect = gd::RectF(left + pad, rowY, width - pad * 2.0f, itemRow);
|
||||
markerRow.close = gd::RectF(left + width - pad - closeSize,
|
||||
rowY + (itemRow - closeSize) * 0.5f, closeSize, closeSize);
|
||||
layout.markerRows.push_back(markerRow);
|
||||
markerRow.rect = gd::RectF(layout.content.X, rowY, innerWidth, itemRow);
|
||||
markerRow.close = gd::RectF(layout.content.X + innerWidth - closeSize,
|
||||
rowY + (itemRow - closeSize) * 0.5f, closeSize,
|
||||
closeSize);
|
||||
if (markerRow.rect.Y + markerRow.rect.Height > layout.content.Y &&
|
||||
markerRow.rect.Y < contentBottom)
|
||||
{
|
||||
layout.markerRows.push_back(markerRow);
|
||||
}
|
||||
rowY += itemRow;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部切换按钮:整行宽度、与面板下边界留一个 pad。
|
||||
layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
|
||||
std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
@@ -6207,9 +6353,17 @@ namespace jdi
|
||||
mixRect(layout.panel);
|
||||
mixRect(layout.title);
|
||||
mixRect(layout.close);
|
||||
mixRect(layout.disclosure);
|
||||
mixRect(layout.badge);
|
||||
mixRect(layout.edit);
|
||||
mixRect(layout.button);
|
||||
mixRect(layout.content);
|
||||
mixRect(layout.scrollTrack);
|
||||
mixRect(layout.scrollThumb);
|
||||
mixRect(layout.opacityTrack);
|
||||
mixRect(layout.opacityValue);
|
||||
mix(layout.collapsed ? 1ull : 0ull);
|
||||
mix(layout.markerHasEnumColumn ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(layout.rows));
|
||||
for (const gd::RectF& rect : layout.rowChecks)
|
||||
{
|
||||
@@ -6247,12 +6401,25 @@ namespace jdi
|
||||
mixFloat(column.left);
|
||||
mixFloat(column.width);
|
||||
}
|
||||
// 「自由标记」的分组栏(托底矩形 / 三角 / ✕)与各自的收起状态都在面板像素里。
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
mixRect(group.header);
|
||||
mixRect(group.triangle);
|
||||
mixRect(group.close);
|
||||
mix(group.collapsed ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(group.ref.groupIndex + 2));
|
||||
mix(static_cast<std::uint64_t>(group.ref.signalIndex + 2));
|
||||
}
|
||||
|
||||
const Lane& lane = Tab().Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (kind == EdgePanelKind::MARK)
|
||||
{
|
||||
// 底部切换按钮的文案取决于光标模式 ⇒ 必须进键,否则开关后仍贴回旧像素。
|
||||
// 底部切换按钮的激活态、透明度滑块与内容滚动偏移直接改变面板像素 ⇒ 必须进键,
|
||||
// 否则开关 / 拖滑块 / 滚动之后仍会贴回旧像素。
|
||||
mix(lane.freeMarkerCursor ? 1ull : 0ull);
|
||||
mixFloat(lane.freeMarkerOpacity);
|
||||
mixFloat(lane.freeMarkerScroll);
|
||||
// 固定点的**内容**(哪个信号、什么时刻)直接决定面板文字与行数,必须进键;
|
||||
// 否则新增/删除一个固定点后可能贴回旧像素。
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
@@ -6499,10 +6666,11 @@ namespace jdi
|
||||
: nullptr;
|
||||
text = FreeMarkerValueText(value, hasPrev, delta, enumText);
|
||||
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = kFreeMarkerCloseGap * m_scale;
|
||||
// ✕ 按钮 = **与取值胶囊同高的正方形**(用户明确要求:长宽都等于胶囊的上下高度)。
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale;
|
||||
const float textHeight = 17.0f * m_scale;
|
||||
const float closeSize = textHeight;
|
||||
const float gap = kFreeMarkerCloseGap * m_scale;
|
||||
const float totalWidth = textWidth + gap + closeSize;
|
||||
|
||||
float left = rawX + 6.0f * m_scale;
|
||||
@@ -6516,28 +6684,93 @@ namespace jdi
|
||||
top = std::max(clipTop + 1.0f * m_scale, std::min(top, clipBottom - textHeight - 1.0f * m_scale));
|
||||
|
||||
label = gd::RectF(left, top, textWidth, textHeight);
|
||||
close = gd::RectF(left + textWidth + gap, top + (textHeight - closeSize) * 0.5f, closeSize,
|
||||
closeSize);
|
||||
close = gd::RectF(left + textWidth + gap, top, closeSize, closeSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 用 **GDI+** 绘制一段可带 alpha 的文字(仅用于「标记透明度」低于 100% 的胶囊)。
|
||||
*
|
||||
* ⚠️ 界面其余文字一律走 `UiText`(GDI `DrawTextW`)。GDI 文本**忽略 alpha**
|
||||
*(`SetTextColor` 只吃 RGB)⇒ 直接把 alpha 乘进颜色不会让文字淡出。
|
||||
* 因此只有在"固定点被调成半透明"这个特殊状态下才改用 GDI+ `DrawString`:
|
||||
* 那样才能让文字与胶囊一起真正淡出;不透明度为 100% 时仍走 `UiText`,像素零变化。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] text 文本。
|
||||
* @param [IN] font 字体(与 `UiText` 同一 HFONT)。
|
||||
* @param [IN] rect 文本矩形(居中)。
|
||||
* @param [IN] argb 颜色(含目标 alpha)。
|
||||
*/
|
||||
void DrawFreeMarkerAlphaText(gd::Graphics& g, const std::wstring& text, HFONT font,
|
||||
const gd::RectF& rect, std::uint32_t argb)
|
||||
{
|
||||
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LOGFONTW lf{};
|
||||
if (GetObjectW(font, static_cast<int>(sizeof(lf)), &lf) != static_cast<int>(sizeof(lf)))
|
||||
{
|
||||
UiText(g, text, font, rect, argb, TextAlign::CENTER);
|
||||
return;
|
||||
}
|
||||
HDC hdc = g.GetHDC();
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
gd::Font textFont(hdc, &lf);
|
||||
g.ReleaseHDC(hdc);
|
||||
if (textFont.GetLastStatus() != gd::Ok)
|
||||
{
|
||||
UiText(g, text, font, rect, argb, TextAlign::CENTER);
|
||||
return;
|
||||
}
|
||||
gd::SolidBrush brush(ToGdiColor(argb));
|
||||
gd::StringFormat format;
|
||||
format.SetAlignment(gd::StringAlignmentCenter);
|
||||
format.SetLineAlignment(gd::StringAlignmentCenter);
|
||||
format.SetTrimming(gd::StringTrimmingEllipsisCharacter);
|
||||
format.SetFormatFlags(gd::StringFormatFlagsNoWrap);
|
||||
g.DrawString(text.c_str(), static_cast<INT>(text.size()), &textFont, rect, &format, &brush);
|
||||
}
|
||||
|
||||
void PlotView::PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label,
|
||||
const std::wstring& text, std::uint32_t color,
|
||||
const gd::RectF& close, bool closeHot)
|
||||
const gd::RectF& close, bool closeHot, float alphaScale)
|
||||
{
|
||||
const float scale = std::max(0.0f, std::min(1.0f, alphaScale));
|
||||
const auto scaled = [scale](std::uint32_t argb, float base) -> std::uint32_t {
|
||||
const int alpha = static_cast<int>(std::lround(base * scale));
|
||||
return WithAlpha(argb, (alpha <= 0) ? 0 : (alpha >= 255 ? 255 : alpha));
|
||||
};
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
UiFillRoundRect(g, label, 3.0f * m_scale, WithAlpha(color, 0xF0));
|
||||
DrawLabelOutline(g, label, 3.0f * m_scale, ink.outline);
|
||||
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER);
|
||||
if (scale <= 0.02f)
|
||||
{
|
||||
return; // 完全透明:连描边与文字都不画(否则会留下"看不见的"像素)
|
||||
}
|
||||
UiFillRoundRect(g, label, 3.0f * m_scale, scaled(color, 0xF0));
|
||||
DrawLabelOutline(g, label, 3.0f * m_scale, scaled(ink.outline, 255.0f));
|
||||
if (scale >= 0.999f)
|
||||
{
|
||||
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER);
|
||||
}
|
||||
else
|
||||
{
|
||||
// GDI 文本忽略 alpha ⇒ 半透明时必须改用 GDI+ 才能真的淡出(见上面说明)。
|
||||
DrawFreeMarkerAlphaText(g, text, m_fonts->monoSmall, label, scaled(ink.text, 255.0f));
|
||||
}
|
||||
if (close.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ✕ 按钮:悬停时用 danger 底色 + 叉(与面板里的 ✕ 同一套语义)。
|
||||
UiFillRoundRect(g, close, 3.0f * m_scale,
|
||||
closeHot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(color, 0xC0));
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
|
||||
closeHot ? WithAlpha(m_theme->danger, 0x66) : scaled(color, 0xC0));
|
||||
const float crossPad = 4.0f * m_scale;
|
||||
const std::uint32_t crossColor =
|
||||
closeHot ? m_theme->danger : scaled(ink.text, 255.0f);
|
||||
UiDrawLine(g, close.X + crossPad, close.Y + crossPad, close.X + close.Width - crossPad,
|
||||
close.Y + close.Height - crossPad, crossColor, 1.4f * m_scale);
|
||||
UiDrawLine(g, close.X + close.Width - crossPad, close.Y + crossPad, close.X + crossPad,
|
||||
@@ -6560,6 +6793,8 @@ namespace jdi
|
||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||
|
||||
// ---- (1) 已固定的数据点(含各自的 ✕)----
|
||||
// ⚠️ 固定点**不再画纵向细线**(用户 2026-10-21 明确要求);
|
||||
// 取值胶囊彼此重叠时做**纵向避让**(依次尝试下方 / 上方错开),保证都能读清。
|
||||
for (std::size_t laneIndex = 0; laneIndex < count; ++laneIndex)
|
||||
{
|
||||
const Lane& lane = tabLanes[laneIndex];
|
||||
@@ -6567,6 +6802,8 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float markerAlpha = std::max(0.0f, std::min(1.0f, lane.freeMarkerOpacity));
|
||||
std::vector<gd::RectF> placed;
|
||||
for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex)
|
||||
{
|
||||
gd::RectF label;
|
||||
@@ -6592,27 +6829,71 @@ namespace jdi
|
||||
double enumValue = 0.0;
|
||||
const bool valueOk = FreeMarkerValue(lane, item, value, enumValue);
|
||||
|
||||
// 纵向细线(仅限本网格,低透明度):让用户看得出固定点在哪一刻。
|
||||
const float x = TimeToX(item.time);
|
||||
UiDrawLine(g, x, clipTop, x, clipBottom, WithAlpha(signalColor, 0x70),
|
||||
1.0f * m_scale);
|
||||
// ---- 纵向避让:与本次已放置的胶囊重叠时,向下 / 向上交替错开 ----
|
||||
const auto occupied = [&placed](const gd::RectF& rect) {
|
||||
for (const gd::RectF& other : placed)
|
||||
{
|
||||
if (rect.X < other.X + other.Width && other.X < rect.X + rect.Width &&
|
||||
rect.Y < other.Y + other.Height && other.Y < rect.Y + rect.Height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto totalRect = [](const gd::RectF& labelRect, const gd::RectF& closeRect) {
|
||||
const float right = std::max(labelRect.X + labelRect.Width,
|
||||
closeRect.X + closeRect.Width);
|
||||
return gd::RectF(labelRect.X, labelRect.Y, right - labelRect.X,
|
||||
labelRect.Height);
|
||||
};
|
||||
const float step = label.Height + 2.0f * m_scale;
|
||||
const float minTop = clipTop + 1.0f * m_scale;
|
||||
const float maxTop = std::max(minTop, clipBottom - label.Height - 1.0f * m_scale);
|
||||
const float baseTop = label.Y;
|
||||
float chosenTop = baseTop;
|
||||
for (int attempt = 0; attempt <= 8; ++attempt)
|
||||
{
|
||||
const float magnitude = static_cast<float>((attempt + 1) / 2) * step;
|
||||
const float offset =
|
||||
(attempt == 0) ? 0.0f : ((attempt % 2 == 1) ? magnitude : -magnitude);
|
||||
const float top = std::max(minTop, std::min(baseTop + offset, maxTop));
|
||||
gd::RectF candidateLabel = label;
|
||||
gd::RectF candidateClose = close;
|
||||
candidateLabel.Y = top;
|
||||
candidateClose.Y = top;
|
||||
if (!occupied(totalRect(candidateLabel, candidateClose)))
|
||||
{
|
||||
chosenTop = top;
|
||||
break;
|
||||
}
|
||||
}
|
||||
label.Y = chosenTop;
|
||||
close.Y = chosenTop;
|
||||
placed.push_back(totalRect(label, close));
|
||||
|
||||
// 数据点:与光标的数据点同一画法(圆点)。
|
||||
// 数据点:与光标的数据点同一画法(圆点),同样受「标记透明度」控制。
|
||||
const float x = TimeToX(item.time);
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush dot(ToGdiColor(signalColor));
|
||||
const int dotAlpha = static_cast<int>(std::lround(255.0f * markerAlpha));
|
||||
gd::SolidBrush dot(ToGdiColor(WithAlpha(signalColor, dotAlpha)));
|
||||
const float dotY = valueOk ? ValueToY(value, visual) : (clipTop + clipBottom) * 0.5f;
|
||||
g.FillEllipse(&dot, x - radius, dotY - radius, radius * 2.0f, radius * 2.0f);
|
||||
|
||||
// ✕ 按钮的悬停态决定它的配色,因此先算再画胶囊。
|
||||
const bool hot = (m_freeMarkerHoverLane == static_cast<int>(laneIndex) &&
|
||||
m_freeMarkerHoverItem == itemIndex);
|
||||
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot);
|
||||
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, markerAlpha);
|
||||
|
||||
FreeMarkerHit hit;
|
||||
hit.laneIndex = static_cast<int>(laneIndex);
|
||||
hit.itemIndex = itemIndex;
|
||||
hit.close = close;
|
||||
m_freeMarkerHits.push_back(hit);
|
||||
if (markerAlpha > 0.02f)
|
||||
{
|
||||
// 完全透明时不可见 ⇒ 也不该被点中(否则会“隔着看不见的按钮”移除固定点)。
|
||||
m_freeMarkerHits.push_back(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7079,6 +7360,33 @@ namespace jdi
|
||||
UiFillRoundRect(g, gd::RectF(panel.X, panel.Y, panel.Width, titleHeight), 5.0f * m_scale,
|
||||
WithAlpha(m_theme->accent, 0x2A));
|
||||
|
||||
// 标题栏左侧的「展开 / 折起」指示器:三角(朝下 = 已展开、朝右 = 已折起)
|
||||
// + 三角后方的一条短横线(经典展开/折起标识),位于 GX 徽标左边。
|
||||
// 单击它切换折起状态;折起后面板只剩标题栏,这里是唯一的“重新展开”按钮。
|
||||
{
|
||||
const gd::RectF& disclosureRect = layout.disclosure;
|
||||
if (disclosureRect.Width > 0.0f)
|
||||
{
|
||||
const bool disclosureHot =
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(disclosureRect, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
const std::uint32_t disclosureColor =
|
||||
disclosureHot ? m_theme->accent : m_theme->text;
|
||||
const float centerX = disclosureRect.X + 5.5f * m_scale;
|
||||
const float centerY = disclosureRect.Y + disclosureRect.Height * 0.5f;
|
||||
UiDrawTriangleIcon(
|
||||
g,
|
||||
gd::RectF(centerX - 4.5f * m_scale, centerY - 4.5f * m_scale,
|
||||
9.0f * m_scale, 9.0f * m_scale),
|
||||
disclosureColor,
|
||||
layout.collapsed ? IconDirection::RIGHT : IconDirection::DOWN);
|
||||
UiDrawLine(g, centerX + 6.0f * m_scale, centerY,
|
||||
disclosureRect.X + disclosureRect.Width, centerY, disclosureColor,
|
||||
1.4f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
// 标题:与左侧信号面板一致的 “GX” 徽标。
|
||||
// ⚠️ 几何必须取 `layout.badge`(与悬停提示 / 手型光标 / 点击联动同源);
|
||||
// 悬停时给一圈强调色描边,作为“可点击”的视觉提示(用户 2026-10-19 要求)。
|
||||
@@ -7125,6 +7433,9 @@ namespace jdi
|
||||
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.6f * m_scale);
|
||||
|
||||
// 折起的面板只保留标题栏:内容区(条目 / 分组栏 / 按钮 / 滚动条)全部省略。
|
||||
if (!layout.collapsed)
|
||||
{
|
||||
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板都采用「逐条目」排布:
|
||||
// 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目,
|
||||
// 弱峰调整没有参数输入框),中心轴再多第三排 = 对应的统计结果。
|
||||
@@ -7345,68 +7656,101 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「自由标记」面板:逐信号一行名称(带清除全部固定点的 ✕),
|
||||
// 下面按时间升序列出该信号的固定点(`值(±增量) = 枚举` + 右对齐的 `t:… Δt:…` + 行尾 ✕)。
|
||||
// 「自由标记」面板:按信号分组的**分组栏**(信号色托底 + 三角 + 名称 +
|
||||
// 「N 标记点」+ ✕),下属固定点条目五列对齐(取值 / 枚举 / 增量 / 时刻 / 间隔),
|
||||
// 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。
|
||||
if (freeMarker)
|
||||
{
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = 4.0f * m_scale;
|
||||
const float groupRowHeight = kFreeMarkerNameRowHeight * m_scale;
|
||||
bool markerAny = false;
|
||||
for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems)
|
||||
{
|
||||
markerAny = markerAny || item.ref.IsValid();
|
||||
}
|
||||
|
||||
const int contentState = g.Save();
|
||||
g.SetClip(layout.content, gd::CombineModeIntersect);
|
||||
|
||||
if (!markerAny)
|
||||
{
|
||||
UiText(g, L"在网格内单击可固定数据点",
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(panel.X + pad, panel.Y + titleHeight + pad,
|
||||
panel.Width - pad * 2.0f, kFreeMarkerNameRowHeight * m_scale),
|
||||
UiText(g, L"在网格内单击可固定数据点", m_fonts->uiSmall,
|
||||
gd::RectF(layout.content.X, layout.content.Y, layout.content.Width,
|
||||
groupRowHeight),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
|
||||
// ---- 分组栏 ----
|
||||
for (const EdgePanelLayout::MarkerGroup& group : layout.markerGroups)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
const ResolvedSignal resolved = ResolveSignal(group.ref);
|
||||
if (resolved.data == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
// 名称行矩形直接取自布局(与 ✕ 同一套几何,绘制侧不反推)。
|
||||
const gd::RectF nameRect =
|
||||
(rowIndex < layout.rowNameRects.size())
|
||||
? layout.rowNameRects[rowIndex]
|
||||
: gd::RectF(panel.X + pad, panel.Y + titleHeight + pad,
|
||||
panel.Width - pad * 2.0f - (closeSize + gap),
|
||||
kFreeMarkerNameRowHeight * m_scale);
|
||||
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameRect.Width),
|
||||
m_fonts->uiSmall, nameRect, signalColor, TextAlign::LEFT);
|
||||
const std::uint32_t signalColor =
|
||||
(resolved.style != nullptr) ? StyleColor(*resolved.style) : m_theme->border;
|
||||
// 托底 = 信号色;文字一律用**自适应对比色**(与信号面板的组名徽标同规则),
|
||||
// 因此无论信号是亮黄还是深蓝都能看清分组栏上的所有内容。
|
||||
const UiInk ink = UiContrastInk(signalColor);
|
||||
UiFillRoundRect(g, group.header, 3.0f * m_scale, WithAlpha(signalColor, 0xE8));
|
||||
|
||||
// 信号名右侧的 ✕:清除该信号的**全部**固定点。
|
||||
if (rowIndex < layout.rowNameCloses.size() &&
|
||||
layout.rowNameCloses[rowIndex].Width > 0.0f)
|
||||
// 左侧三角(与信号选择区域组名同款):朝下 = 展开、朝右 = 收起。
|
||||
const bool triangleHot =
|
||||
(m_freeMarkerTriangleHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerTriangleHoverSignal == group.ref);
|
||||
UiDrawTriangleIcon(g, group.triangle,
|
||||
triangleHot ? m_theme->accentSoft : ink.text,
|
||||
group.collapsed ? IconDirection::RIGHT
|
||||
: IconDirection::DOWN);
|
||||
|
||||
// 右侧:`N 标记点` + ✕(清除该信号全部固定点)。
|
||||
std::size_t markerCount = 0;
|
||||
for (const Lane::FreeMarkerItem& entry : tabLane.freeMarkerItems)
|
||||
{
|
||||
const gd::RectF& closeRect = layout.rowNameCloses[rowIndex];
|
||||
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverSignal == ref &&
|
||||
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
|
||||
UiFillRoundRect(g, closeRect, 3.0f * m_scale,
|
||||
hot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(signalColor, 0xC0));
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textOnAccent;
|
||||
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
|
||||
closeRect.X + closeRect.Width - crossPad,
|
||||
closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.4f * m_scale);
|
||||
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad,
|
||||
closeRect.Y + crossPad, closeRect.X + crossPad,
|
||||
closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.4f * m_scale);
|
||||
if (entry.ref == group.ref)
|
||||
{
|
||||
++markerCount;
|
||||
}
|
||||
}
|
||||
const std::wstring countText =
|
||||
std::to_wstring(markerCount) + L" 标记点";
|
||||
const float countWidth =
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, countText) + 2.0f * m_scale;
|
||||
const float countRight = group.close.X - 5.0f * m_scale;
|
||||
UiText(g, countText, m_fonts->monoSmall,
|
||||
gd::RectF(countRight - countWidth, group.header.Y, countWidth,
|
||||
group.header.Height),
|
||||
ink.text, TextAlign::RIGHT);
|
||||
|
||||
const float nameLeft = group.triangle.X + group.triangle.Width + 6.0f * m_scale;
|
||||
const float nameWidth =
|
||||
std::max(10.0f, countRight - countWidth - 6.0f * m_scale - nameLeft);
|
||||
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(nameLeft, group.header.Y, nameWidth, group.header.Height),
|
||||
ink.text, TextAlign::LEFT);
|
||||
|
||||
const bool closeHot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverSignal == group.ref &&
|
||||
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
|
||||
if (closeHot)
|
||||
{
|
||||
UiFillRoundRect(g, group.close, 3.0f * m_scale,
|
||||
WithAlpha(m_theme->danger, 0x77));
|
||||
}
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
|
||||
UiDrawLine(g, group.close.X + crossPad, group.close.Y + crossPad,
|
||||
group.close.X + group.close.Width - crossPad,
|
||||
group.close.Y + group.close.Height - crossPad, crossColor,
|
||||
1.5f * m_scale);
|
||||
UiDrawLine(g, group.close.X + group.close.Width - crossPad,
|
||||
group.close.Y + crossPad, group.close.X + crossPad,
|
||||
group.close.Y + group.close.Height - crossPad, crossColor,
|
||||
1.5f * m_scale);
|
||||
}
|
||||
|
||||
// 固定点行(已按信号分组 + 组内时间升序展开)。
|
||||
// ---- 固定点条目(五列,列起点由布局给定 ⇒ 纵向严格对齐)----
|
||||
for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows)
|
||||
{
|
||||
const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[markerRow.itemIndex];
|
||||
@@ -7426,48 +7770,43 @@ namespace jdi
|
||||
FreeMarkerDelta(tabLane, markerRow.itemIndex, value, hasPrev, delta,
|
||||
timeDelta);
|
||||
}
|
||||
const std::wstring* enumText =
|
||||
resolved.data->enumText.Find(enumValue);
|
||||
const std::wstring* enumText = resolved.data->enumText.Find(enumValue);
|
||||
const std::wstring valueText =
|
||||
FreeMarkerValueText(valueOk ? value : 0.0, hasPrev, delta, enumText);
|
||||
valueOk ? FormatValue(value) : std::wstring(kFreeMarkerNotAvailable);
|
||||
const std::wstring deltaText =
|
||||
(valueOk && hasPrev) ? FormatSignedValue(delta)
|
||||
: std::wstring(kFreeMarkerNotAvailable);
|
||||
const std::wstring timeText = FreeMarkerTimeText(item.time);
|
||||
const std::wstring deltaText = FreeMarkerDeltaText(hasPrev, timeDelta);
|
||||
const std::wstring timeDeltaText =
|
||||
FreeMarkerDeltaText(valueOk && hasPrev, timeDelta);
|
||||
|
||||
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)各列**左对齐在固定横坐标**上,
|
||||
// 于是同一信息在所有条目上纵向上严格对齐(用户明确要求);
|
||||
// 列宽固定,超长文本用 `UiEllipsize` 截断(布局期不做文本测量)。
|
||||
const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) {
|
||||
return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y,
|
||||
layout.markerColumns[column].width,
|
||||
markerRow.rect.Height);
|
||||
};
|
||||
UiText(g, UiEllipsize(valueText, m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_VALUE].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_VALUE),
|
||||
m_theme->text, TextAlign::LEFT);
|
||||
if (hasPrev)
|
||||
const auto paintCell = [&](EdgePanelLayout::MarkerColumnIndex column,
|
||||
const std::wstring& text, std::uint32_t color) {
|
||||
if (layout.markerColumns[column].width <= 0.0f || text.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const gd::RectF rect = columnRect(column);
|
||||
UiText(g, UiEllipsize(text, m_fonts->monoSmall, rect.Width),
|
||||
m_fonts->monoSmall, rect, color, TextAlign::LEFT);
|
||||
};
|
||||
paintCell(EdgePanelLayout::MARKER_COL_VALUE, valueText, m_theme->textStrong);
|
||||
if (layout.markerHasEnumColumn)
|
||||
{
|
||||
UiText(g,
|
||||
UiEllipsize(FormatSignedValue(delta), m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_DELTA].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DELTA),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
UiText(g,
|
||||
UiEllipsize(timeText, m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_TIME].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_TIME),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
if (!deltaText.empty())
|
||||
{
|
||||
UiText(g,
|
||||
UiEllipsize(deltaText, m_fonts->monoSmall,
|
||||
layout.markerColumns[EdgePanelLayout::MARKER_COL_DT].width),
|
||||
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DT),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_ENUM,
|
||||
(enumText != nullptr) ? *enumText : std::wstring(),
|
||||
m_theme->text);
|
||||
}
|
||||
paintCell(EdgePanelLayout::MARKER_COL_DELTA, deltaText, m_theme->textMuted);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_TIME, timeText, m_theme->textMuted);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_DT, timeDeltaText, m_theme->textMuted);
|
||||
|
||||
// 行尾 ✕:移除该固定点。
|
||||
// 行尾 ✕:移除该固定点(与之前一致)。
|
||||
const gd::RectF& closeRect = markerRow.close;
|
||||
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverItem == markerRow.itemIndex);
|
||||
@@ -7484,6 +7823,39 @@ namespace jdi
|
||||
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad,
|
||||
crossColor, 1.2f * m_scale);
|
||||
}
|
||||
|
||||
g.Restore(contentState);
|
||||
|
||||
// ---- 内容滚动条(只在内容超高时出现;按钮与滑块不参与滚动)----
|
||||
if (layout.scrollTrack.Width > 0.0f)
|
||||
{
|
||||
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
|
||||
WithAlpha(m_theme->border, 0x88));
|
||||
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
|
||||
m_theme->borderStrong);
|
||||
}
|
||||
|
||||
// ---- 「标记透明度」滑动条(仅「光标选取模式」激活时出现,固定在按钮上方)----
|
||||
if (layout.opacityTrack.Width > 0.0f)
|
||||
{
|
||||
UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall,
|
||||
gd::RectF(panel.X + pad, layout.opacityValue.Y,
|
||||
layout.opacityTrack.X - panel.X - pad, layout.opacityValue.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
const bool sliderActive =
|
||||
(m_dragMode == PlotDragMode::MARKER_OPACITY &&
|
||||
m_markerOpacityDragLane == static_cast<int>(index)) ||
|
||||
UiPointInRect(layout.opacityTrack, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
// 外观与信号样式区的「透明度」滑动条完全相同(同一 `UiDrawSlider` 口径)。
|
||||
UiDrawSlider(g, layout.opacityTrack,
|
||||
static_cast<double>(tabLane.freeMarkerOpacity), m_theme->accent,
|
||||
sliderActive, m_scale);
|
||||
const int percent =
|
||||
static_cast<int>(tabLane.freeMarkerOpacity * 100.0f + 0.5f);
|
||||
UiText(g, std::to_wstring(percent) + L"%", m_fonts->monoSmall,
|
||||
layout.opacityValue, m_theme->textStrong, TextAlign::RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
float rowY = panel.Y + titleHeight + pad;
|
||||
@@ -7520,7 +7892,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
|
||||
// 「自由标记」面板的按钮是「启用光标模式 / 关闭光标模式」的切换按钮。
|
||||
// 「自由标记」面板的按钮是「光标选取模式」的**切换类按钮**
|
||||
//(交互外观与工具栏的切换按钮一致:激活时浅强调色底 + 强调描边 + 强文本色)。
|
||||
if (hasButton || freeMarker)
|
||||
{
|
||||
const gd::RectF& buttonRect = layout.button;
|
||||
@@ -7532,16 +7905,21 @@ namespace jdi
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(inner, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
UiFillRoundRect(g, inner, 4.0f * m_scale,
|
||||
hot ? WithAlpha(m_theme->accent, 0x3A) : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, inner, 4.0f * m_scale, m_theme->accent, 1.2f);
|
||||
const std::wstring label =
|
||||
freeMarker ? (tabLane.freeMarkerCursor ? std::wstring(kFreeMarkerCursorOffLabel)
|
||||
: std::wstring(kFreeMarkerCursorOnLabel))
|
||||
: std::wstring(kBuildSignalLabel);
|
||||
UiText(g, label, m_fonts->uiSmall, inner, m_theme->accent, TextAlign::CENTER);
|
||||
const bool checked = freeMarker && tabLane.freeMarkerCursor;
|
||||
const std::uint32_t background =
|
||||
checked ? (hot ? MixColor(m_theme->accentSoft, m_theme->accent, 0.16)
|
||||
: m_theme->accentSoft)
|
||||
: (hot ? m_theme->hover : m_theme->cardBg);
|
||||
UiFillRoundRect(g, inner, 4.0f * m_scale, background);
|
||||
UiDrawRoundRect(g, inner, 4.0f * m_scale,
|
||||
checked ? m_theme->accent : m_theme->border, 1.2f);
|
||||
const std::wstring label = freeMarker ? std::wstring(kFreeMarkerCursorModeLabel)
|
||||
: std::wstring(kBuildSignalLabel);
|
||||
UiText(g, label, m_fonts->uiSmall, inner,
|
||||
checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
}
|
||||
}
|
||||
} // if (!layout.collapsed)
|
||||
|
||||
// 面板画完了:把它刚画进后备缓冲区的像素(连同缓存键)存进面板图层,
|
||||
// 下一帧只要键不变就直接贴图。矩形外的 4 逻辑像素边距用于容纳投影。
|
||||
|
||||
Reference in new issue
Block a user