修复拆分网格误继承实验性功能 / 阶跃线宽 / 面板拖拽残影 / 自由标记卡顿与交互
- 拆分网格:实验性功能只由原网格的首个信号继承(LaneCarry::owner); SplitLane 只让第一份继承;新增 PruneLaneSignalItems 按 lane.signals 过滤四类 逐信号条目(修复"新网格出现别人的固定点、面板里却查不到") - 阶跃信号:包络带横向膨胀半径 ceil -> floor(1px 线宽不再把竖直沿撑成 3px) - 浮动面板:面板位置移出 PanelGlobalKey(拖动单个面板不再重画全部面板), 新增 vacatedRects 让"上一帧被移动/缩小/关闭的面板腾出的区域"也强制重画 - 自由标记:新增 FreeMarkerContents 内容缓存(键不含视界),大量固定点时拖光标/平移不再逐帧采样; 透明度滑块与光标模式解耦(始终显示);悬停胶囊恢复不透明(瞄着被遮住的数据点时除外); 胶囊与面板五列按实测文本撑开,不再省略 - 面板交互:折起指示器与信号列表组名同款三角;GX 徽标/标题栏悬停不改光标、移除徽标气泡; 标题栏双击滚动到该网格(单击不触发,双击不再折起) - 验证:新增离屏 .preflight/fixcheck 37/37(三处 A/B 均能如实失败); 回归 round3check 31/31、round4check 34/34、round5check 5/5、editcheck 20/20、convcheck 18/18; make release/debug 零警告
This commit is contained in:
1 parent
3400ac7c29
commit
8f735d51be
8 files changed
+654
-198
No files matched your search
@@ -1836,6 +1836,46 @@ namespace jdi
|
||||
gd::RectF close;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 「自由标记」某个固定点的**内容**(取值与各种文本)。
|
||||
*
|
||||
* 这些值与**视界无关**(只取决于采样数据与信号样式),因此单独缓存:
|
||||
* 平移视界、拖动时间光标时不需要重新采样/重新格式化。
|
||||
* 面板的五列与网格内的取值胶囊共用这一份内容。
|
||||
*/
|
||||
struct FreeMarkerContentEntry
|
||||
{
|
||||
std::size_t itemIndex = 0; /**< `Lane::freeMarkerItems` 下标。 */
|
||||
bool valid = false; /**< 取值是否有效(无效时全部文本为 `N/A`)。 */
|
||||
double value = 0.0; /**< 绘制用取值(用于算胶囊/圆点的纵向位置)。 */
|
||||
std::wstring capsuleText; /**< 网格内取值胶囊的文本(`值(±增量) = 枚举`)。 */
|
||||
std::wstring valueText; /**< 面板:取值列。 */
|
||||
std::wstring enumText; /**< 面板:枚举列(可能是空串)。 */
|
||||
std::wstring deltaText; /**< 面板:增量列。 */
|
||||
std::wstring timeText; /**< 面板:时刻列。 */
|
||||
std::wstring deltaTimeText; /**< 面板:间隔(Δt)列。 */
|
||||
std::uint32_t color = 0; /**< 信号色(信号样式里的颜色,已乘不透明度)。 */
|
||||
};
|
||||
|
||||
/** 一个网格的「自由标记」内容缓存(`key` 只含与视界无关的输入)。 */
|
||||
struct FreeMarkerContentCache
|
||||
{
|
||||
std::uint64_t key = 0;
|
||||
std::vector<FreeMarkerContentEntry> entries;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 取某个网格的「自由标记」内容列表(必要时重建,**与视界无关**)。
|
||||
*
|
||||
* ⚠️ 性能硬约束:大量固定点时,逐帧重新 `SampleDisplayedSignalAt` + 拼字符串
|
||||
* 是“拖动全局光标/移动视界变卡”的主因。内容只依赖数据与样式,因此缓存起来;
|
||||
* 命中缓存时每帧只做几何与绘制。
|
||||
*/
|
||||
const std::vector<FreeMarkerContentEntry>& FreeMarkerContents(int laneIndex) const;
|
||||
|
||||
/** 按网格下标的「自由标记」内容缓存(`mutable`:只在绘制时按需重建)。 */
|
||||
mutable std::vector<FreeMarkerContentCache> m_freeMarkerContent;
|
||||
|
||||
/**
|
||||
* @brief 计算某个网格里「第 index 个固定点」的标注几何(绘制与命中共源)。
|
||||
*
|
||||
@@ -2407,15 +2447,6 @@ namespace jdi
|
||||
*/
|
||||
void PaintSignalInteraction(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 绘制浮动面板「GX」徽标的悬停提示气泡(用户 2026-10-19 要求)。
|
||||
*
|
||||
* 气泡锚定在徽标下方,内容为“G 编号”与当前可执行的操
|
||||
*(目标网格已在视界内 / 点击滚动到该网格)。
|
||||
* ⚠️ 必须在**面板像素图层之后**绘制(见 `PaintEdgeCursor` 末尾),否则会被缓存进面板位图。
|
||||
*/
|
||||
void PaintPanelBadgeTooltip(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 在绘图区底部绘制临时提示条(如“已放弃当前选区”)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
@@ -2620,8 +2651,9 @@ namespace jdi
|
||||
/**
|
||||
* @brief 上一帧鼠标是否悬停在某个浮动面板标题栏的「GX」徽标上(是则记录网格下标 / 面板种类)。
|
||||
*
|
||||
* 徽标绘制在面板**像素图层**里(缓存键只在鼠标位于面板内时才混入坐标),
|
||||
* 而提示气泡是贴图之后的叠加层(见 `PaintPanelBadgeTooltip`),因此需要单独记录状态。
|
||||
* 徽标本体绘制在面板**像素图层**里(缓存键只在鼠标位于面板内时才混入坐标),
|
||||
* 因此需要单独记录状态才能让高亮跟着鼠标进出而刷新。
|
||||
* ⚠️ 用户 2026-10-21 要求:徽标悬停**不改变鼠标形状**、也**不弹提示气泡**。
|
||||
*/
|
||||
int m_hoverPanelBadgeLane = -1;
|
||||
int m_hoverPanelBadgeKind = -1;
|
||||
|
||||
@@ -934,12 +934,13 @@ namespace jdi
|
||||
// ✕ 按钮:与面板/标签栏的其它 ✕ 一致,用手型光标作为可点击提示。
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
// 「GX」编号徽标:悬停给出提示 + 手型光标;点击把该网格滚动到视界内。
|
||||
// ⚠️ 必须排在标题栏之前(徽标位于标题栏矩形内部,否则会被判成“可拖拽”)。
|
||||
// 「GX」编号徽标:用户 2026-10-21 明确要求**悬停不改变鼠标形状**,
|
||||
// 也不弹出“是否在视界内”的浮动提示(点击行为不变:滚动到该网格)。
|
||||
// ⚠️ 必须提前 return(而不是落到下面的标题栏判定),否则会被当成“可拖拽”。
|
||||
if (layout.badge.Width > 0.0f &&
|
||||
UiPointInRect(layout.badge, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
return nullptr;
|
||||
}
|
||||
// 「展开 / 折起」三角指示器:可点击控件 ⇒ 手型光标。
|
||||
if (layout.disclosure.Width > 0.0f &&
|
||||
@@ -949,7 +950,9 @@ namespace jdi
|
||||
}
|
||||
if (UiPointInRect(layout.title, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_SIZEALL);
|
||||
// 标题栏:悬停**保持箭头**,只有按住左键拖动时(`EDGE_PANEL_MOVE`,
|
||||
// 见本函数开头的拖拽分支)才变成十字箭头(用户 2026-10-21 要求)。
|
||||
return nullptr;
|
||||
}
|
||||
if ((static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MEAN ||
|
||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK) &&
|
||||
@@ -2720,8 +2723,9 @@ namespace jdi
|
||||
int panelKind = 0;
|
||||
if (TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane, panelKind))
|
||||
{
|
||||
// 标题栏双击 = 展开 / 折起该面板(折起的面板只显示标题栏)。
|
||||
// ⚠️ 折起状态下标题栏是唯一可见区域,因此这里也是“双击重新展开”的入口。
|
||||
// 标题栏双击 = 把该面板所属网格**滚动到视界内**(与单击 GX 徽标完全同效)。
|
||||
// 用户 2026-10-21 明确要求:**只有双击标题栏**才触发这个联动,单击标题栏不触发
|
||||
//(单击标题栏只是准备拖拽面板);面板的折起/展开只由左侧三角指示器负责。
|
||||
{
|
||||
const EdgePanelLayout titleLayout =
|
||||
EdgePanelLayoutFor(panelLane, static_cast<EdgePanelKind>(panelKind));
|
||||
@@ -2737,7 +2741,11 @@ namespace jdi
|
||||
UiPointInRect(titleLayout.disclosure, static_cast<float>(x),
|
||||
static_cast<float>(y))))
|
||||
{
|
||||
ToggleLanePanelCollapsed(panelLane, static_cast<EdgePanelKind>(panelKind));
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
if (ScrollLaneIntoView(panelLane))
|
||||
{
|
||||
SetHint(L"正在滚动到 G" + std::to_wstring(panelLane + 1) + L" 网格…");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2884,7 +2892,18 @@ namespace jdi
|
||||
int panelKind = 0;
|
||||
if (TopPanelAtPoint(fx, fy, panelLane, panelKind))
|
||||
{
|
||||
return true;
|
||||
// ⚠️ 例外:**标题栏**(排除徽标 / ✕ / 折起三角)双击有明确语义 ——
|
||||
// 把该面板所属网格滚动到视界内(用户 2026-10-21 要求,与单击 GX 徽标一致)。
|
||||
// 其余落点仍然只有“按下”语义。
|
||||
const EdgePanelLayout layout =
|
||||
EdgePanelLayoutFor(panelLane, static_cast<EdgePanelKind>(panelKind));
|
||||
const bool onTitle = layout.title.Width > 0.0f &&
|
||||
UiPointInRect(layout.title, fx, fy) &&
|
||||
!(layout.badge.Width > 0.0f && UiPointInRect(layout.badge, fx, fy)) &&
|
||||
!(layout.close.Width > 0.0f && UiPointInRect(layout.close, fx, fy)) &&
|
||||
!(layout.disclosure.Width > 0.0f &&
|
||||
UiPointInRect(layout.disclosure, fx, fy));
|
||||
return !onTitle;
|
||||
}
|
||||
}
|
||||
// 右下角「回到最上方」浮动按钮:按下只登记、抬起才生效 ⇒ 只有单击语义。
|
||||
|
||||
@@ -1074,7 +1074,13 @@ namespace jdi
|
||||
// 列间距由前两列推断(同一段内等宽等距);退化时按 1px 处理。
|
||||
const float spacing = columns[1].x - columns[0].x;
|
||||
const float step = (spacing > 0.01f) ? spacing : 1.0f;
|
||||
std::size_t radius = static_cast<std::size_t>(std::ceil(halfWidth / step));
|
||||
// ⚠️ **必须向下取整**(用户 2026-10-21 报告的阶跃信号线宽不一致):
|
||||
// `ceil` 会让 1px 线宽(`halfWidth` ≈ 0.5–0.7 设备像素)也膨胀 1 列,
|
||||
// 阶跃的竖直沿因此从 1 列变成 3 列(≈3px),而水平段只有 ~1.25px ⇒
|
||||
// 视觉上“竖直跳变的线宽明显大于水平持续段”(放大到走逐点描边路径后又正常了)。
|
||||
// `floor` 下 1px 线宽不膨胀(竖直沿 = 1 列),粗线(≥ 2 列宽)仍按整数列膨胀。
|
||||
const std::size_t radius =
|
||||
static_cast<std::size_t>(std::floor(halfWidth / step + 1e-4f));
|
||||
if (radius > 0 && radius * 2 + 1 < count)
|
||||
{
|
||||
widened.resize(count);
|
||||
|
||||
@@ -144,12 +144,42 @@ namespace jdi
|
||||
|
||||
if (kind == EdgePanelKind::MARK)
|
||||
{
|
||||
// 「自由标记」面板:**宽度用固定值**,不做逐行文本测量。
|
||||
// ⚠️ `EdgePanelWidth` 被 `EdgePanelLayoutFor` 调用,而后者又在 `TopPanelAtPoint` /
|
||||
// `SyncPanelOrder` 里被高频调用(每次鼠标移动数次)——若在这里逐行取采样值算文本宽度,
|
||||
// 就变成"鼠标一动就做几十次采样查询",会明显拖慢交互。
|
||||
// 取值的文本超长时由绘制侧的 `UiEllipsize` 截断即可。
|
||||
width = std::max(width, kFreeMarkerPanelWidth * m_scale);
|
||||
// 「自由标记」面板:宽度 = 五列(取「缺省列宽」与**实测内容宽度**的较大者)+ 行 ✕
|
||||
// + 左右内边距 + **滚动条预留位**(内容超高时右侧要放滚动条)。
|
||||
// ⚠️ 用户 2026-10-21 要求面板里的数据列**不得出现省略号**,因此不能再按固定列宽。
|
||||
// 逐行取采样值算宽度会拖慢交互(本函数被 `TopPanelAtPoint` / `SyncPanelOrder`
|
||||
// 高频调用),所以走 `Lane::freeMarkerColumnWidths`:绘制该面板时顺便量好真实
|
||||
// 文本宽度存起来,这里只读。还没量过(全 0)时退回缺省列宽。
|
||||
const bool hasEnum = [this, &lane]() {
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data != nullptr && resolved.visible &&
|
||||
!resolved.data->enumText.Empty() && lane.HasFreeMarkers(ref))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
float columnsWidth = 0.0f;
|
||||
for (int column = 0; column < 5; ++column)
|
||||
{
|
||||
const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum;
|
||||
if (omitted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
columnsWidth += std::max(kFreeMarkerColumnWidths[column] * m_scale,
|
||||
lane.freeMarkerColumnWidths[static_cast<std::size_t>(column)]);
|
||||
columnsWidth += kFreeMarkerColumnGap * m_scale;
|
||||
}
|
||||
// 行尾 ✕(按胶囊同高的正方形取宽)+ 滚动条预留 + 少量余量。
|
||||
const float rowCloseWidth = kFreeMarkerItemRowHeight * m_scale + 2.0f * m_scale;
|
||||
const float markerWidth = kEdgePanelPad * 2.0f * m_scale +
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale + columnsWidth +
|
||||
rowCloseWidth;
|
||||
width = std::max(width, std::max(kFreeMarkerPanelWidth * m_scale, markerWidth));
|
||||
}
|
||||
|
||||
// 夹在绘图区宽度之内(过窄的网格也不至于把面板挤到外面)。
|
||||
@@ -5441,8 +5471,8 @@ namespace jdi
|
||||
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;
|
||||
// 「标记透明度」滑块行**始终显示**(用户 2026-10-21 要求:不再依附于光标模式按钮)。
|
||||
const float sliderRow = kFreeMarkerSliderRowHeight * m_scale;
|
||||
float content = 0.0f;
|
||||
std::vector<std::size_t> order;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
@@ -5735,16 +5765,17 @@ namespace jdi
|
||||
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
|
||||
const float sliderRow = lane.freeMarkerCursor ? (kFreeMarkerSliderRowHeight * m_scale) : 0.0f;
|
||||
// 「标记透明度」独立于光标模式按钮:**总是**显示在按钮上方。
|
||||
const float sliderRow = kFreeMarkerSliderRowHeight * m_scale;
|
||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||
|
||||
// 底部固定区:切换按钮(整行宽度、与面板下边界留一个 pad)。
|
||||
layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
|
||||
std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
|
||||
if (sliderRow > 0.0f)
|
||||
{
|
||||
// 「标记透明度」行:与信号样式区的「透明度」行同一套排版
|
||||
//(左侧标签 + 中间轨道 + 右侧百分比),只是位置固定在按钮上方。
|
||||
//(左侧标签 + 中间轨道 + 右侧百分比),位置固定在按钮上方。
|
||||
// ⚠️ 无论是否启用「光标选取模式」都显示(用户 2026-10-21 要求)。
|
||||
const float rowTop = layout.button.Y - sliderRow;
|
||||
const float valueWidth =
|
||||
std::max(36.0f * m_scale,
|
||||
@@ -5829,8 +5860,11 @@ namespace jdi
|
||||
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);
|
||||
omitted ? 0.0f
|
||||
: std::max(kFreeMarkerColumnWidths[column] * m_scale,
|
||||
lane.freeMarkerColumnWidths[static_cast<std::size_t>(column)]);
|
||||
layout.markerColumns[column].left = columnX;
|
||||
layout.markerColumns[column].width = columnWidth;
|
||||
columnX += columnWidth + (omitted ? 0.0f : kFreeMarkerColumnGap * m_scale);
|
||||
@@ -6303,18 +6337,23 @@ namespace jdi
|
||||
const std::vector<Lane>& lanes = m_tab->Lanes();
|
||||
for (const Lane& lane : lanes)
|
||||
{
|
||||
mixFloat(static_cast<float>(lane.edgePanel.x));
|
||||
mixFloat(static_cast<float>(lane.edgePanel.y));
|
||||
mixFloat(static_cast<float>(lane.edgeCenterPanel.x));
|
||||
mixFloat(static_cast<float>(lane.edgeCenterPanel.y));
|
||||
mixFloat(static_cast<float>(lane.meanFilterPanel.x));
|
||||
mixFloat(static_cast<float>(lane.meanFilterPanel.y));
|
||||
mixFloat(static_cast<float>(lane.weakPeakPanel.x));
|
||||
mixFloat(static_cast<float>(lane.weakPeakPanel.y));
|
||||
// ⚠️⚠️ **面板的位置 / 尺寸绝不能进全局键**(用户 2026-10-21 报告“拖动面板时
|
||||
// 下方另一个面板出现拖拽残影”):位置只影响该面板自己,已在 `PanelSlotKey`
|
||||
// 里(它混合 `layout.panel/title/...`)。以前把 4 类面板的 x/y 混进全局键 ⇒
|
||||
// 拖动任意面板都会让**所有**面板重画(15 个 ≈33ms/帧),而「自由标记」面板的
|
||||
// 位置偏偏漏了没混 ⇒ 拖动它时其它面板照旧贴旧像素 → 正好在下方留残影。
|
||||
// 现在:拖动面板只重画它自己;它**腾出的区域**由 `PaintEdgeCursor` 的
|
||||
// `vacatedRects` 机制兜底(覆盖到该区域的面板必须重画)。
|
||||
mix(lane.edgePanel.open ? 1ull : 0ull);
|
||||
mix(lane.edgeCenterPanel.open ? 1ull : 0ull);
|
||||
mix(lane.meanFilterPanel.open ? 1ull : 0ull);
|
||||
mix(lane.weakPeakPanel.open ? 1ull : 0ull);
|
||||
mix(lane.freeMarkerPanel.open ? 1ull : 0ull);
|
||||
mix(lane.edgePanel.collapsed ? 1ull : 0ull);
|
||||
mix(lane.edgeCenterPanel.collapsed ? 1ull : 0ull);
|
||||
mix(lane.meanFilterPanel.collapsed ? 1ull : 0ull);
|
||||
mix(lane.weakPeakPanel.collapsed ? 1ull : 0ull);
|
||||
mix(lane.freeMarkerPanel.collapsed ? 1ull : 0ull);
|
||||
mix(lane.edgeValueValid ? 1ull : 0ull);
|
||||
mixDouble(lane.edgeValue);
|
||||
}
|
||||
@@ -6614,6 +6653,103 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<PlotView::FreeMarkerContentEntry>& PlotView::FreeMarkerContents(int laneIndex) const
|
||||
{
|
||||
static const std::vector<FreeMarkerContentEntry> kNoEntries;
|
||||
if (m_tab == nullptr || laneIndex < 0 ||
|
||||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return kNoEntries;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (static_cast<std::size_t>(laneIndex) >= m_freeMarkerContent.size())
|
||||
{
|
||||
m_freeMarkerContent.resize(static_cast<std::size_t>(laneIndex) + 1);
|
||||
}
|
||||
FreeMarkerContentCache& cache = m_freeMarkerContent[static_cast<std::size_t>(laneIndex)];
|
||||
|
||||
// 缓存键:只混入**与视界无关**的输入(条目、信号样式、数据地址与长度)。
|
||||
// ⚠️ 绝不能混入 `ViewMin/ViewMax`、鼠标位置或网格几何 —— 否则平移视界、
|
||||
// 拖动时间光标时缓存永远命中不了,又回到"每帧对每个固定点采样一次"。
|
||||
std::uint64_t key = 1469598103934665603ull;
|
||||
const auto mix = [&key](std::uint64_t value) {
|
||||
key ^= value;
|
||||
key *= 1099511628211ull;
|
||||
};
|
||||
const auto mixFloat = [&mix](float value) {
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mix(static_cast<std::uint64_t>(bits));
|
||||
};
|
||||
const auto mixDouble = [&mix](double value) {
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mix(bits);
|
||||
};
|
||||
mix(static_cast<std::uint64_t>(lane.freeMarkerItems.size()));
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(static_cast<std::uint32_t>(item.ref.groupIndex)));
|
||||
mix(static_cast<std::uint64_t>(static_cast<std::uint32_t>(item.ref.signalIndex)));
|
||||
mixDouble(item.time);
|
||||
const ResolvedSignal resolved = ResolveSignal(item.ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
mix(0ull);
|
||||
continue;
|
||||
}
|
||||
mix(reinterpret_cast<std::uint64_t>(resolved.data));
|
||||
mix(static_cast<std::uint64_t>(resolved.data->values.size()));
|
||||
mix(resolved.visible ? 1ull : 2ull);
|
||||
mix(static_cast<std::uint64_t>(resolved.style->color));
|
||||
mixFloat(resolved.style->opacity);
|
||||
mixFloat(resolved.style->gain);
|
||||
mixFloat(resolved.style->offsetY);
|
||||
mixDouble(resolved.style->offsetTime);
|
||||
mix(static_cast<std::uint64_t>(static_cast<int>(resolved.style->mode)));
|
||||
mix((resolved.style->absolute ? 1ull : 0ull) | (resolved.style->mirrorVertical ? 2ull : 0ull) |
|
||||
(resolved.style->mirrorHorizontal ? 4ull : 0ull));
|
||||
}
|
||||
|
||||
if (cache.key != key)
|
||||
{
|
||||
cache.key = key;
|
||||
cache.entries.clear();
|
||||
cache.entries.reserve(lane.freeMarkerItems.size());
|
||||
for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex)
|
||||
{
|
||||
const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex];
|
||||
FreeMarkerContentEntry entry;
|
||||
entry.itemIndex = itemIndex;
|
||||
double value = 0.0;
|
||||
double enumValue = 0.0;
|
||||
const bool valueOk = FreeMarkerValue(lane, item, value, enumValue);
|
||||
bool hasPrev = false;
|
||||
double delta = 0.0;
|
||||
double timeDelta = 0.0;
|
||||
if (valueOk)
|
||||
{
|
||||
FreeMarkerDelta(lane, itemIndex, value, hasPrev, delta, timeDelta);
|
||||
}
|
||||
const ResolvedSignal resolved = ResolveSignal(item.ref);
|
||||
const std::wstring* enumPtr =
|
||||
(resolved.data != nullptr) ? resolved.data->enumText.Find(enumValue) : nullptr;
|
||||
entry.valid = valueOk;
|
||||
entry.value = value;
|
||||
entry.color = (resolved.style != nullptr) ? StyleColor(*resolved.style) : 0u;
|
||||
entry.capsuleText = FreeMarkerValueText(value, hasPrev, delta, enumPtr);
|
||||
entry.valueText = valueOk ? FormatValue(value) : std::wstring(kFreeMarkerNotAvailable);
|
||||
entry.enumText = (enumPtr != nullptr) ? *enumPtr : std::wstring();
|
||||
entry.deltaText = (valueOk && hasPrev) ? FormatSignedValue(delta)
|
||||
: std::wstring(kFreeMarkerNotAvailable);
|
||||
entry.timeText = FreeMarkerTimeText(item.time);
|
||||
entry.deltaTimeText = FreeMarkerDeltaText(valueOk && hasPrev, timeDelta);
|
||||
cache.entries.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
return cache.entries;
|
||||
}
|
||||
|
||||
bool PlotView::FreeMarkerLabelRects(int laneIndex, std::size_t itemIndex, gd::RectF& label,
|
||||
gd::RectF& close, std::wstring& text) const
|
||||
{
|
||||
@@ -6632,16 +6768,14 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex];
|
||||
double value = 0.0;
|
||||
double enumValue = 0.0;
|
||||
if (!FreeMarkerValue(lane, item, value, enumValue))
|
||||
// 取值与文本来自**与视界无关**的内容缓存(大量固定点 + 拖光标/平移时不重复采样)。
|
||||
const std::vector<FreeMarkerContentEntry>& contents = FreeMarkerContents(laneIndex);
|
||||
if (itemIndex >= contents.size() || !contents[itemIndex].valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool hasPrev = false;
|
||||
double delta = 0.0;
|
||||
double timeDelta = 0.0;
|
||||
FreeMarkerDelta(lane, itemIndex, value, hasPrev, delta, timeDelta);
|
||||
const FreeMarkerContentEntry& entry = contents[itemIndex];
|
||||
const double value = entry.value;
|
||||
|
||||
// 超出视界**直接不显示**(用户明确要求:不按常规数据点那样贴边定格 + 三角指示)。
|
||||
const float rawX = TimeToX(item.time);
|
||||
@@ -6660,14 +6794,12 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 枚举文本按**采样原值**查找(与光标读数完全一致)。
|
||||
const ResolvedSignal labelResolved = ResolveSignal(item.ref);
|
||||
const std::wstring* enumText = (labelResolved.data != nullptr)
|
||||
? labelResolved.data->enumText.Find(enumValue)
|
||||
: nullptr;
|
||||
text = FreeMarkerValueText(value, hasPrev, delta, enumText);
|
||||
text = entry.capsuleText;
|
||||
|
||||
// ✕ 按钮 = **与取值胶囊同高的正方形**(用户明确要求:长宽都等于胶囊的上下高度)。
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale;
|
||||
// ⚠️ 宽度多加 2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 有时会因为亚像素/字形
|
||||
// 伸部而把“刚好装满”的文本截成省略号(用户报告枚举文本被 … 缺省)。
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 12.0f * m_scale;
|
||||
const float textHeight = 17.0f * m_scale;
|
||||
const float closeSize = textHeight;
|
||||
const float gap = kFreeMarkerCloseGap * m_scale;
|
||||
@@ -6803,6 +6935,8 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
const float markerAlpha = std::max(0.0f, std::min(1.0f, lane.freeMarkerOpacity));
|
||||
// 内容(取值 / 文本 / 信号色)来自与视界无关的缓存,不逐帧重新采样。
|
||||
const std::vector<FreeMarkerContentEntry>& contents = FreeMarkerContents(static_cast<int>(laneIndex));
|
||||
std::vector<gd::RectF> placed;
|
||||
for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex)
|
||||
{
|
||||
@@ -6814,20 +6948,18 @@ namespace jdi
|
||||
continue; // 超出视界直接不显示(不贴边、不画三角)
|
||||
}
|
||||
const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(item.ref);
|
||||
if (resolved.style == nullptr)
|
||||
if (itemIndex >= contents.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const FreeMarkerContentEntry& entry = contents[itemIndex];
|
||||
const std::uint32_t signalColor = entry.color;
|
||||
const LaneVisual& visual = lanes[laneIndex];
|
||||
const float clipTop = std::max(visual.rect.Y, m_plotRect.Y);
|
||||
const float clipBottom =
|
||||
std::min(visual.rect.Y + visual.rect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
|
||||
double value = 0.0;
|
||||
double enumValue = 0.0;
|
||||
const bool valueOk = FreeMarkerValue(lane, item, value, enumValue);
|
||||
const bool valueOk = entry.valid;
|
||||
|
||||
// ---- 纵向避让:与本次已放置的胶囊重叠时,向下 / 向上交替错开 ----
|
||||
const auto occupied = [&placed](const gd::RectF& rect) {
|
||||
@@ -6877,13 +7009,32 @@ namespace jdi
|
||||
const float radius = 3.4f * m_scale;
|
||||
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;
|
||||
const float dotY =
|
||||
valueOk ? ValueToY(entry.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, markerAlpha);
|
||||
// 「标记透明度」:鼠标停在某个取值胶囊上时把**这一个**胶囊恢复为全不透明
|
||||
//(用户 2026-10-21 要求:既能看淡全局标记,又能读清当前关注的数值)。
|
||||
// ⚠️ 例外:如果此刻跟随探针正瞄着一个**被这个胶囊遮住的数据点**
|
||||
//(用户意图标记被遮住的那条曲线),则不恢复 —— 否则胶囊会重新遮住目标点、
|
||||
// 无法看出自己到底瞄到了哪个采样点。
|
||||
float capsuleAlpha = markerAlpha;
|
||||
const float fx = static_cast<float>(m_mouseX);
|
||||
const float fy = static_cast<float>(m_mouseY);
|
||||
if (markerAlpha < 0.999f && UiPointInRect(label, fx, fy))
|
||||
{
|
||||
const bool probeHiddenByLabel =
|
||||
m_freeMarker.valid && m_freeMarker.laneIndex == static_cast<int>(laneIndex) &&
|
||||
UiPointInRect(label, m_freeMarker.x, m_freeMarker.y);
|
||||
if (!probeHiddenByLabel)
|
||||
{
|
||||
capsuleAlpha = 1.0f;
|
||||
}
|
||||
}
|
||||
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot, capsuleAlpha);
|
||||
|
||||
FreeMarkerHit hit;
|
||||
hit.laneIndex = static_cast<int>(laneIndex);
|
||||
@@ -7261,8 +7412,62 @@ namespace jdi
|
||||
// ⚠️ **按层序从下往上绘制**(与命中测试、像素缓存的层叠顺序完全一致):
|
||||
// `m_panelOrder` 队尾 = 最上层 = 最后绘制。
|
||||
SyncPanelOrder();
|
||||
for (const PanelRef& panelRef : m_panelOrder)
|
||||
|
||||
// 预扫描一:本帧各面板的布局(循环里直接复用,避免每帧重复计算)。
|
||||
std::vector<EdgePanelLayout> panelLayouts(m_panelOrder.size());
|
||||
for (std::size_t ordinal = 0; ordinal < m_panelOrder.size(); ++ordinal)
|
||||
{
|
||||
panelLayouts[ordinal] = EdgePanelLayoutFor(
|
||||
m_panelOrder[ordinal].laneIndex, static_cast<EdgePanelKind>(m_panelOrder[ordinal].kind));
|
||||
}
|
||||
|
||||
// 预扫描二:上一帧已缓存、但本帧**位置/尺寸变了(或被移除)**的面板——它们腾出的
|
||||
// 区域可能仍然留在**其它面板**的缓存像素里(那块像素是上一帧连它一起拷下来的),
|
||||
// 于是鼠标拖动面板时,下方面板上会出现拖拽残影(用户 2026-10-21 报告)。
|
||||
// 把腾空区域也当“脏区”,强制覆盖到它的面板重画,就能彻底消除残影。
|
||||
std::vector<gd::RectF> vacatedRects;
|
||||
if (panelCacheReady && !m_panelSlots.empty())
|
||||
{
|
||||
const float margin = 4.0f * m_scale;
|
||||
for (const PanelSlot& slot : m_panelSlots)
|
||||
{
|
||||
int cx0 = slot.x0;
|
||||
int cy0 = slot.y0;
|
||||
int cx1 = slot.x1;
|
||||
int cy1 = slot.y1;
|
||||
bool alive = false;
|
||||
for (std::size_t ordinal = 0; ordinal < m_panelOrder.size(); ++ordinal)
|
||||
{
|
||||
if (m_panelOrder[ordinal].laneIndex != slot.laneIndex ||
|
||||
m_panelOrder[ordinal].kind != slot.kind)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const gd::RectF& panel = panelLayouts[ordinal].panel;
|
||||
if (panel.Width <= 0.0f || panel.Height <= 0.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
cx0 = std::max(0, static_cast<int>(std::floor(panel.X - margin)));
|
||||
cy0 = std::max(0, static_cast<int>(std::floor(panel.Y - margin)));
|
||||
cx1 = std::max(cx0, static_cast<int>(std::ceil(panel.X + panel.Width + margin)));
|
||||
cy1 = std::max(cy0, static_cast<int>(std::ceil(panel.Y + panel.Height + margin)));
|
||||
alive = true;
|
||||
break;
|
||||
}
|
||||
if (!alive || cx0 != slot.x0 || cy0 != slot.y0 || cx1 != slot.x1 || cy1 != slot.y1)
|
||||
{
|
||||
vacatedRects.push_back(gd::RectF(static_cast<float>(slot.x0),
|
||||
static_cast<float>(slot.y0),
|
||||
static_cast<float>(slot.x1 - slot.x0),
|
||||
static_cast<float>(slot.y1 - slot.y0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t panelOrdinal = 0; panelOrdinal < m_panelOrder.size(); ++panelOrdinal)
|
||||
{
|
||||
const PanelRef& panelRef = m_panelOrder[panelOrdinal];
|
||||
const EdgePanelKind kind = static_cast<EdgePanelKind>(panelRef.kind);
|
||||
const std::size_t index = static_cast<std::size_t>(panelRef.laneIndex);
|
||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||
@@ -7278,7 +7483,8 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
const Lane& tabLane = tabLanes[index];
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(static_cast<int>(index), kind);
|
||||
// 布局已在预扫描里算好(同一帧内复用,避免重复计算)。
|
||||
const EdgePanelLayout& layout = panelLayouts[panelOrdinal];
|
||||
const gd::RectF& panel = layout.panel;
|
||||
if (panel.Width <= 0.0f || panel.Height <= 0.0f)
|
||||
{
|
||||
@@ -7302,6 +7508,19 @@ namespace jdi
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!forcedDirty)
|
||||
{
|
||||
// 上一帧有面板在这里腾出了位置(移动 / 缩小 / 关闭)⇒ 本面板的缓存像素可能
|
||||
// 仍然包含那块旧内容,必须重画(否则拖动上面那个面板时这里会留残影)。
|
||||
for (const gd::RectF& vacated : vacatedRects)
|
||||
{
|
||||
if (vacated.IntersectsWith(cachedRect))
|
||||
{
|
||||
forcedDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t slotKey = 0;
|
||||
const std::size_t slotIndex = nextSlots.size();
|
||||
@@ -7360,8 +7579,9 @@ namespace jdi
|
||||
UiFillRoundRect(g, gd::RectF(panel.X, panel.Y, panel.Width, titleHeight), 5.0f * m_scale,
|
||||
WithAlpha(m_theme->accent, 0x2A));
|
||||
|
||||
// 标题栏左侧的「展开 / 折起」指示器:三角(朝下 = 已展开、朝右 = 已折起)
|
||||
// + 三角后方的一条短横线(经典展开/折起标识),位于 GX 徽标左边。
|
||||
// 标题栏左侧的「展开 / 折起」指示器:**与信号列表组名最左边的折叠三角完全同款**
|
||||
//(同一尺寸 12 逻辑像素、同一个 `UiDrawTriangleIcon`、同样只在悬停时变强调色、
|
||||
// 不再带三角后方那条横线 —— 用户 2026-10-21 要求“统一使用相同标记”)。
|
||||
// 单击它切换折起状态;折起后面板只剩标题栏,这里是唯一的“重新展开”按钮。
|
||||
{
|
||||
const gd::RectF& disclosureRect = layout.disclosure;
|
||||
@@ -7373,17 +7593,14 @@ namespace jdi
|
||||
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 centerX = disclosureRect.X + disclosureRect.Width * 0.5f;
|
||||
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),
|
||||
gd::RectF(centerX - 6.0f * m_scale, centerY - 6.0f * m_scale,
|
||||
12.0f * m_scale, 12.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7751,60 +7968,79 @@ namespace jdi
|
||||
}
|
||||
|
||||
// ---- 固定点条目(五列,列起点由布局给定 ⇒ 纵向严格对齐)----
|
||||
// ⚠️ 取值与各列文本来自**与视界无关的内容缓存**(`FreeMarkerContents`):
|
||||
// 大量固定点时逐帧重新采样会让"拖光标 / 平移视界"明显变卡。
|
||||
const std::vector<FreeMarkerContentEntry>& contents =
|
||||
FreeMarkerContents(static_cast<int>(index));
|
||||
// 五列的**实测宽度**:含滚出视界的条目,保证列宽稳定、文本不被省略。
|
||||
// 存回 `Lane` 供下一步的布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)使用。
|
||||
{
|
||||
float widest[5] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
|
||||
const auto measure = [this, &widest](std::size_t column, const std::wstring& text) {
|
||||
if (text.empty() || column >= 5)
|
||||
{
|
||||
return;
|
||||
}
|
||||
widest[column] = std::max(widest[column],
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, text));
|
||||
};
|
||||
for (const FreeMarkerContentEntry& entry : contents)
|
||||
{
|
||||
measure(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText);
|
||||
measure(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText);
|
||||
measure(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText);
|
||||
measure(EdgePanelLayout::MARKER_COL_TIME, entry.timeText);
|
||||
measure(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText);
|
||||
}
|
||||
std::array<float, 5>& stored =
|
||||
tabLanes[index].freeMarkerColumnWidths;
|
||||
for (std::size_t column = 0; column < 5; ++column)
|
||||
{
|
||||
// +2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 对"刚好装满"的文本
|
||||
// 有时仍会截成省略号(亚像素/字形伸部),留一点余量可彻底避免。
|
||||
const float want =
|
||||
(widest[column] > 0.0f) ? (widest[column] + 2.0f * m_scale) : 0.0f;
|
||||
if (std::fabs(stored[column] - want) > 0.5f)
|
||||
{
|
||||
stored[column] = want;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows)
|
||||
{
|
||||
const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[markerRow.itemIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(item.ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
if (markerRow.itemIndex >= contents.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double value = 0.0;
|
||||
double enumValue = 0.0;
|
||||
const bool valueOk = FreeMarkerValue(tabLane, item, value, enumValue);
|
||||
bool hasPrev = false;
|
||||
double delta = 0.0;
|
||||
double timeDelta = 0.0;
|
||||
if (valueOk)
|
||||
{
|
||||
FreeMarkerDelta(tabLane, markerRow.itemIndex, value, hasPrev, delta,
|
||||
timeDelta);
|
||||
}
|
||||
const std::wstring* enumText = resolved.data->enumText.Find(enumValue);
|
||||
const std::wstring valueText =
|
||||
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 timeDeltaText =
|
||||
FreeMarkerDeltaText(valueOk && hasPrev, timeDelta);
|
||||
const FreeMarkerContentEntry& entry = contents[markerRow.itemIndex];
|
||||
|
||||
const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) {
|
||||
return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y,
|
||||
layout.markerColumns[column].width,
|
||||
markerRow.rect.Height);
|
||||
};
|
||||
// ⚠️ 面板里的数据列**不得省略**(用户 2026-10-21 明确要求):
|
||||
// 列宽已按实测文本宽度撑开,因此这里直接整段输出,不再 `UiEllipsize`。
|
||||
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);
|
||||
UiText(g, text, m_fonts->monoSmall, columnRect(column), color,
|
||||
TextAlign::LEFT);
|
||||
};
|
||||
paintCell(EdgePanelLayout::MARKER_COL_VALUE, valueText, m_theme->textStrong);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText,
|
||||
m_theme->textStrong);
|
||||
if (layout.markerHasEnumColumn)
|
||||
{
|
||||
paintCell(EdgePanelLayout::MARKER_COL_ENUM,
|
||||
(enumText != nullptr) ? *enumText : std::wstring(),
|
||||
m_theme->text);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_ENUM, entry.enumText, 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);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_DELTA, entry.deltaText,
|
||||
m_theme->textMuted);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_TIME, entry.timeText, m_theme->textMuted);
|
||||
paintCell(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText,
|
||||
m_theme->textMuted);
|
||||
|
||||
// 行尾 ✕:移除该固定点(与之前一致)。
|
||||
const gd::RectF& closeRect = markerRow.close;
|
||||
@@ -7960,58 +8196,6 @@ namespace jdi
|
||||
m_panelSlots = std::move(nextSlots);
|
||||
m_panelGlobalKey = panelGlobalKey;
|
||||
m_panelValid = panelCacheReady;
|
||||
|
||||
// 「GX」徽标的悬停提示气泡:必须在**全部面板像素都存进图层之后**绘制,
|
||||
// 否则气泡会被烘进面板缓存(下一帧即使鼠标移开也会重现)。
|
||||
PaintPanelBadgeTooltip(g);
|
||||
}
|
||||
|
||||
void PlotView::PaintPanelBadgeTooltip(gd::Graphics& g)
|
||||
{
|
||||
if (m_hoverPanelBadgeLane < 0 || m_fonts == nullptr || m_fonts->uiSmall == nullptr ||
|
||||
m_tab == nullptr || m_hoverPanelBadgeKind < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const EdgePanelKind kind = static_cast<EdgePanelKind>(m_hoverPanelBadgeKind);
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(m_hoverPanelBadgeLane, kind);
|
||||
if (layout.badge.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 目标网格是否已完整可见:决定提示文案(不可见时提示“点击会滚动过去”)。
|
||||
bool laneVisible = false;
|
||||
const std::size_t laneIndex = static_cast<std::size_t>(m_hoverPanelBadgeLane);
|
||||
if (laneIndex < m_lanes.size())
|
||||
{
|
||||
const gd::RectF& rect = m_lanes[laneIndex].rect;
|
||||
laneVisible = (rect.Y >= m_plotRect.Y - 0.5f) &&
|
||||
(rect.Y + rect.Height <= m_plotRect.Y + m_plotRect.Height + 0.5f);
|
||||
}
|
||||
const std::wstring label = L"G" + std::to_wstring(m_hoverPanelBadgeLane + 1);
|
||||
const std::wstring text =
|
||||
label + (laneVisible ? L" · 已在视界内" : L" · 点击滚动到该网格");
|
||||
|
||||
const float padX = 8.0f * m_scale;
|
||||
const float height = 22.0f * m_scale;
|
||||
const float width = std::min(UiMeasureTextWidth(m_fonts->uiSmall, text) + padX * 2.0f,
|
||||
std::max(10.0f, m_plotRect.Width - 4.0f * m_scale));
|
||||
float left = layout.badge.X - 2.0f * m_scale;
|
||||
left = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale));
|
||||
// 默认贴在徽标下方;下方放不下就翻到上方(与弹出菜单的翻转规则一致)。
|
||||
float top = layout.badge.Y + layout.badge.Height + 4.0f * m_scale;
|
||||
if (top + height > m_plotRect.Y + m_plotRect.Height - 2.0f * m_scale)
|
||||
{
|
||||
top = layout.badge.Y - height - 4.0f * m_scale;
|
||||
}
|
||||
top = std::max(m_plotRect.Y + 2.0f * m_scale, top);
|
||||
|
||||
const gd::RectF bubble(left, top, width, height);
|
||||
UiFillRoundRect(g, bubble, 4.0f * m_scale, m_theme->readoutBg);
|
||||
UiDrawRoundRect(g, bubble, 4.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||
UiText(g, text, m_fonts->uiSmall, bubble, m_theme->textStrong, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
void PlotView::PaintEditField(gd::Graphics& g)
|
||||
|
||||
Reference in new issue
Block a user