feat(ui): 双光标样式统一与配色、光标滞留视野外、差值标签与拖拽、选区放大交互与提示

1) 光标 2 的样式与光标 1 完全一致,并改用更鲜艳的颜色
- 原先光标 2 用朝上的三角把手,观感突兀;现两条光标统一为同款朝下三角
- 主题新增 cursorAlt 并改用 Solarized 洋红 #D33682(原青色偏灰),与光标 1 的
  橙色 #CB4B16 同属暖色且更醒目,靠颜色而非形状区分

2) 光标允许滞留在网格可视区域之外(不再被强行夹回视野)
- 删除"视图变化后夹取光标"的逻辑(时间轴选区放大、适应视图、按网格时间轴展开、
  平移时都不再移动光标);仅在拖拽光标本身时才使用 ClampCursorToView(which)
- 新增 CursorDisplayX(which):光标在视野外时把它的显示位置"钉"在最近的网格边缘,
  取值标注与时间读数因此始终可见;数据点改为边缘三角指示(指向光标实际方向)
- 命中测试改用"显示横坐标",因此被钉在边缘的光标仍可抓住拖回

3) 光标读数增加细描边,避免与背景/曲线颜色过于接近
- 新增 DrawLabelOutline():按文字颜色取反色做 1px 半透明描边,
  网格内的取值胶囊、光标时间读数、差值标签统一使用

4) 双光标差值显示(时间轴上方居中,自适应单位,可拖拽、可编辑)
- 新增 FormatDuration()/FormatDurationNumber()/ChosenDurationUnit():
  在 h / min / s / ms / us / ns / ps 之间自动选择单位(数值落在 [1,1000))
- 差值标签位于两条时间读数的中点,样式与时间读数一致
- 双击进入编辑:输入值采用**当前显示单位**;确认后**前方(时间较小)光标不动**,
  后方光标移动到"前方 + 输入差值"
- 标签支持左右拖拽(新增 PlotDragMode::CURSOR_SPAN_MOVE):两条光标整体平移、间隔不变

5) 双光标内容重叠时的层次与差值标签位置
- 新增 m_activeCursor:最后操作过(拖拽/单击/编辑)的光标其内容绘制在上层,
  两条光标的竖线、取值胶囊、时间读数都按此顺序绘制
- 差值标签改为按**两条时间读数的实际净空**判断:放不下时自动落到主时间轴内部
  (原先按光标线间距判断,光标被钉到边缘时会用差值标签盖住时间读数)

6) 选区放大的光标形状与颜色
- 未开始框选时不再改变鼠标形状(保持箭头);按下但未拖动时为十字 IDC_CROSS;
  确定为时间轴选区后为左右箭头、Y 轴选区后为上下箭头
- 两种方向的选区统一使用强调色(原先 Y 轴选区用警示红,观感不一致)
- 新增 Application::UpdateDragCursor():Windows 在鼠标捕获期间不派发 WM_SETCURSOR,
  必须在 WM_MOUSEMOVE / WM_LBUTTONDOWN 后主动刷新光标形状,否则形状不会变化

7) 提示浮层加大并支持"停留后淡出",光标模式不再提示
- 提示字号由 uiSmall 改为 ui,胶囊 30px 高、内边距更大
- 新增停留 1300ms + 淡出 500ms 的时间控制;宿主用 WM_TIMER(33ms) 驱动重绘,
  淡出结束自动 KillTimer(文字走 GDI 不支持 alpha,改为向胶囊底色混色实现淡出)
- 「光标」三段式切换不再显示任何提示

验证([1].CSV 11 信号,窗口 1725x996,125% DPI):
- 双光标:橙 0.63 / 洋红 0.7812,同款把手;底部时间读数 + 居中差值 "151.2 ms"
- 放大到不含光标的区间:光标不移动,取值以边缘三角 + 胶囊贴边显示
- 拖拽差值标签:两光标从 0.63/0.7812 整体平移到 0.424202/0.575402,差值仍为 151.2 ms
- 差值编辑:输入 300(显示单位为 ms)→ 前方光标保持 0.63,后方移动到 0.93
- 重叠层次:差值设为 0 时洋红(活动)在上;单击光标线后橙色(活动)在上
- 光标形状:未框选 ARROW / 按下 CROSS / 横向 SIZEWE / 纵向 SIZENS(cursor.ps1 实测)
- 提示浮层:出现后约 1.3s 开始淡出、约 1.8s 消失;右键放弃选区后视图不变且模式保持选中
- 空闲 CPU 0.00%;make clean; make all 无警告无错误
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-08 19:37:26 +08:00
1 parent 388d3b6441
commit eaf5b9cadf
5 files changed
+500 -117

No files matched your search

@@ -301,6 +301,17 @@ namespace jdi
*/
bool HandleMouseMove(int x, int y, bool buttonDown);
/**
* @brief 拖动(鼠标捕获)期间主动刷新光标形状。
*
* Windows 在鼠标被捕获时不再派发 `WM_SETCURSOR`,若不主动设置,
* 光标会一直停留在按下瞬间的形状(例如选区放大时看不到十字/方向箭头)。
*
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
*/
void UpdateDragCursor(int x, int y);
HINSTANCE m_instance = nullptr; /**< 模块实例句柄。 */
HWND m_hwnd = nullptr; /**< 主窗口句柄。 */
UINT m_dpi = 96; /**< 当前 DPI。 */
@@ -339,6 +350,7 @@ namespace jdi
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
bool m_hintTimerActive = false; /**< 提示浮层淡出定时器是否已启动。 */
int m_tabMenuIndex = -1; /**< 标签栏右键菜单的目标标签页下标。 */
int m_plotMenuLane = -1; /**< 绘图区右键菜单的目标网格下标。 */
@@ -34,6 +34,8 @@ namespace jdi
const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */
const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */
const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */
const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
/** @brief 标签页右键菜单命令 ID。 */
const int kMenuTabNew = 1;
@@ -716,6 +718,21 @@ namespace jdi
BitBlt(windowDc, 0, 0, width, height, m_backDc, 0, 0, SRCCOPY);
ReleaseDC(m_hwnd, windowDc);
}
// 提示浮层需要停留后淡出:仅在其仍可见时保持定时重绘,淡出结束即停表。
if (m_plot.HintNeedsAnimation())
{
if (m_hintTimerActive == false)
{
SetTimer(m_hwnd, kHintTimerId, kHintTimerPeriodMs, nullptr);
m_hintTimerActive = true;
}
}
else if (m_hintTimerActive)
{
KillTimer(m_hwnd, kHintTimerId);
m_hintTimerActive = false;
}
}
void Application::SetStatusMessage(const std::wstring& text, bool isError)
@@ -1148,6 +1165,17 @@ namespace jdi
return false;
}
void Application::UpdateDragCursor(int x, int y)
{
// 鼠标捕获期间不再有 WM_SETCURSOR,只能自己按当前状态设置光标。
HCURSOR cursor = nullptr;
if (m_activeTarget == Target::PLOT)
{
cursor = m_plot.QueryCursor(x, y);
}
SetCursor(cursor != nullptr ? cursor : LoadCursorW(nullptr, IDC_ARROW));
}
bool Application::HandleMouseMove(int x, int y, bool buttonDown)
{
if (buttonDown && m_activeTarget != Target::NONE)
@@ -1820,6 +1848,12 @@ namespace jdi
{
InvalidateRect(hwnd, nullptr, FALSE);
}
// 拖动(鼠标捕获)期间 Windows 不会派发 WM_SETCURSOR,
// 因此需要主动刷新一次,否则光标形状会一直停在按下的状态。
if (buttonDown)
{
UpdateDragCursor(x, y);
}
return 0;
}
case WM_MOUSELEAVE:
@@ -1916,6 +1950,9 @@ namespace jdi
ApplyPendingRename();
InvalidateRect(hwnd, nullptr, FALSE);
}
// 拖动期间 Windows 不再派发 WM_SETCURSOR,需在按下后立即刷新一次,
// 使"按下但尚未拖动"时能立刻看到十字/方向箭头提示。
UpdateDragCursor(x, y);
return 0;
}
@@ -2225,6 +2262,16 @@ namespace jdi
RemoveFile(static_cast<int>(wParam));
return 0;
}
case WM_TIMER:
{
// 提示浮层淡出:仅重绘,不再做其它处理;Render 会在淡出结束后停表。
if (wParam == kHintTimerId)
{
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
}
case WM_DROPFILES:
{
HDROP drop = reinterpret_cast<HDROP>(wParam);
+52 -5
View File
@@ -26,6 +26,7 @@ namespace jdi
RUBBER_SELECT, /**< 选区放大:按拖拽方向决定横向(时间轴)或纵向(幅值轴)选区。 */
SCROLL_LANES, /**< 拖动网格滚动条。 */
CURSOR_MOVE, /**< 拖拽时间光标。 */
CURSOR_SPAN_MOVE, /**< 拖拽双光标差值标签:两条光标整体平移,间隔不变。 */
MOVE_SIGNAL /**< 拖拽信号曲线到其它网格。 */
};
@@ -46,6 +47,7 @@ namespace jdi
{
NONE = 0, /**< 未处于输入状态。 */
CURSOR_TIME, /**< 编辑时间光标位置。 */
CURSOR_SPAN, /**< 编辑双光标之间的时间差(前方光标不动,后方光标随之移动)。 */
LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */
LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */
SIGNAL_NAME /**< 编辑图例中某信号的显示名称。 */
@@ -257,6 +259,15 @@ namespace jdi
*/
float LaneContentHeight() const { return m_laneContentHeight; }
/**
* @brief 提示浮层是否仍在显示(含淡出阶段)。
*
* 宿主可在返回 true 时保持定时重绘,使提示能"停留后淡出"。
*
* @return 需要继续重绘返回 true。
*/
bool HintNeedsAnimation() const;
/**
* @brief 清除已启用的时间光标。
*/
@@ -641,6 +652,13 @@ namespace jdi
*/
float CursorX(int which) const;
/**
* @brief 计算指定光标在屏幕上的**显示**横坐标(滞留在可视区外时贴到网格边缘)。
* @param [IN] which 光标序号(1 或 2)。
* @return 显示横坐标。
*/
float CursorDisplayX(int which) const;
/**
* @brief 计算指定光标时间读数标签的矩形。
* @param [IN] which 光标序号(1 或 2)。
@@ -659,11 +677,25 @@ namespace jdi
/**
* @brief 将两条光标的时间夹取到当前可见时间范围内。
*
* 缩放/平移/自适应会改变可见区间,若不夹取,光标会落到视图之外并“消失”,
* 表现为“放大后无法再拖动光标”。
* ⚠️ 现在**只在拖拽光标时**使用。视图变化(平移/放大/自适应)必须**不**移动光标:
* 光标允许滞留在网格可视区域之外,此时仅把它的取值/时间读数贴在网格边缘显示。
*
* @param [IN] which 要夹取的光标序号(1 或 2)。
*/
void ClampCursorToView();
void ClampCursorToView(int which);
/**
* @brief 计算双光标之间的时间差(绝对值)。
* @return 时间差;非双光标模式返回 0。
*/
double CursorSpan() const;
/**
* @brief 计算双光标差值标签的矩形(时间轴上方、两条时间读数的中间位置)。
* @param [OUT] insideAxis 为 true 时表示两条光标过近,标签落在主时间轴内部。
* @return 标签矩形;非双光标模式返回空矩形。
*/
gd::RectF CursorSpanRect(bool& insideAxis) const;
/**
* @brief 获取指定光标对应的显示颜色。
* @param [IN] which 光标序号(1 或 2)。
@@ -672,11 +704,19 @@ namespace jdi
std::uint32_t CursorColor(int which) const;
/**
* @brief 设置并显示一条临时提示(在绘图区底部以浮层方式绘制)。
* @param [IN] text 提示文本;传空串则清除提示。
* @brief 设置并显示一条临时提示(在绘图区底部以浮层方式绘制,短暂停留后淡出)。
* @param [IN] text 提示文本;传空串则立即清除提示。
*/
void SetHint(const std::wstring& text);
/**
* @brief 画文字时给底色胶囊加一层与文字反色的细描边,提升与背景/曲线的对比度。
* @param [IN] rect 胶囊矩形。
* @param [IN] radius 圆角半径。
* @param [IN] textDark 文字是否用深色。
*/
void DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark);
/**
* @brief 精确检测鼠标位置是否命中某条可见信号曲线。
* @param [IN] x 客户区横坐标。
@@ -837,7 +877,12 @@ namespace jdi
int m_dragMouseY = 0; /**< 拖拽过程中鼠标的当前纵坐标(虚影跟随)。 */
bool m_cursorHover = false; /**< 鼠标是否悬停在某条时间光标上。 */
int m_hoverCursor = 0; /**< 悬停/拖拽中的光标序号(1 或 2,0 表示无)。 */
int m_activeCursor = 1; /**< 最近操作过的光标:内容重叠时它绘制在上层。 */
int m_editCursor = 1; /**< 内联输入所作用的 Time 光标序号。 */
double m_editDurationUnit = 1.0; /**< 差值编辑时采用的显示单位(秒)。 */
int m_spanDragStartX = 0; /**< 差值标签拖拽起点横坐标。 */
double m_spanDragTime1 = 0.0; /**< 差值标签拖拽起始时两条光标的时间。 */
double m_spanDragTime2 = 0.0;
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
int m_editLane = -1; /**< 内联输入所作用的网格下标。 */
@@ -851,6 +896,8 @@ namespace jdi
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
std::wstring m_hintText; /**< 临时提示文本(空表示不显示)。 */
std::uint64_t m_hintShownAt = 0; /**< 提示出现的时刻(毫秒,GetTickCount64)。 */
gd::RectF m_cursorSpanRect; /**< 最近一次绘制时差值标签的矩形(用于命中拖拽/双击)。 */
std::vector<LaneVisual> m_lanes; /**< 当前网格绘制信息集合。 */
/**
+387 -110
View File
@@ -44,6 +44,90 @@ namespace jdi
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
const float kLegendMaxTextWidth = 200.0f; /**< 图例条目的文本宽度上限(超出则省略)。 */
const std::uint64_t kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */
const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */
/**
* @brief 时间间隔的显示单位。
*/
struct DurationUnit
{
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
const wchar_t* name = L"s"; /**< 单位名称。 */
};
/**
* @brief 为给定时间间隔挑选最合适的显示单位(使数值落在 [1, 1000))。
* @param [IN] seconds 时间间隔(秒)。
* @return 选中的单位。
*/
DurationUnit ChosenDurationUnit(double seconds)
{
const double value = std::fabs(seconds);
// 从大到小排列,取第一个使数值 >= 1 的单位。
const DurationUnit units[] = {
{3600.0, L"h"}, {60.0, L"min"}, {1.0, L"s"}, {1.0e-3, L"ms"},
{1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
};
const DurationUnit* chosen = &units[sizeof(units) / sizeof(units[0]) - 1];
for (const DurationUnit& unit : units)
{
if (value >= unit.scale)
{
chosen = &unit;
break;
}
}
return *chosen;
}
/**
* @brief 把时间间隔格式化为自适应单位的可读文本(含单位)。
*
* 在 h / min / s / ms / us / ns / ps 之间自动选择最合适的单位。
*
* @param [IN] seconds 时间间隔(秒)。
* @return 形如 "12.3 ms" 的文本。
*/
std::wstring FormatDuration(double seconds)
{
if (!std::isfinite(seconds))
{
return L"—";
}
const DurationUnit unit = ChosenDurationUnit(seconds);
std::wstring text = FormatValue(std::fabs(seconds) / unit.scale);
text += L" ";
text += unit.name;
return text;
}
/**
* @brief 把时间间隔格式化为"当前显示单位下的纯数值"(不含单位),用于内联编辑。
* @param [IN] seconds 时间间隔(秒)。
* @return 形如 "12.3" 的文本。
*/
std::wstring FormatDurationNumber(double seconds)
{
if (!std::isfinite(seconds))
{
return L"0";
}
const DurationUnit unit = ChosenDurationUnit(seconds);
return FormatValue(std::fabs(seconds) / unit.scale);
}
/**
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
* @param [IN] argb 原始 0xAARRGGBB 颜色。
* @param [IN] alpha 新的 alpha(0-255)。
* @return 缩放后的颜色。
*/
std::uint32_t ScaleAlpha(std::uint32_t argb, unsigned int alpha)
{
const unsigned int a = std::min(255u, alpha);
return (argb & 0x00FFFFFFu) | (a << 24);
}
/**
* @brief 判断颜色是否偏暗(用于在信号色底板上选择可读的文字颜色)。
@@ -524,7 +608,7 @@ namespace jdi
lane.autoRange = true;
}
}
ClampCursorToView();
// 适应视图已回到初始状态,这里不再移动光标(光标允许停留在可视区外)。
BuildLaneVisuals();
}
@@ -533,11 +617,11 @@ namespace jdi
Tab().CursorMode() = PlotCursorMode::NONE;
m_cursorHover = false;
m_hoverCursor = 0;
if (m_editTarget == PlotEditTarget::CURSOR_TIME)
if (m_editTarget == PlotEditTarget::CURSOR_TIME || m_editTarget == PlotEditTarget::CURSOR_SPAN)
{
EndEdit(false);
}
if (m_dragMode == PlotDragMode::CURSOR_MOVE)
if (m_dragMode == PlotDragMode::CURSOR_MOVE || m_dragMode == PlotDragMode::CURSOR_SPAN_MOVE)
{
m_dragMode = PlotDragMode::NONE;
}
@@ -545,7 +629,7 @@ namespace jdi
bool PlotView::CycleCursorMode()
{
// 三态循环:无 → 单光标 → 双光标 → 无。
// 三态循环:无 → 单光标 → 双光标 → 无。光标模式本身不显示提示。
switch (Tab().CursorMode())
{
case PlotCursorMode::NONE:
@@ -557,7 +641,7 @@ namespace jdi
Tab().CursorTimeAlt() = center;
m_cursorHover = true;
m_hoverCursor = 1;
SetHint(L"单光标:拖动竖线移动,双击竖线可输入精确时间");
m_activeCursor = 1;
break;
}
case PlotCursorMode::SINGLE:
@@ -568,7 +652,7 @@ namespace jdi
const double low = Tab().ViewMin();
const double high = Tab().ViewMax();
const double span = (high > low) ? (high - low) : 1.0;
double offset = span * 0.12;
const double offset = span * 0.12;
double second = Tab().CursorTime() + offset;
if (second > high - span * 0.02)
{
@@ -579,16 +663,15 @@ namespace jdi
second = low + span * 0.5;
}
Tab().CursorTimeAlt() = second;
ClampCursorToView();
ClampCursorToView(2);
m_cursorHover = true;
m_hoverCursor = 2;
SetHint(L"双光标:右侧光标显示当前值与 ±增量(右键光标可取消当前光标)");
m_activeCursor = 2;
break;
}
case PlotCursorMode::DOUBLE:
default:
DisableCursor();
SetHint(std::wstring());
break;
}
return true;
@@ -597,9 +680,29 @@ namespace jdi
void PlotView::SetHint(const std::wstring& text)
{
m_hintText = text;
m_hintShownAt = (text.empty()) ? 0 : GetTickCount64();
}
void PlotView::ClampCursorToView()
bool PlotView::HintNeedsAnimation() const
{
if (m_hintText.empty() || m_hintShownAt == 0)
{
return false;
}
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
return elapsed < (kHintHoldMs + kHintFadeMs);
}
double PlotView::CursorSpan() const
{
if (m_tab == nullptr || Tab().CursorMode() != PlotCursorMode::DOUBLE)
{
return 0.0;
}
return std::fabs(Tab().CursorTimeAlt() - Tab().CursorTime());
}
void PlotView::ClampCursorToView(int which)
{
if (m_tab == nullptr || m_plotRect.Width <= 0.0f)
{
@@ -611,10 +714,17 @@ namespace jdi
{
return;
}
// 不夹到边界上,留一点内缩,保证光标线及把手完整可见。
// 只在拖拽时夹取;留一点内缩,保证光标线及把手完整可见。
// 视图变化(平移/缩放/自适应)**不**调用本函数:光标允许滞留在网格可视区域之外。
const double inset = (high - low) * 0.002;
Tab().CursorTime() = ClampDouble(Tab().CursorTime(), low + inset, high - inset);
Tab().CursorTimeAlt() = ClampDouble(Tab().CursorTimeAlt(), low + inset, high - inset);
if (which == 2)
{
Tab().CursorTimeAlt() = ClampDouble(Tab().CursorTimeAlt(), low + inset, high - inset);
}
else
{
Tab().CursorTime() = ClampDouble(Tab().CursorTime(), low + inset, high - inset);
}
}
std::uint32_t PlotView::CursorColor(int which) const
@@ -718,7 +828,7 @@ namespace jdi
// 否则展开后会看到“时间对了但幅值区间不合适”的波形。
AutoRangeLaneToView(laneIndex);
ClampCursorToView();
// 注意:不在这里移动光标——光标允许滞留在网格可视区域之外。
BuildLaneVisuals();
return true;
}
@@ -836,6 +946,15 @@ namespace jdi
return TimeToX(time);
}
float PlotView::CursorDisplayX(int which) const
{
// 光标允许滞留在可视区之外:此时把它"钉"在最近的网格边缘上显示,
// 保证它的取值与时间读数始终可见。
const float raw = CursorX(which);
return std::max(m_plotRect.X + 1.0f * m_scale,
std::min(raw, m_plotRect.X + m_plotRect.Width - 1.0f * m_scale));
}
gd::RectF PlotView::CursorLabelRect(int which) const
{
if (m_fonts == nullptr || m_fonts->monoSmall == nullptr)
@@ -850,7 +969,7 @@ namespace jdi
const float height = 20.0f * m_scale;
// 时间读数改到绘图区**底部**:顶部留给网格图例与幅值标尺,避免互相遮挡。
float left = CursorX(which) - width * 0.5f;
float left = CursorDisplayX(which) - width * 0.5f;
left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width));
const float top = m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale;
@@ -869,12 +988,13 @@ namespace jdi
const float fx = static_cast<float>(x);
// 双光标重叠时优先命中光标 1,避免出现“抓不起来第二条”的情况。
if (std::fabs(fx - CursorX(1)) <= tolerance)
// 使用"显示横坐标":滞留在可视区外的光标被钉在边缘,也应该可以被抓住拖回来。
if (std::fabs(fx - CursorDisplayX(1)) <= tolerance)
{
which = 1;
return true;
}
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && std::fabs(fx - CursorX(2)) <= tolerance)
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && std::fabs(fx - CursorDisplayX(2)) <= tolerance)
{
which = 2;
return true;
@@ -912,9 +1032,7 @@ namespace jdi
Tab().ViewMin() = timeA;
Tab().ViewMax() = timeB;
ClampTimeView();
// 视图范围变化后必须把光标夹回可见区间,否则光标会跑到视野外“消失”,
// 用户会看到“放大后光标无法再拖动”。
ClampCursorToView();
// 注意:不在这里移动光标——即使选区不包含光标,也只把它的读数贴在网格边缘显示。
// 「选区 Y 轴自适应」开关开启的网格:时间选区放大后自动把 Y 轴匹配到新的可见区间。
ApplySelectionAutoFit();
@@ -1509,6 +1627,21 @@ namespace jdi
height);
}
if (m_editTarget == PlotEditTarget::CURSOR_SPAN)
{
// 差值输入框落在差值标签处(时间轴上方或时间轴内部)。
bool insideAxis = false;
const gd::RectF displayRect = CursorSpanRect(insideAxis);
if (displayRect.Width <= 0.0f)
{
return gd::RectF();
}
width = std::max(width, displayRect.Width);
float left = displayRect.X + displayRect.Width * 0.5f - width * 0.5f;
left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width));
return gd::RectF(left, displayRect.Y, width, height);
}
if (m_editTarget == PlotEditTarget::SIGNAL_NAME)
{
// 图例改名:输入框直接落在被双击的图例条目位置上。
@@ -1646,6 +1779,29 @@ namespace jdi
{
Tab().CursorTime() = clamped;
}
m_activeCursor = (m_editCursor == 2) ? 2 : 1;
return true;
}
if (target == PlotEditTarget::CURSOR_SPAN)
{
// 差值编辑:输入值采用编辑开始时的显示单位(如标签显示 ms 则输入的是 ms)。
// **前方(时间较小)的光标不动**,后方光标向后移动到"前方 + 输入差值"。
const double span = std::fabs(parsed) * m_editDurationUnit;
const double front = std::min(Tab().CursorTime(), Tab().CursorTimeAlt());
const bool secondIsBack = (Tab().CursorTimeAlt() >= Tab().CursorTime());
const double moved = front + span;
if (secondIsBack)
{
Tab().CursorTime() = front;
Tab().CursorTimeAlt() = moved;
}
else
{
Tab().CursorTimeAlt() = front;
Tab().CursorTime() = moved;
}
m_activeCursor = secondIsBack ? 2 : 1;
return true;
}
@@ -1829,15 +1985,20 @@ namespace jdi
case PlotDragMode::PAN:
return LoadCursorW(nullptr, IDC_SIZEALL);
case PlotDragMode::RUBBER_SELECT:
// 选区放大:按当前轴向给出对应方向的光标提示。
// 选区放大:轴向未定时为十字,确定后按方向给出上下/左右箭头。
if (m_rubberAxis == PlotRubberAxis::VALUE)
{
return LoadCursorW(nullptr, IDC_SIZENS);
}
return LoadCursorW(nullptr, IDC_SIZEWE);
if (m_rubberAxis == PlotRubberAxis::TIME)
{
return LoadCursorW(nullptr, IDC_SIZEWE);
}
return LoadCursorW(nullptr, IDC_CROSS);
case PlotDragMode::SCROLL_LANES:
return LoadCursorW(nullptr, IDC_SIZENS);
case PlotDragMode::CURSOR_MOVE:
case PlotDragMode::CURSOR_SPAN_MOVE:
return LoadCursorW(nullptr, IDC_SIZEWE);
case PlotDragMode::MOVE_SIGNAL:
return LoadCursorW(nullptr, IDC_SIZEALL);
@@ -1851,6 +2012,13 @@ namespace jdi
return LoadCursorW(nullptr, IDC_SIZENS);
}
// 双光标差值标签可拖拽。
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f &&
UiPointInRect(m_cursorSpanRect, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEWE);
}
{
int cursorWhich = 0;
if (IsOnCursorLine(x, cursorWhich) && UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y)))
@@ -1867,11 +2035,9 @@ namespace jdi
return LoadCursorW(nullptr, IDC_SIZEALL);
}
// 选区放大模式下不固定方向,用四向光标表示“横向/纵向都可拉”。
if (Tab().ZoomMode() == PlotZoomMode::SELECT)
{
return LoadCursorW(nullptr, IDC_SIZEALL);
}
// 选区放大模式下**未开始框选时保持默认箭头**(不改变鼠标形状);
// 只有真正按下并拖动后才切成十字/方向箭头。
// (m_dragMode == RUBBER_SELECT 的情况已在上面处理。)
}
return nullptr;
}
@@ -1906,7 +2072,6 @@ namespace jdi
Tab().ViewMin() += delta;
Tab().ViewMax() += delta;
ClampTimeView();
ClampCursorToView();
}
const int laneIndex = m_dragLane;
@@ -1991,11 +2156,6 @@ namespace jdi
{
target = snapped;
}
// 只允许在当前可见区间内移动,避免把光标拖到视野之外而“丢失”。
const double low = Tab().ViewMin();
const double high = Tab().ViewMax();
const double inset = (high > low) ? (high - low) * 0.002 : 0.0;
target = ClampDouble(target, low + inset, high - inset);
if (m_hoverCursor == 2 && Tab().CursorMode() == PlotCursorMode::DOUBLE)
{
@@ -2009,6 +2169,16 @@ namespace jdi
m_cursorHover = true;
return true;
}
case PlotDragMode::CURSOR_SPAN_MOVE:
{
// 拖拽差值标签:两条光标整体平移,间隔保持不变。
const double startTime = XToTime(static_cast<float>(m_spanDragStartX));
const double delta = XToTime(fx) - startTime;
Tab().CursorTime() = m_spanDragTime1 + delta;
Tab().CursorTimeAlt() = m_spanDragTime2 + delta;
m_dragMoved = true;
return true;
}
case PlotDragMode::MOVE_SIGNAL:
{
const int target = LaneAtY(y);
@@ -2028,13 +2198,6 @@ namespace jdi
bool changed = false;
// 网格内再次移动鼠标即认为用户已读到提示,把浮层收起(避免长期遮挡数据点取值)。
if (!m_hintText.empty())
{
m_hintText.clear();
changed = true;
}
const int lane = LaneAtY(y);
if (lane != m_hoverLane)
{
@@ -2112,6 +2275,17 @@ namespace jdi
return true;
}
// 双光标模式下先检查差值标签:它可拖拽(两条光标整体平移,间隔不变)。
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f &&
UiPointInRect(m_cursorSpanRect, fx, fy))
{
m_dragMode = PlotDragMode::CURSOR_SPAN_MOVE;
m_spanDragStartX = x;
m_spanDragTime1 = Tab().CursorTime();
m_spanDragTime2 = Tab().CursorTimeAlt();
return true;
}
// 靠近时间光标线时优先进入拖动,避免误触发平移或缩放选区。
{
int cursorWhich = 0;
@@ -2119,6 +2293,8 @@ namespace jdi
{
m_dragMode = PlotDragMode::CURSOR_MOVE;
m_hoverCursor = cursorWhich;
// 点中的光标成为“活动光标”:它触发的内容将画在上层。
m_activeCursor = cursorWhich;
m_cursorHover = true;
return true;
}
@@ -2201,6 +2377,8 @@ namespace jdi
case PlotDragMode::CURSOR_MOVE:
m_cursorHover = true;
break;
case PlotDragMode::CURSOR_SPAN_MOVE:
break;
case PlotDragMode::MOVE_SIGNAL:
{
const int target = LaneAtY(y);
@@ -2267,12 +2445,21 @@ namespace jdi
return false;
}
// 双光标差值标签:双击进入差值编辑(输入值采用当前显示单位)。
if (Tab().CursorMode() == PlotCursorMode::DOUBLE && m_cursorSpanRect.Width > 0.0f &&
UiPointInRect(m_cursorSpanRect, static_cast<float>(x), static_cast<float>(y)))
{
m_editDurationUnit = ChosenDurationUnit(CursorSpan()).scale;
return BeginEdit(PlotEditTarget::CURSOR_SPAN, -1, FormatDurationNumber(CursorSpan()));
}
// 命中时间光标:进入精确时间输入(交互与线宽输入框一致)。
{
int cursorWhich = 0;
if (IsOnCursorLine(x, cursorWhich))
{
m_editCursor = (cursorWhich == 2) ? 2 : 1;
m_activeCursor = m_editCursor;
const double time =
(m_editCursor == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
return BeginEdit(PlotEditTarget::CURSOR_TIME, -1, FormatValue(time));
@@ -3078,10 +3265,10 @@ namespace jdi
return;
}
// 时间轴选区用强调色,Y 轴选区用警示色,便于区分两种方向。
const std::uint32_t band = timeMode ? m_theme->accent : m_theme->danger;
UiFillRect(g, m_rubberRect, WithAlpha(band, 0x24));
UiDrawRoundRect(g, m_rubberRect, 2.0f * m_scale, band, 1.2f);
// 两种方向的选区使用**同一种强调色**,保持视觉一致。
const std::uint32_t band = m_theme->accent;
UiFillRect(g, m_rubberRect, WithAlpha(band, 0x28));
UiDrawRoundRect(g, m_rubberRect, 2.0f * m_scale, band, 1.4f);
// 选区跨度提示(时间用秒/数值用幅度量纲),放在选区顶部。
std::wstring text;
@@ -3113,25 +3300,41 @@ namespace jdi
void PlotView::PaintHint(gd::Graphics& g)
{
if (m_hintText.empty() || m_fonts == nullptr || m_fonts->uiSmall == nullptr)
if (m_hintText.empty() || m_fonts == nullptr || m_fonts->ui == nullptr || m_hintShownAt == 0)
{
return;
}
const std::uint64_t elapsed = GetTickCount64() - m_hintShownAt;
if (elapsed >= (kHintHoldMs + kHintFadeMs))
{
return;
}
// 停留结束后在 kHintFadeMs 内线性淡出(颜色 alpha 按比例缩放)。
const double fade = (elapsed <= kHintHoldMs)
? 1.0
: (1.0 - static_cast<double>(elapsed - kHintHoldMs) /
static_cast<double>(kHintFadeMs));
const unsigned int alpha =
static_cast<unsigned int>(std::max(0.0, std::min(1.0, fade)) * 255.0);
// 提示条贴在绘图区底部居中;时间光标的时间读数也在底部,
// 因此光标启用时把提示条上移一行,避免两者互相遮挡。
const float reserved = (m_tab != nullptr && Tab().CursorEnabled()) ? 24.0f * m_scale : 0.0f;
const float width = UiTextWidth(g, m_hintText, m_fonts->uiSmall) + 20.0f * m_scale;
const float height = 22.0f * m_scale;
const float reserved = (m_tab != nullptr && Tab().CursorEnabled()) ? 26.0f * m_scale : 0.0f;
const float width = UiTextWidth(g, m_hintText, m_fonts->ui) + 30.0f * m_scale;
const float height = 30.0f * m_scale;
float left = m_plotRect.X + (m_plotRect.Width - width) * 0.5f;
left = std::max(m_plotRect.X + 2.0f * m_scale,
std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale));
const gd::RectF pill(left, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale - reserved,
const gd::RectF pill(left, m_plotRect.Y + m_plotRect.Height - height - 4.0f * m_scale - reserved,
width, height);
UiFillRoundRect(g, pill, 3.0f * m_scale, WithAlpha(m_theme->readoutBg, 0xF2));
UiDrawRoundRect(g, pill, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
UiText(g, m_hintText, m_fonts->uiSmall, pill, m_theme->textStrong, TextAlign::CENTER);
UiFillRoundRect(g, pill, 4.0f * m_scale, ScaleAlpha(m_theme->readoutBg, alpha));
UiDrawRoundRect(g, pill, 4.0f * m_scale, ScaleAlpha(m_theme->borderStrong, alpha), 1.2f);
// 文字走 GDI(不支持 alpha 通道),因此把文字颜色向胶囊底色**混色**来实现淡出。
const std::uint32_t textColor =
MixColor(m_theme->textStrong, m_theme->readoutBg, 1.0f - static_cast<float>(fade));
UiText(g, m_hintText, m_fonts->ui, pill, textColor, TextAlign::CENTER);
}
void PlotView::PaintLane(gd::Graphics& g, int laneIndex, const LaneVisual& lane,
@@ -3644,6 +3847,62 @@ namespace jdi
}
}
void PlotView::DrawLabelOutline(gd::Graphics& g, const gd::RectF& rect, float radius, bool textDark)
{
// 用与文字同色的半透明细描边:深色文字 → 浅色描边,浅色文字 → 深色描边。
// 这样胶囊压在同类色背景或信号曲线上时仍有清晰的边界。
const std::uint32_t outline = textDark ? 0x99FDF6E3u : 0x99073642u;
UiDrawRoundRect(g, rect, radius, outline, 1.0f);
}
gd::RectF PlotView::CursorSpanRect(bool& insideAxis) const
{
insideAxis = false;
if (m_tab == nullptr || Tab().CursorMode() != PlotCursorMode::DOUBLE ||
m_fonts == nullptr || m_fonts->monoSmall == nullptr)
{
return gd::RectF();
}
const std::wstring text = FormatDuration(CursorSpan());
const float width = std::max(
72.0f * m_scale, UiMeasureTextWidth(m_fonts->monoSmall, text) + 18.0f * m_scale);
const float height = 20.0f * m_scale;
// 水平位置取两条光标时间读数的中点(与它们同一行,位于时间轴上方)。
// 注意用"显示横坐标":光标滞留在可视区外时会被钉在网格边缘,
// 此时二者在屏幕上必然很近,差值标签应改放到主时间轴内部。
const gd::RectF first = CursorLabelRect(1);
const gd::RectF second = CursorLabelRect(2);
float center = (CursorDisplayX(1) + CursorDisplayX(2)) * 0.5f;
if (first.Width > 0.0f && second.Width > 0.0f)
{
center = ((first.X + first.Width * 0.5f) + (second.X + second.Width * 0.5f)) * 0.5f;
}
float left = center - width * 0.5f;
left = std::max(m_plotRect.X + 2.0f * m_scale,
std::min(left, m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale));
// 两条**时间读数标签**之间的净空不足时,同一行放不下第三个标签
// → 改放到主时间轴内部(时间轴上方已无空间)。
const gd::RectF& leftRect = (first.X <= second.X) ? first : second;
const gd::RectF& rightRect = (first.X <= second.X) ? second : first;
const float freeStart = leftRect.X + leftRect.Width;
const float freeEnd = rightRect.X;
const bool fits = (freeStart <= freeEnd) && ((freeEnd - freeStart) >= (width + 2.0f * m_scale));
if (!fits)
{
insideAxis = true;
const float top = m_timeAxisRect.Y + 2.0f * m_scale;
const float axisHeight = std::max(14.0f * m_scale, m_timeAxisRect.Height - 4.0f * m_scale);
return gd::RectF(left, top, width, std::min(height, axisHeight));
}
return gd::RectF(left, m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale, width, height);
}
void PlotView::PaintCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
{
if (!Tab().CursorEnabled() || m_tab == nullptr)
@@ -3652,73 +3911,51 @@ namespace jdi
}
const bool doubleMode = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
// 双光标时统一以“时间较小者为左、较大者为右”,右侧光标负责显示 ±增量,
// 这样不论用户拖动哪一条,读数含义都稳定。
double leftTime = Tab().CursorTime();
double rightTime = Tab().CursorTimeAlt();
int leftWhich = 1;
int rightWhich = 2;
if (doubleMode && rightTime < leftTime)
{
std::swap(leftTime, rightTime);
leftWhich = 2;
rightWhich = 1;
}
const int cursorCount = doubleMode ? 2 : 1;
// 重叠时把"最近操作过的那条光标"的内容画在上层,因此先画另一条。
const int order[2] = { (m_activeCursor == 2) ? 1 : 2, (m_activeCursor == 2) ? 2 : 1 };
// ---- 光标竖线 + 顶部把手 ----
for (int which = 1; which <= cursorCount; ++which)
for (int pass = 0; pass < cursorCount; ++pass)
{
const int which = doubleMode ? order[pass] : 1;
const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
const float x = TimeToX(time);
if (x < m_plotRect.X - 2.0f || x > m_plotRect.X + m_plotRect.Width + 2.0f)
{
// 光标滞留在可视区外:不画竖线(其取值/时间读数会在下面贴边显示)。
continue;
}
const bool active = (m_hoverCursor == which) || (m_dragMode == PlotDragMode::CURSOR_MOVE && m_hoverCursor == which);
const bool hot = (m_hoverCursor == which);
const std::uint32_t color = CursorColor(which);
// 光标纵向贯穿全部网格,并延伸到时间轴底部,便于与刻度对齐读数。
const float lineTop = m_plotRect.Y;
const float lineBottom = m_timeAxisRect.Y + m_timeAxisRect.Height;
UiDrawLine(g, x, lineTop, x, lineBottom, color, (active ? 2.0f : 1.4f) * m_scale);
UiDrawLine(g, x, lineTop, x, lineBottom, color, (hot ? 2.0f : 1.4f) * m_scale);
// 顶部三角把手,提示该线可以被拖拽。(光标 2 用朝下的把手与光标 1 区分)
// 顶部三角把手(两条光标样式一致,靠颜色区分)。
const float arm = 5.0f * m_scale;
gd::PointF points[3];
if (which == 1)
{
points[0] = gd::PointF(x - arm, lineTop);
points[1] = gd::PointF(x + arm, lineTop);
points[2] = gd::PointF(x, lineTop + arm * 1.4f);
}
else
{
const float bottom = lineTop + arm * 1.4f + arm * 1.4f;
points[0] = gd::PointF(x - arm, bottom);
points[1] = gd::PointF(x + arm, bottom);
points[2] = gd::PointF(x, lineTop);
}
points[0] = gd::PointF(x - arm, lineTop);
points[1] = gd::PointF(x + arm, lineTop);
points[2] = gd::PointF(x, lineTop + arm * 1.4f);
gd::SolidBrush handle(ToGdiColor(color));
g.FillPolygon(&handle, points, 3);
}
// ---- 直接在光标穿过的数据点上标注取值 ----
// 每个网格里每条可见信号都标注一次,颜色与信号自身颜色对应;
// 双光标时右侧光标额外显示“当前值 , ±增量”。
// ---- 数据点取值标注 ----
for (int pass = 0; pass < cursorCount; ++pass)
{
const int which = doubleMode ? ((pass == 0) ? leftWhich : rightWhich) : 1;
const int which = doubleMode ? order[pass] : 1;
const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime();
const double otherTime = (which == 2) ? Tab().CursorTime() : Tab().CursorTimeAlt();
const bool showDelta = doubleMode && (which == rightWhich);
const float x = TimeToX(time);
if (x < m_plotRect.X - 2.0f || x > m_plotRect.X + m_plotRect.Width + 2.0f)
{
continue;
}
const bool showDelta = doubleMode && (which == order[1]);
// 光标在可视区外时,把标注"钉"在对应的网格边上,保证取值仍然可见。
const float pinX = CursorDisplayX(which);
const float rawX = TimeToX(time);
const bool pinned = (std::fabs(rawX - pinX) > 0.5f);
for (std::size_t laneIndex = 0; laneIndex < lanes.size(); ++laneIndex)
{
@@ -3755,16 +3992,34 @@ namespace jdi
const std::uint32_t signalColor = StyleColor(*resolved.style);
// 数据点:底色环 + 信号色实心点。
// 数据点:底色环 + 信号色实心点(贴边时改画成三角指示方向)。
const float radius = 3.4f * m_scale;
gd::SolidBrush ring(ToGdiColor(m_theme->cardBg));
gd::SolidBrush fill(ToGdiColor(signalColor));
g.FillEllipse(&ring, x - radius - 1.2f * m_scale, y - radius - 1.2f * m_scale,
(radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f);
g.FillEllipse(&fill, x - radius, y - radius, radius * 2.0f, radius * 2.0f);
if (pinned)
{
const float arm = 5.0f * m_scale;
const bool offLeft = (rawX < pinX);
const float baseX = offLeft ? (pinX - arm) : (pinX + arm);
gd::PointF tri[3];
tri[0] = gd::PointF(baseX, y - arm);
tri[1] = gd::PointF(baseX, y + arm);
tri[2] = gd::PointF(pinX, y);
g.FillPolygon(&ring, tri, 3);
gd::PointF inner[3];
inner[0] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y - arm * 0.72f);
inner[1] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y + arm * 0.72f);
inner[2] = gd::PointF(pinX - (offLeft ? -1.4f : 1.4f) * m_scale, y);
g.FillPolygon(&fill, inner, 3);
}
else
{
g.FillEllipse(&ring, pinX - radius - 1.2f * m_scale, y - radius - 1.2f * m_scale,
(radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f);
g.FillEllipse(&fill, pinX - radius, y - radius, radius * 2.0f, radius * 2.0f);
}
// 取值标注:底色用信号色、文字用反色,保证在任何波形上都清晰可读,
// 并让“读数 ↔ 信号”一一对应。
// 取值标注:底色用信号色、文字用反色,并加一层细描边提升对比度。
std::wstring text = FormatValue(value);
if (showDelta)
{
@@ -3782,14 +4037,14 @@ namespace jdi
const float textWidth = UiTextWidth(g, text, m_fonts->monoSmall) + 10.0f * m_scale;
const float textHeight = 17.0f * m_scale;
// 默认标在数据点右侧;右侧空间不足时改到左侧。
float labelLeft = x + radius + 4.0f * m_scale;
float labelLeft = pinX + radius + 4.0f * m_scale;
if (labelLeft + textWidth > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale)
{
labelLeft = x - radius - 4.0f * m_scale - textWidth;
labelLeft = pinX - radius - 4.0f * m_scale - textWidth;
}
labelLeft = std::max(m_plotRect.X + 2.0f * m_scale,
std::min(labelLeft, m_plotRect.X + m_plotRect.Width - textWidth - 2.0f * m_scale));
labelLeft = std::max(
m_plotRect.X + 2.0f * m_scale,
std::min(labelLeft, m_plotRect.X + m_plotRect.Width - textWidth - 2.0f * m_scale));
float labelTop = y - textHeight * 0.5f;
labelTop = std::max(clipTop + 1.0f * m_scale,
std::min(labelTop, clipBottom - textHeight - 1.0f * m_scale));
@@ -3797,23 +4052,24 @@ namespace jdi
UiFillRoundRect(g, labelRect, 3.0f * m_scale, WithAlpha(signalColor, 0xF0));
const bool dark = IsDarkColor(signalColor);
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
UiText(g, text, m_fonts->monoSmall, labelRect,
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
}
}
}
// ---- 时间读数:改到绘图区底部 ----
if (m_editTarget == PlotEditTarget::CURSOR_TIME)
// ---- 底部:时间读数 + 双光标差值 ----
if (m_editTarget == PlotEditTarget::CURSOR_TIME ||
m_editTarget == PlotEditTarget::CURSOR_SPAN)
{
PaintEditField(g);
return;
}
// 时间读数固定在绘图区底部,单/双光标都始终显示,
// 这样不必把鼠标移到光线上就能读到时间。
for (int which = 1; which <= cursorCount; ++which)
for (int pass = 0; pass < cursorCount; ++pass)
{
const int which = doubleMode ? order[pass] : 1;
const gd::RectF labelRect = CursorLabelRect(which);
if (labelRect.Width <= 0.0f)
{
@@ -3824,10 +4080,31 @@ namespace jdi
UiFillRoundRect(g, labelRect, 3.0f * m_scale,
highlighted ? color : WithAlpha(color, 0xE0));
const bool dark = IsDarkColor(color);
DrawLabelOutline(g, labelRect, 3.0f * m_scale, dark);
UiText(g, FormatValue(which == 2 ? Tab().CursorTimeAlt() : Tab().CursorTime()),
m_fonts->monoSmall, labelRect,
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
}
// 双光标差值:显示在两条时间读数中间(时间轴上方;过近时落到主时间轴上)。
if (doubleMode)
{
bool insideAxis = false;
const gd::RectF spanRect = CursorSpanRect(insideAxis);
m_cursorSpanRect = spanRect;
if (spanRect.Width > 0.0f && spanRect.Height > 0.0f)
{
const std::uint32_t fill = m_theme->readoutBg;
UiFillRoundRect(g, spanRect, 3.0f * m_scale, WithAlpha(fill, 0xF2));
UiDrawRoundRect(g, spanRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
UiText(g, FormatDuration(CursorSpan()), m_fonts->monoSmall, spanRect,
m_theme->textStrong, TextAlign::CENTER);
}
}
else
{
m_cursorSpanRect = gd::RectF();
}
}
void PlotView::PaintEditField(gd::Graphics& g)
+2 -2
View File
@@ -62,8 +62,8 @@ namespace jdi
theme.gridStrong = MakeArgb(0xC3B896u);
theme.axisLine = MakeArgb(kBase1);
theme.cursor = MakeArgb(kOrange);
// 第二光标用 Solarized 青色:与橙色同族饱和度、对比清晰又不刺眼。
theme.cursorAlt = MakeArgb(kCyan);
// 第二光标用 Solarized 洋红:与橙色同族暖色但更鲜艳,对比清晰又不刺眼。
theme.cursorAlt = MakeArgb(kMagenta);
theme.readoutBg = MakeArgb(0xF6EFDCu);
theme.danger = MakeArgb(kRed);
theme.warning = MakeArgb(kYellow);