新标签页分组默认折起 + GX 徽标悬停不改光标 + 标题栏 RAM 改私有工作集 + 边沿计数改为预算推进

一、新建标签页的信号分组默认折起
- Application::NewTab(bool collapseGroups):启动时的首个标签页传 false(加载文件后可直接勾选
  信号),用户新建的标签页(Ctrl+T / 标签栏 ⊕ / 标签栏右键)传 true。
- TabConfig::SetGroupsCollapsedByDefault(bool) + SetAllCollapsed(bool)(新增);
  SyncToDataSet 里 m_collapsed.resize(fileCount, m_groupsCollapsedByDefault)(原来固定 false)——
  只影响之后新出现的分组,已存在的分组保持用户当前选择;ToggleCollapsed 补建状态时同样用它。

二、网格 GX 徽标:悬停不改变鼠标形状
- PlotView::QueryCursor 里 GX 徽标分支由 IDC_HAND 改为 return nullptr(= 保持箭头),
  仍然保持"提前 return"的位置以免被光标线/图例判定抢走优先级;
  只有拖拽中(LANE_REORDER)才是 IDC_SIZENS。

三、标题栏 RAM 口径:总工作集 → 私有工作集(任务管理器"内存"列)
- 症状:加载大文件再关闭分组后,标题栏仍显示几十/上百 MB,任务管理器只有几 MB。
- 根因:原实现取 PROCESS_MEMORY_COUNTERS::WorkingSetSize(总工作集),它把映射进来的 DLL /
  共享页面以及已释放但尚未被系统回收的驻留页面都算进去;任务管理器"内存"列的口径是私有工作集。
- 修法:ProcessWorkingSetBytes() → ProcessPrivateWorkingSetBytes(),用 PROCESS_MEMORY_COUNTERS_EX2
  (MinGW 的 psapi.h 没有该类型 → 在 .cpp 里就地声明,布局与文档一致),失败时依次退回
  PrivateUsage(私有提交)→ WorkingSetSize。
- 实测(离屏探针):加载后 total/priv/commit = 117.55/109.96/138.28 MiB;
  关闭分组后 = 15.44/7.62/8.27 MiB ⇒ 内存没有泄漏、完整归还,用户的差异来自口径。

四、边沿计数(EdgeCountsFor)改为「逐块累积 + 每 tick 预算推进」
- 量化(探针):改前 6M 采样 = 26.8ms / 次阈值变化 → 25.8M 采样 ≈115ms/条;
  横向光标拖动时阈值每帧都变 ⇒ 单条信号每帧 115ms,5 条信号同一网格 = 575ms/帧
  (与用户"开启即卡顿、之后光标几乎拖不动"完全吻合)。
- 新模型:
  * 采样对 k(采样 k-1 → k)按 kEdgeBlockSamples = 8192 分块,每块各存上升/下降沿数;
    scannedPair = 下一个待扫描的采样对([1, scannedPair) 已扫完)。
  * 查询区间:内部整块用块和相加,两端边界块就地精确扫描(≤2 块,代价固定)⇒ 拖动时间光标的
    单帧代价与区间长度无关(单调时间轴下区间不进键);非单调时间轴无法用下标区间表达 ⇒ 区间进键。
  * 每 tick 预算 kEdgeScanPairsPerTick = 1200000(GetTickCount64 分桶,同帧多次调用共享);
    预算用完返回当前部分值,complete 出参告诉界面"还没算完"。
  * 拖动横向光标(EDGE_MOVE)期间不推进(阈值每帧都变,推进纯属白做功)→ 返回"最新的完整条目"
    的结果(数字稳定)+ complete = false。
  * 快路径:单调 && 无镜像 && 无调整变换时用紧凑无分支循环直接比较 values[k] 与阈值
    (NaN 与任何数比较都是 false ⇒ 天然满足"NaN 不参与且打断连续",无需 isfinite 分支)。
- 界面:新增 EdgeCountsText(rising, falling, complete) 统一生成文本(完全未算出显示"统计中…"、
  部分结果追加" …"),EdgePanelWidth 与绘制都改用它,宽度按实际文本测量避免数字被截断。
- 实测(改后):6M 采样首次调用 1.7ms(部分值)、5 帧内扫完且与参考实现逐位一致;
  单次调用最坏 1.9ms;拖动中连查 10 个阈值合计 0.11ms;区间查询在扫描完成后一次调用即精确。
- EdgeCenterCountsFor 未改(它的曲线窗口本来就被 CurveSpanFor 限制,不是本次报告的场景)。

五、内存与性能排查报告(第 4 项按用户要求只排查、未改动)
- 泄漏审计:RemoveFile 路径会 PauseDerivedAsync(清 m_curveEntries/m_curveJobs/m_curvePins)
  → InvalidateDerivedCaches(清折线/边沿/均值/弱峰/场景缓存),DataSet::RemoveFile 用
  "前移 + pop_back + shrink_to_fit" 立即归还采样缓冲;实测三种内存口径全部回落到十几 MB 以下。
- 弱峰调整内存暴涨(峰值 4.4GB):实测工作缓冲 ≈34 B/采样(主导项 = 极值表 PeakExtremum 24 B/极值,
  编码器方波极值密集 ⇒ 接近 24 B/采样),3M 采样单次 ≈103 MiB、4 条工作线程并发 +412 MiB;
  按 25.8M 采样外推 ⇒ 单次 ≈0.9GB、峰值 ≈3.5GB,再加数据本体 1.03GB 与曲线缓存预算 ≤768MB
  ⇒ 4~5GB,与用户观测吻合。
- 现有机制只解决"算完/关掉之后归还"(ReuseBufferScope / TrimDerivedCurveDataIfIdle /
  kHugeSignalPreviewSamples / 曲线缓存字节预算),没有任何"峰值上限"与"优雅降级":
  全仓没有 try/catch,重分配失败抛 std::bad_alloc 而重计算都在 std::thread 里
  (CurveWorkerLoop / RunAsyncLoad),异常逸出线程入口 ⇒ std::terminate() ⇒ 进程终止。
  1GB 剩余内存时:先大量换页(系统卡顿),提交失败则直接崩溃。
- 建议(未实施):①超大信号派生计算串行化 ②极值表字段瘦身/分块流式 ③废弃任务尽早退出
  ④GlobalMemoryStatusEx 自适应降档 ⑤线程入口加 try/catch。

六、验证(全部离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/probe(诊断探针):内存口径与泄漏审计、边沿计数扫描耗时、弱峰工作缓冲峰值
  (带 1ms 峰值采样线程)。
- .preflight/uitest 新增 [AR](边沿计数预算/精确性/区间查询/拖动期 O(1)、GX 徽标悬停光标)与
  [AS](标题栏 RAM 口径 = 私有工作集);并给菜单类用例加了关闭残留菜单 + 允许补点的卫生逻辑。
- 合计 738/738 通过(连跑 5 次约 1 次出现既有 harness 抖动:菜单点击类用例,重跑即好);
  make release 零警告(Target.exe 被占用时链接报 Permission denied,用临时 -o 名字验证链接)。

规则同步:Documents/agent_mem.md 新增 §9.22(含内存与性能排查报告),§11.2 新增第 26–31 条硬约束,
变更日志补一行。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-14 14:12:54 +08:00
1 parent a2a38236fe
commit d6d3fcd6c1
7 files changed
+548 -119

No files matched your search

+275 -85
View File
@@ -149,6 +149,42 @@ namespace jdi
const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */
/**
* @brief 「边沿计数」累积统计的分块大小(采样点数)。
*
* 每块各存上升/下降沿数:查询某个时间区间时,内部整块用存储值相加、只有两端边界块
* 需要就地精确扫描(≤ 2 块),因此拖动时间光标的单帧代价与区间长度无关。
* 取小一点 ⇒ 边界块扫描更便宜;取大一点 ⇒ 块数组更省内存。
*/
const std::size_t kEdgeBlockSamples = 8192;
/**
* @brief 每个 tick(约一帧)允许「边沿计数」扫描的采样对总数。
*
* 千万级信号整段扫描一次 ≈115ms(用户报告:5 条信号同一网格时单帧 ≈575ms,
* 界面完全失去响应)。改成预算推进后,单帧最多只花 ≈5ms 在统计上,
* 剩下的部分在后续帧里接着扫(未扫完时界面显示上一个完整结果 + “…”)。
*/
const std::size_t kEdgeScanPairsPerTick = 1200000;
/**
* @brief 「边沿计数」结果的显示文本。
* @param [IN] rising 上升沿数量。 @param [IN] falling 下降沿数量。
* @param [IN] complete 是否为该阈值的完整精确值。
* @return 面板/宽度测量共用的文本(未算完时带省略号,完全未开始时显示“统计中…”)。
*/
std::wstring EdgeCountsText(std::size_t rising, std::size_t falling, bool complete)
{
if (!complete && rising == 0 && falling == 0)
{
return L"统计中…";
}
std::wstring text = L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
std::to_wstring(falling);
if (!complete)
{
text += L" …";
}
return text;
}
/**
* @brief 判断坐标是否命中「弱峰调整」的某条滑块轨道。
@@ -2896,27 +2932,31 @@ namespace jdi
void PlotView::EdgeCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
std::size_t& rising, std::size_t& falling) const
std::size_t& rising, std::size_t& falling, bool& complete) const
{
rising = 0;
falling = 0;
complete = false;
const std::vector<double>& values = signal.values;
// ⚠️ 阈值(横向光标幅值)是**屏幕上的值**,因此判定必须基于**画出来的曲线**:
// 先按下标映射(水平镜像)取源采样,再套用「调整」变换(系数 → 绝对值 → 垂直镜像 →
// Y 偏移)。只比较原始采样值时,“系数 50”这类信号的原始值域与阈值完全不相交,
// 表现就是“明明是很规整的方波,统计出来却全是 0”(用户报告)。
// Y 偏移)。只比较原始采样值时,"系数 50" 这类信号的原始值域与阈值完全不相交,
// 表现就是"明明是很规整的方波,统计出来却全是 0"(用户报告)。
const std::size_t count = values.size();
if (count < 2)
{
complete = true;
return;
}
const bool mirrored = style.mirrorHorizontal;
const bool transform = (style.gain != 1.0f || style.absolute || style.mirrorVertical ||
style.offsetY != 0.0f);
const std::uint64_t styleKey = EdgeStyleKey(style);
std::size_t begin = 0;
std::size_t end = 0;
const bool indexed = EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, begin, end);
// 时间单调 ⇒ 统计区间可以用**采样对下标区间**表达(区间不进键,拖动时间光标无需重扫)。
const bool monotonic = file.timeMonotonic;
// 判定基准:**绘制值**相对阈值的偏差。区间之外的采样返回 NaN(等价于原实现
// 「区间外采样不参与判定且打断连续」的语义)。
// 判定基准:**绘制值**相对阈值的偏差(区间之外 / 缺失值返回 NaN = 不参与且打断连续)。
const auto deltaAt = [&values, &file, &style, mirrored, transform, count, hasRange, rangeMin,
rangeMax](std::size_t index) -> double
{
@@ -2946,86 +2986,234 @@ namespace jdi
return std::isfinite(drawn) ? drawn : std::numeric_limits<double>::quiet_NaN();
};
// 按「文件 + 信号 + 样式指纹 + 阈值 + 是否限定区间」定位缓存项(**时间区间不进缓存键**)。
EdgeCountEntry* entry = nullptr;
for (EdgeCountEntry& candidate : m_edgeCountCache)
// 请求的「采样对」区间 [pairFirst, pairLast](pair k = 采样 k-1 → k,k ≥ 1)。
// ⚠️ 只有**两个端点都落在区间内**的采样对才计入(非单调时无法用下标表达,由 deltaAt 过滤)。
std::size_t pairFirst = 1;
std::size_t pairLast = count - 1;
if (hasRange && monotonic)
{
if (candidate.file == &file && candidate.signal == &signal &&
candidate.styleKey == styleKey && candidate.hasRange == hasRange &&
candidate.edgeValue == edgeValue)
std::size_t indexBegin = 0;
std::size_t indexEnd = 0;
EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, indexBegin, indexEnd);
pairFirst = (indexBegin + 1 < count) ? (indexBegin + 1) : count;
pairLast = (indexEnd > 0) ? (indexEnd - 1) : 0;
if (pairLast < pairFirst)
{
entry = &candidate;
break;
complete = true;
return;
}
}
if (entry == nullptr)
// ---- 定位 / 推进累积条目 -------------------------------------------------
// ⚠️ 拖动横向光标(`EDGE_MOVE`)时阈值每帧都变,推进等于纯白做功:此时不推进,
// 直接把上一个完整结果当作临时值(界面用 "…" 标出"还没算完")。
const bool thresholdDragging = (m_dragMode == PlotDragMode::EDGE_MOVE);
const auto sameSignal = [&file, &signal, styleKey](const EdgeCountEntry& candidate)
{ return candidate.file == &file && candidate.signal == &signal && candidate.styleKey == styleKey; };
const auto sameKey = [&](const EdgeCountEntry& candidate)
{
if (m_edgeCountCache.size() >= kEdgeCountCacheLimit)
if (!sameSignal(candidate) || candidate.threshold != edgeValue)
{
m_edgeCountCache.erase(m_edgeCountCache.begin());
return false;
}
EdgeCountEntry created;
created.file = &file;
created.signal = &signal;
created.styleKey = styleKey;
created.edgeValue = edgeValue;
created.hasRange = hasRange;
m_edgeCountCache.push_back(created);
entry = &m_edgeCountCache.back();
}
// 非单调时间轴:区间参与判定,必须一并比较(否则会混用别的区间结果)。
return monotonic || (candidate.hasRange == hasRange && candidate.rangeMin == rangeMin &&
candidate.rangeMax == rangeMax);
};
// 阈值参与判定,因此仿函数要在累加时一并带入。
const auto crossingAt = [&deltaAt, edgeValue](std::size_t index) -> double
{ return deltaAt(index) - edgeValue; };
// 区间完全一致 → 直接命中。
if (indexed && entry->reusable && entry->begin == begin && entry->end == end)
// 用**下标**而不是指针记录候选(`push_back` 可能重新分配,指针会失效)。
int entryIndex = -1;
int newestCompleteIndex = -1;
for (std::size_t index = 0; index < m_edgeCountCache.size(); ++index)
{
rising = entry->rising;
falling = entry->falling;
return;
const EdgeCountEntry& candidate = m_edgeCountCache[index];
if (!sameSignal(candidate))
{
continue;
}
if (entryIndex < 0 && sameKey(candidate))
{
entryIndex = static_cast<int>(index);
}
if (candidate.complete)
{
newestCompleteIndex = static_cast<int>(index); // 淘汰从 front 开始 ⇒ 越靠后越新
}
}
EdgeCountEntry* entry = (entryIndex >= 0) ? &m_edgeCountCache[static_cast<std::size_t>(entryIndex)] : nullptr;
if (!thresholdDragging && (entry == nullptr || !entry->complete))
{
if (entry == nullptr)
{
if (m_edgeCountCache.size() >= kEdgeCountCacheLimit)
{
m_edgeCountCache.erase(m_edgeCountCache.begin());
}
EdgeCountEntry created;
created.file = &file;
created.signal = &signal;
created.styleKey = styleKey;
created.threshold = edgeValue;
created.hasRange = hasRange && !monotonic;
created.rangeMin = rangeMin;
created.rangeMax = rangeMax;
const std::size_t blocks = (count - 1) / kEdgeBlockSamples + 2;
created.blockRising.assign(blocks, 0);
created.blockFalling.assign(blocks, 0);
m_edgeCountCache.push_back(std::move(created));
entry = &m_edgeCountCache.back();
newestCompleteIndex = -1; // 可能已被重新分配/淘汰,不再使用它
}
// 扫描预算按 tick 共享:同一帧里本函数会被调用多次(排版 + 绘制 + 多条信号),
// 若每次都各扫一份,单帧总开销就会翻好几倍(实测整段 25.8M 採样 ≈115ms)。
const std::uint64_t tick = GetTickCount64();
if (tick != m_edgeScanTick)
{
m_edgeScanTick = tick;
m_edgeScanBudget = kEdgeScanPairsPerTick;
}
if (m_edgeScanBudget > 0 && entry->scannedPair < count)
{
std::size_t scanned = 0;
std::size_t block = (entry->scannedPair - 1) / kEdgeBlockSamples;
std::size_t pair = entry->scannedPair;
const bool fastPath = monotonic && !mirrored && !transform;
while (pair < count && scanned < m_edgeScanBudget)
{
const std::size_t blockFirst = block * kEdgeBlockSamples + 1;
const std::size_t blockLast = std::min(count - 1, (block + 1) * kEdgeBlockSamples);
const std::size_t from = std::max(pair, blockFirst);
const std::size_t to = std::min(blockLast, from + (m_edgeScanBudget - scanned) - 1);
if (to < from)
{
break;
}
std::size_t localRising = 0;
std::size_t localFalling = 0;
if (fastPath)
{
// 紧凑循环:无变换 / 无镜像 / 时间单调 ⇒ 直接比较阈值,NaN 天然不计数。
// (NaN 与任何数比较都为 false,因此不需要额外的 isfinite 分支。)
double previous = values[from - 1] - edgeValue;
for (std::size_t index = from; index <= to; ++index)
{
const double current = values[index] - edgeValue;
if (previous < 0.0 && current >= 0.0)
{
++localRising;
}
else if (previous > 0.0 && current <= 0.0)
{
++localFalling;
}
previous = current;
}
}
else
{
double previous = deltaAt(from - 1) - edgeValue;
for (std::size_t index = from; index <= to; ++index)
{
const double current = deltaAt(index) - edgeValue;
if (std::isfinite(previous) && std::isfinite(current))
{
if (previous < 0.0 && current >= 0.0)
{
++localRising;
}
else if (previous > 0.0 && current <= 0.0)
{
++localFalling;
}
}
previous = current;
}
}
if (block < entry->blockRising.size())
{
entry->blockRising[block] += localRising;
entry->blockFalling[block] += localFalling;
}
entry->rising += localRising;
entry->falling += localFalling;
scanned += (to - from + 1);
pair = to + 1;
if (to == blockLast)
{
++block;
}
}
entry->scannedPair = pair;
entry->complete = (pair >= count);
m_edgeScanBudget -= std::min(m_edgeScanBudget, scanned);
}
}
// 与上一帧共享一个端点(拖动光标时必然如此)→ 只扫描两端差集并增量加减。
const bool shareBegin = indexed && entry->reusable && entry->begin == begin;
const bool shareEnd = indexed && entry->reusable && entry->end == end;
const std::size_t oldBegin = entry->begin;
const std::size_t oldEnd = entry->end;
std::size_t deltaRising = 0;
std::size_t deltaFalling = 0;
if (shareBegin && !shareEnd)
// ---- 汇总请求区间 -------------------------------------------------------
// 优先用「键完全匹配」的条目(可能仍在推进);没有就用最新的完整条目顶替,
// 这样拖动阈值期间数字保持稳定、算完后自动切换到精确值。
const EdgeCountEntry* source = (entry != nullptr)
? entry
: ((newestCompleteIndex >= 0)
? &m_edgeCountCache[static_cast<std::size_t>(newestCompleteIndex)]
: nullptr);
if (source == nullptr)
{
// 右端移动:变动的是采样对 k ∈ [min(oldEnd, end), max(oldEnd, end) - 1]。
const std::size_t low = std::min(oldEnd, end);
const std::size_t high = std::max(oldEnd, end);
AccumulateEdgeCrossings(std::max<std::size_t>(1, low), high - 1, crossingAt, deltaRising,
deltaFalling);
rising = (end > oldEnd) ? (entry->rising + deltaRising) : (entry->rising - deltaRising);
falling = (end > oldEnd) ? (entry->falling + deltaFalling) : (entry->falling - deltaFalling);
return; // 尚无任何结果:界面显示"统计中…"
}
else if (shareEnd && !shareBegin)
const auto sumRange = [&](const EdgeCountEntry& from, std::size_t first, std::size_t last,
std::size_t& outRising, std::size_t& outFalling) -> bool
{
// 左端移动:变动的是采样对 k ∈ [min(oldBegin, begin) + 1, max(oldBegin, begin)]。
const std::size_t low = std::min(oldBegin, begin);
const std::size_t high = std::max(oldBegin, begin);
AccumulateEdgeCrossings(low + 1, high, crossingAt, deltaRising, deltaFalling);
rising = (begin < oldBegin) ? (entry->rising + deltaRising) : (entry->rising - deltaRising);
falling = (begin < oldBegin) ? (entry->falling + deltaFalling) : (entry->falling - deltaFalling);
}
else
{
// 两端都变了(或首次统计):整段重算。
const std::size_t pairFirst = (end > begin) ? (begin + 1) : 1;
const std::size_t pairLast = (end > begin) ? (end - 1) : 0;
AccumulateEdgeCrossings(pairFirst, pairLast, crossingAt, rising, falling);
}
entry->reusable = indexed;
entry->begin = begin;
entry->end = end;
entry->rising = rising;
entry->falling = falling;
outRising = 0;
outFalling = 0;
if (last < first)
{
return true;
}
const std::size_t limit = (from.scannedPair > 0) ? std::min(last, from.scannedPair - 1) : 0;
if (limit >= first)
{
std::size_t block = (first - 1) / kEdgeBlockSamples;
const std::size_t lastBlock = (limit - 1) / kEdgeBlockSamples;
for (; block <= lastBlock; ++block)
{
const std::size_t blockFirst = block * kEdgeBlockSamples + 1;
const std::size_t blockLast = (block + 1) * kEdgeBlockSamples;
const std::size_t low = std::max(first, blockFirst);
const std::size_t high = std::min(limit, blockLast);
const bool wholeBlock = (low == blockFirst) && (high == blockLast);
if (wholeBlock && from.scannedPair > blockLast && block < from.blockRising.size())
{
outRising += from.blockRising[block];
outFalling += from.blockFalling[block];
continue;
}
// 边界块 / 尚未扫完的块:就地精确扫描这一小段(≤ 1 个块)。
double previous = deltaAt(low - 1) - edgeValue;
for (std::size_t index = low; index <= high; ++index)
{
const double current = deltaAt(index) - edgeValue;
if (std::isfinite(previous) && std::isfinite(current))
{
if (previous < 0.0 && current >= 0.0)
{
++outRising;
}
else if (previous > 0.0 && current <= 0.0)
{
++outFalling;
}
}
previous = current;
}
}
}
return (last < from.scannedPair);
};
const bool covered = sumRange(*source, pairFirst, pairLast, rising, falling);
complete = covered && sameKey(*source);
}
float PlotView::EdgePanelWidth(int laneIndex, EdgePanelKind kind) const
@@ -3070,6 +3258,7 @@ namespace jdi
}
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = true;
if (meanFilter || weakPeak)
{
continue;
@@ -3088,12 +3277,12 @@ namespace jdi
else
{
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, lane.edgeValue,
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling);
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
}
// 宽度按实际要显示的文本测量(含未算完时的“统计中…”/省略号),
// 否则推进过程中数字变长会被截断(统计数字必须完整显示)。
const float countWidth =
UiMeasureTextWidth(m_fonts->monoSmall,
L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
std::to_wstring(falling));
UiMeasureTextWidth(m_fonts->monoSmall, EdgeCountsText(rising, falling, complete));
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth));
}
@@ -4233,12 +4422,12 @@ namespace jdi
}
}
// 图例中的「GX」编号徽标:悬停时返回**手型** —— 与左侧信号列表里同一个 GX 徽标的
// 悬停效果完全一致(用户明确要求"参考信号选区的信号名后方的 GX 标签");
// 真正按下拖拽时再切为上下箭头(见 LANE_REORDER 分支)。
// 图例中的「GX」编号徽标:**悬停时不改变鼠标形状**(用户明确要求:只在按下左键
// 拖拽时才显示上下箭头,见 `LANE_REORDER` 分支)。这里保持“不改变”的语义并
// **提前返回**(不再往下落到光标线/图例判定),与原先的优先级完全一致。
if (LaneBadgeAtPoint(x, y) >= 0)
{
return LoadCursorW(nullptr, IDC_HAND);
return nullptr;
}
// 光标时间读数标签与双光标差值标签:**悬停(未按下)时不改变鼠标形状**。
@@ -11665,10 +11854,11 @@ namespace jdi
std::size_t rising = 0;
std::size_t falling = 0;
bool complete = true;
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, tabLane.edgeValue,
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling);
const std::wstring counts = L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
std::to_wstring(falling);
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling,
complete);
const std::wstring counts = EdgeCountsText(rising, falling, complete);
UiText(g, counts, m_fonts->monoSmall,
gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight),
m_theme->textMuted, TextAlign::LEFT);