feat(ui): 工具栏"选区"改名、菜单对勾右置与边距修正、选区自适应勾选、悬停/选区拖拽性能
1) 工具栏文案:"时间轴缩放"/"Y 轴缩放" -> "时间轴选区"/"Y 轴选区" - Toolbar.cpp 按钮标签、Toolbar.hpp 枚举注释、Application 状态提示语同步更新 2) 网格右键菜单:对勾从左侧移到右侧,并修正菜单内各元素边距 - 文本列统一为 kPadH + kItemPad(5 + 8),三类条目(HEADER/NORMAL/EDIT)共用; 原先左侧预留 22px 标记列,导致"网格 N"、"Y 轴最大值"、"选区 Y 轴自适应"等 所有文本的左边距被整体推右(用户报告的"文本左边距过大") - 对勾改画在条目右侧的 kMarkerColumn(18) 列内,文本宽度相应预留,不会与文字重叠 - EDIT 行输入框改为右对齐到条目内容区右边界,宽度由 NaturalEditFieldWidth() 统一计算, 空间不足时为左侧说明文字保留自然宽度(避免被菜单宽度上限挤成省略号) - 分隔线、色块行同样改用 kItemPad 内缩;kRightPad 已删除 3) "选区 Y 轴自适应"增加与"Y 轴自适应"一致的勾选标记 - Lane 新增 viewAutoRange / viewAutoMin / viewAutoMax,记录应用该操作时的视图时间区间 - 新增 PlotView::LaneRangeIsAutoToView():只有未被任何手动幅值改动且时间选区未变化时才为 true - 所有手动改动路径(ApplyValueSelection/ScaleLaneRange/ShiftLaneRange/ApplyLaneValueText/ EndEdit/ResetLaneRange/MoveSignalToLane)都会清除该状态; 时间选区变化后 viewAutoMin/Max 与当前视图不符,勾选自动消失 - RefreshPlotMenuValues() 同步刷新该项状态 4) 修复大文件(VERY_BIG_DATA 1.77GB / 5 信号)下悬停与选区缩放的严重卡顿 - 根因:任何悬停态变化(左侧信号列表、网格内命中曲线提示)或选区拖拽都会触发整幅重绘, 其中波形绘制约 80ms/帧,因此交互帧率只有 ~10fps - 静态场景缓存从"仅拖拽信号/光标"推广为"只要场景键一致就复用",缓存区域由整个客户区 收敛为仅绘图区:工具栏/标签栏/面板各自带悬停态必须每帧绘制(约 10.5ms), 而真正昂贵的波形改为直接贴图(约 1ms) - 场景键新增 contentHash(FNV-1a 折叠网格几何/幅值、每条信号的引用/可见性/ 颜色/透明度/线宽/样式/显示名与所属文件名):改样式、重命名、增删信号都会自动失效 - 随鼠标变化的内容全部移出缓存:新增 PaintHoverHighlight() 在贴图之后绘制 悬停网格底色(半透明叠加)与悬停信号光晕(PaintSignalHalo()); PaintLane 内只绘制拖拽中的信号光晕(key 中记录 haloFile/haloSignal) - EnsureSceneCache 改为要求位图尺寸与当前区域严格相等(原 >= 复用会在窗口缩小后 把旧像素画到绘图区之外) - Application::Render 移除 reuseScene 分支,统一每帧绘制界面框架 实测(VERY_BIG_DATA 5 信号,Release): - 整幅重绘 ~80ms -> 命中缓存贴图 ~1ms - 悬停左侧信号列表:~95ms -> ~14ms/帧 - 悬停网格内信号(含光晕高亮):~95ms -> ~28~32ms/帧 - 时间轴 / Y 轴选区拖拽:~95~105ms -> ~19~22ms/帧 - 非拖拽静止帧 ~118ms 维持不变;空闲 CPU 0.00% 构建: make clean; make all 无警告无错误
This commit is contained in:
1 parent
a3e638fa58
commit
dd48cdd2e1
9 files changed
+389
-169
No files matched your search
@@ -689,20 +689,15 @@ namespace jdi
|
||||
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
|
||||
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
|
||||
|
||||
// 拖拽(拖动信号曲线 / 时间光标)期间视图与数据都不变:
|
||||
// 静态场景缓存已包含工具栏/标签栏/面板/全部网格,直接跳过这些区域的重复绘制,
|
||||
// 实测可省下约 13ms/帧(面板绘制就占 8ms)。
|
||||
const bool reuseScene = m_plot.CanReuseSceneCache();
|
||||
|
||||
if (!reuseScene)
|
||||
{
|
||||
// 工具栏/标签栏/面板各自带悬停态,必须每帧绘制;
|
||||
// 真正昂贵的是绘图区的波形(大文件约 80ms/帧),
|
||||
// 由 PlotView 内部的静态场景缓存消除,与数据量无关。
|
||||
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
|
||||
m_theme.windowBg);
|
||||
|
||||
m_tabBar.Paint(g);
|
||||
m_toolbar.Paint(g);
|
||||
m_panel.Paint(g);
|
||||
}
|
||||
|
||||
m_plot.Paint(g);
|
||||
|
||||
@@ -1033,8 +1028,8 @@ namespace jdi
|
||||
if (m_plot.ToggleZoomMode(PlotZoomMode::TIME))
|
||||
{
|
||||
SetStatusMessage((m_plot.CurrentZoomMode() == PlotZoomMode::TIME)
|
||||
? L"时间轴缩放:在绘图区横向拖拽选择区间即可放大。"
|
||||
: L"已退出时间轴缩放模式。",
|
||||
? L"时间轴选区:在绘图区横向拖拽选择区间即可放大。"
|
||||
: L"已退出时间轴选区模式。",
|
||||
false);
|
||||
}
|
||||
break;
|
||||
@@ -1042,8 +1037,8 @@ namespace jdi
|
||||
if (m_plot.ToggleZoomMode(PlotZoomMode::VALUE))
|
||||
{
|
||||
SetStatusMessage((m_plot.CurrentZoomMode() == PlotZoomMode::VALUE)
|
||||
? L"Y 轴缩放:在某个网格内纵向拖拽选择区间即可放大。"
|
||||
: L"已退出 Y 轴缩放模式。",
|
||||
? L"Y 轴选区:在某个网格内纵向拖拽选择区间即可放大。"
|
||||
: L"已退出 Y 轴选区模式。",
|
||||
false);
|
||||
}
|
||||
break;
|
||||
@@ -1126,7 +1121,7 @@ namespace jdi
|
||||
// Esc 的语义:
|
||||
// 1) 放弃当前正在进行的拖拽(典型场景:缩放的框选选区还没松开左键);
|
||||
// 取消后不应用选区、视图范围保持不变;
|
||||
// 2) 同时退出全部缩放模式,使工具栏「时间轴缩放」「Y 轴缩放」回到未选中态。
|
||||
// 2) 同时退出全部缩放模式,使工具栏「时间轴选区」「Y 轴选区」回到未选中态。
|
||||
// Esc 不与时间光标关联:关闭光标请用工具栏「光标」按钮,避免误关指针。
|
||||
bool changed = m_plot.CancelDrag();
|
||||
changed = m_plot.ExitZoomModes() || changed;
|
||||
@@ -1409,6 +1404,7 @@ namespace jdi
|
||||
MenuItem autoViewItem;
|
||||
autoViewItem.label = L"选区 Y 轴自适应";
|
||||
autoViewItem.id = PLOT_CMD_LANE_AUTO_VIEW;
|
||||
autoViewItem.checked = m_plot.LaneRangeIsAutoToView(laneIndex);
|
||||
items.push_back(autoViewItem);
|
||||
|
||||
MenuItem separator;
|
||||
@@ -1458,6 +1454,10 @@ namespace jdi
|
||||
const bool isAuto = m_plot.LaneRangeIsAuto(laneIndex);
|
||||
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO, isAuto ? L"Y 轴自适应(当前)" : L"恢复 Y 轴自适应",
|
||||
isAuto, !isAuto);
|
||||
|
||||
// “选区 Y 轴自适应”是带状态的操作,同样需要实时反映其“当前生效”标记。
|
||||
m_menu.UpdateItemLabel(PLOT_CMD_LANE_AUTO_VIEW, L"选区 Y 轴自适应",
|
||||
m_plot.LaneRangeIsAutoToView(laneIndex), true);
|
||||
}
|
||||
|
||||
void Application::DispatchMenuResult(const MenuResult& result)
|
||||
|
||||
@@ -253,17 +253,7 @@ namespace jdi
|
||||
bool CancelDrag();
|
||||
|
||||
/**
|
||||
* @brief 判断本帧是否可以整体复用拖拽静态场景缓存。
|
||||
*
|
||||
* 拖动信号曲线 / 时间光标时视图与数据都不变,缓存内容仍然有效。
|
||||
* 宿王可据此跳过工具栏/标签栏/面板的重复绘制(它们也在缓存内)。
|
||||
*
|
||||
* @return 可复用返回 true。
|
||||
*/
|
||||
bool CanReuseSceneCache() const;
|
||||
|
||||
/**
|
||||
* @brief 退出全部缩放模式(时间轴缩放 / Y 轴缩放),使工具栏缩放按钮回到未选中态。
|
||||
* @brief 退出全部缩放模式(时间轴选区 / Y 轴选区),使工具栏缩放按钮回到未选中态。
|
||||
* @return 之前处于某个缩放模式返回 true。
|
||||
*/
|
||||
bool ExitZoomModes();
|
||||
@@ -320,6 +310,17 @@ namespace jdi
|
||||
*/
|
||||
bool LaneRangeIsAuto(int laneIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 判断某网格当前范围是否正是「选区 Y 轴自适应」的结果(用于菜单勾选标记)。
|
||||
*
|
||||
* 只有在该操作之后未发生任何手动幅值改动、且时间选区没有变化时才返回 true,
|
||||
* 因为一旦时间选区改变,之前的选区自适应结果就不再对应当前视野。
|
||||
*
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @return 当前生效返回 true。
|
||||
*/
|
||||
bool LaneRangeIsAutoToView(int laneIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 让某网格的 Y 轴恢复自适应。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
@@ -844,14 +845,15 @@ namespace jdi
|
||||
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
|
||||
|
||||
/**
|
||||
* @brief 静态场景缓存的几何键。
|
||||
* @brief 静态场景缓存的几何/内容键。
|
||||
*
|
||||
* 只要这组参数完全一致,"背景 + 全部网格 + 时间轴"的渲染结果就逐像素相同,
|
||||
* 可以直接复用离屏位图,无需重算百万级采样的包络与绘制。
|
||||
* 这使鼠标悬停、拖拽选区等"只改一小块叠加层"的交互不再重绘波形:
|
||||
* 大文件下整幅重绘约 80ms,命中缓存时只需 1~2ms 贴图。
|
||||
*/
|
||||
struct SceneCacheKey
|
||||
{
|
||||
int dragMode = -1; /**< 拖拽模式(拖拽中才允许复用)。 */
|
||||
double viewMin = 0.0; /**< 时间轴下界。 */
|
||||
double viewMax = 0.0; /**< 时间轴上界。 */
|
||||
float laneScroll = 0.0f; /**< 网格滚动偏移。 */
|
||||
@@ -861,6 +863,9 @@ namespace jdi
|
||||
float areaW = 0.0f; /**< 绘图区尺寸。 */
|
||||
float areaH = 0.0f;
|
||||
float scale = 1.0f; /**< DPI 缩放。 */
|
||||
int haloFile = -1; /**< 已绘入缓存的拖拽光晕信号(无则 -1)。 */
|
||||
int haloSignal = -1;
|
||||
std::uint64_t contentHash = 0; /**< 信号集合/显示样式/网格幅值等内容指纹。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -869,6 +874,49 @@ namespace jdi
|
||||
*/
|
||||
SceneCacheKey MakeSceneCacheKey() const;
|
||||
|
||||
/**
|
||||
* @brief 判断当前缓存键是否与已捕获的场景一致。
|
||||
* @return 可直接贴图返回 true。
|
||||
*/
|
||||
bool SceneCacheMatches() const;
|
||||
|
||||
/**
|
||||
* @brief 计算场景内容指纹(信号集合、显示样式、网格幅值、图例文案)。
|
||||
*
|
||||
* 视图几何以外的任何内容变化(改颜色/线宽/透明度/样式、重命名、增删信号)
|
||||
* 都会改变该指纹,从而让静态场景缓存自动失效,无需在各处手工调用失效接口。
|
||||
*
|
||||
* @return 指纹值。
|
||||
*/
|
||||
std::uint64_t SceneContentHash() const;
|
||||
|
||||
/**
|
||||
* @brief 当前应当绘入静态场景缓存的信号光晕(仅拖拽中的信号)。
|
||||
*
|
||||
* 拖拽信号的光晕在整个拖拽过程中不变,可以进入缓存;
|
||||
* 悬停光晕随鼠标移动,必须画在缓存之外。
|
||||
*
|
||||
* @return 信号引用,无则返回无效引用。
|
||||
*/
|
||||
SignalRef SceneHaloRef() const;
|
||||
|
||||
/**
|
||||
* @brief 绘制静态场景缓存之外的"悬停高亮"(悬停网格底色 + 悬停信号光晕)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintHoverHighlight(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 绘制某条信号的悬停/拖拽光晕。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] ref 信号引用。
|
||||
* @param [IN] resolved 已解析的信号。
|
||||
* @param [IN] points 折线顶点。
|
||||
* @param [IN] breaks 每段折线的起始下标。
|
||||
*/
|
||||
void PaintSignalHalo(gd::Graphics& g, const SignalRef& ref, const ResolvedSignal& resolved,
|
||||
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks);
|
||||
|
||||
/**
|
||||
* @brief 确保离屏缓存位图存在且尺寸匹配。
|
||||
* @param [IN] width 位图宽度(像素)。
|
||||
@@ -878,7 +926,11 @@ namespace jdi
|
||||
bool EnsureSceneCache(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief 把当前画布内容(背景 + 界面外框 + 全部网格 + 时间轴)拷贝进离屏缓存。
|
||||
* @brief 把当前画布内容(绘图区底色 + 全部网格 + 时间轴)拷贝进离屏缓存。
|
||||
*
|
||||
* 缓存区域严格限定为绘图区(m_area)。悬停与拖拽产生的内容一律不进入缓存,
|
||||
* 因此在任何交互过程中都可以安全复用,只贴图而不再重绘波形。
|
||||
*
|
||||
* @param [IN,OUT] g 当前绘图上下文(必须未设置裁剪区)。
|
||||
* @return 拷贝成功返回 true。
|
||||
*/
|
||||
@@ -906,8 +958,8 @@ namespace jdi
|
||||
HGDIOBJ m_sceneOldBitmap = nullptr; /**< 离屏 DC 原选位图。 */
|
||||
int m_sceneWidth = 0; /**< 离屏位图宽度。 */
|
||||
int m_sceneHeight = 0; /**< 离屏位图高度。 */
|
||||
int m_sceneX = 0; /**< 离屏缓存对应的客户区横坐标。 */
|
||||
int m_sceneY = 0; /**< 离屏缓存对应的客户区纵坐标。 */
|
||||
int m_sceneX = 0; /**< 缓存对应的客户区横坐标(绘图区左边界)。 */
|
||||
int m_sceneY = 0; /**< 缓存对应的客户区纵坐标(绘图区上边界)。 */
|
||||
bool m_sceneValid = false; /**< 缓存内容是否有效。 */
|
||||
SceneCacheKey m_sceneKey; /**< 缓存对应的场景参数。 */
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
#include "Utf8Util.hpp"
|
||||
@@ -501,6 +502,7 @@ namespace jdi
|
||||
for (Lane& lane : m_tab->Lanes())
|
||||
{
|
||||
lane.autoRange = true;
|
||||
lane.viewAutoRange = false;
|
||||
}
|
||||
}
|
||||
BuildLaneVisuals();
|
||||
@@ -820,6 +822,7 @@ namespace jdi
|
||||
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.autoRange = false;
|
||||
lane.viewAutoRange = false;
|
||||
lane.yMin = std::min(valueTop, valueBottom);
|
||||
lane.yMax = std::max(valueTop, valueBottom);
|
||||
return true;
|
||||
@@ -843,6 +846,22 @@ namespace jdi
|
||||
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].autoRange;
|
||||
}
|
||||
|
||||
bool PlotView::LaneRangeIsAutoToView(int laneIndex) const
|
||||
{
|
||||
// “选区 Y 轴自适应”是带状态的操作:只有应用后未被任何手动改动、
|
||||
// 且时间选区没有变化时,当前范围才仍等于该操作的结果。
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (!lane.viewAutoRange || lane.autoRange)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return (lane.viewAutoMin == Tab().ViewMin()) && (lane.viewAutoMax == Tab().ViewMax());
|
||||
}
|
||||
|
||||
bool PlotView::ResetLaneRange(int laneIndex)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
@@ -856,6 +875,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
lane.autoRange = true;
|
||||
lane.viewAutoRange = false;
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
@@ -959,6 +979,10 @@ namespace jdi
|
||||
lane.autoRange = false;
|
||||
lane.yMin = low;
|
||||
lane.yMax = high;
|
||||
// 记录应用时的视图时间区间:时间选区一变,本项就不再是“当前生效”状态。
|
||||
lane.viewAutoRange = true;
|
||||
lane.viewAutoMin = viewMin;
|
||||
lane.viewAutoMax = viewMax;
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
@@ -988,6 +1012,7 @@ namespace jdi
|
||||
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.autoRange = false;
|
||||
lane.viewAutoRange = false;
|
||||
lane.yMin = anchorValue - ratio * newSpan;
|
||||
lane.yMax = lane.yMin + newSpan;
|
||||
BuildLaneVisuals();
|
||||
@@ -1004,6 +1029,7 @@ namespace jdi
|
||||
// 首次纵向拖动时先把当前自适应范围固化下来,再进行偏移。
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.viewAutoRange = false;
|
||||
if (lane.autoRange)
|
||||
{
|
||||
lane.autoRange = false;
|
||||
@@ -1071,6 +1097,7 @@ namespace jdi
|
||||
// 手工输入即视为取消自动自适应,与拖拽框选缩放的语义一致。
|
||||
// 必须先把当前(自适应计算出的)显示范围固化到 lane 里,再只改动被编辑的一端,
|
||||
// 否则另一端会回退到 Lane 的初始占位值,导致量程被莫名其妙地放大。
|
||||
lane.viewAutoRange = false;
|
||||
if (lane.autoRange)
|
||||
{
|
||||
lane.autoRange = false;
|
||||
@@ -1410,6 +1437,7 @@ namespace jdi
|
||||
}
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.viewAutoRange = false;
|
||||
if (lane.autoRange)
|
||||
{
|
||||
lane.autoRange = false;
|
||||
@@ -2359,23 +2387,14 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 拖拽期间的静态场景复用:拖动信号曲线 / 时间光标时视图与数据都没有变化,
|
||||
// 每帧重绘百万级采样的包络纯属浪费(实测 5 条密集信号约 100ms/帧)。
|
||||
// 这里命中缓存时只贴图 + 重绘随鼠标变化的部分,使拖拽帧率不随数据量下降。
|
||||
const bool sceneCacheable =
|
||||
(m_dragMode == PlotDragMode::MOVE_SIGNAL || m_dragMode == PlotDragMode::CURSOR_MOVE);
|
||||
|
||||
if (sceneCacheable && m_sceneValid)
|
||||
{
|
||||
const SceneCacheKey& cached = m_sceneKey;
|
||||
const SceneCacheKey key = MakeSceneCacheKey();
|
||||
const bool same = (key.dragMode == cached.dragMode) && (key.viewMin == cached.viewMin) &&
|
||||
(key.viewMax == cached.viewMax) && (key.laneScroll == cached.laneScroll) &&
|
||||
(key.laneCount == cached.laneCount) && (key.areaX == cached.areaX) &&
|
||||
(key.areaY == cached.areaY) && (key.areaW == cached.areaW) &&
|
||||
(key.areaH == cached.areaH) && (key.scale == cached.scale);
|
||||
if (same && BlitSceneCache(g))
|
||||
// 静态场景复用(不再局限于拖拽):只要视图几何、信号集合与显示样式都没变,
|
||||
// "绘图区底色 + 全部网格 + 时间轴"就逐像素相同,直接贴图即可。
|
||||
// 大文件下整幅重绘约 80ms,贴图约 1ms —— 鼠标悬停、拖拽选区这些
|
||||
// "只改一小块叠加层"的交互因此不再重绘波形。
|
||||
// 随鼠标变化的内容(悬停高亮、读数框、选区、光标、拖拽虚影、输入框)一律画在贴图之后。
|
||||
if (SceneCacheMatches() && BlitSceneCache(g))
|
||||
{
|
||||
PaintHoverHighlight(g);
|
||||
PaintLaneReadouts(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
@@ -2389,11 +2408,9 @@ namespace jdi
|
||||
|
||||
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
|
||||
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width,
|
||||
m_plotRect.Height);
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
@@ -2402,7 +2419,7 @@ namespace jdi
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
|
||||
|
||||
// 绘图区整体底色(缓存路径下该内容已在离屏位图中)。
|
||||
// 绘图区整体底色。
|
||||
UiFillRect(g, m_area, m_theme->plotBg);
|
||||
|
||||
// 波形区域底色。
|
||||
@@ -2420,15 +2437,9 @@ namespace jdi
|
||||
|
||||
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
|
||||
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
|
||||
if (sceneCacheable)
|
||||
{
|
||||
CaptureSceneCache(g);
|
||||
}
|
||||
else
|
||||
{
|
||||
InvalidateSceneCache();
|
||||
}
|
||||
|
||||
PaintHoverHighlight(g);
|
||||
PaintLaneReadouts(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
@@ -2447,10 +2458,87 @@ namespace jdi
|
||||
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHalo(gd::Graphics& g, const SignalRef& ref, const ResolvedSignal& resolved,
|
||||
const std::vector<gd::PointF>& points,
|
||||
const std::vector<std::size_t>& breaks)
|
||||
{
|
||||
const bool drawLine = (resolved.style->mode != DrawMode::SCATTER);
|
||||
if (!drawLine || breaks.empty())
|
||||
{
|
||||
// 散点模式没有光晕(原实现同样只对折线绘制光晕)。
|
||||
(void)g;
|
||||
(void)ref;
|
||||
return;
|
||||
}
|
||||
|
||||
gd::Pen halo(ToGdiColor(WithAlpha(m_theme->accent, 0x66)),
|
||||
(resolved.style->lineWidth + 4.0f) * m_scale);
|
||||
halo.SetLineJoin(gd::LineJoinRound);
|
||||
halo.SetStartCap(gd::LineCapRound);
|
||||
halo.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawLines(&halo, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::PaintHoverHighlight(gd::Graphics& g)
|
||||
{
|
||||
if (m_tab == nullptr || m_dragMode != PlotDragMode::NONE)
|
||||
{
|
||||
// 拖拽过程中不显示悬停提示(拖拽中的信号光晕已绘入静态场景缓存)。
|
||||
return;
|
||||
}
|
||||
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(m_area, gd::CombineModeIntersect);
|
||||
|
||||
// 悬停网格:一层很淡的强调色叠加。为了能画在静态场景之上,
|
||||
// 这里用半透明而非不透明混色,观感与原实现基本一致。
|
||||
if (m_hoverLane >= 0 && m_hoverLane < static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
const gd::RectF& laneRect = m_lanes[static_cast<std::size_t>(m_hoverLane)].rect;
|
||||
const float clipTop = std::max(laneRect.Y, m_plotRect.Y);
|
||||
const float clipBottom = std::min(laneRect.Y + laneRect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
if (clipBottom > clipTop)
|
||||
{
|
||||
// alpha 0x0A ≈ 4%,与原「不透明混色 3.5%」的观感基本一致。
|
||||
UiFillRect(g, gd::RectF(laneRect.X, clipTop, laneRect.Width, clipBottom - clipTop),
|
||||
WithAlpha(m_theme->accent, 0x0A));
|
||||
}
|
||||
}
|
||||
|
||||
// 悬停信号:外扩一圈半透明强调色作为"可拖拽"提示。
|
||||
if (m_hoverSignal.IsValid())
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(m_hoverSignal);
|
||||
if (resolved.data != nullptr && resolved.style != nullptr && resolved.visible)
|
||||
{
|
||||
const int laneIndex = m_tab->LaneOfSignal(m_hoverSignal);
|
||||
if (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(m_hoverSignal, resolved, lane);
|
||||
if (cached != nullptr)
|
||||
{
|
||||
PaintSignalHalo(g, m_hoverSignal, resolved, cached->points, cached->breaks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
PlotView::SceneCacheKey PlotView::MakeSceneCacheKey() const
|
||||
{
|
||||
SceneCacheKey key;
|
||||
key.dragMode = static_cast<int>(m_dragMode);
|
||||
key.viewMin = Tab().ViewMin();
|
||||
key.viewMax = Tab().ViewMax();
|
||||
key.laneScroll = Tab().LaneScroll();
|
||||
@@ -2460,16 +2548,118 @@ namespace jdi
|
||||
key.areaW = m_area.Width;
|
||||
key.areaH = m_area.Height;
|
||||
key.scale = m_scale;
|
||||
|
||||
const SignalRef halo = SceneHaloRef();
|
||||
key.haloFile = halo.fileIndex;
|
||||
key.haloSignal = halo.signalIndex;
|
||||
key.contentHash = SceneContentHash();
|
||||
return key;
|
||||
}
|
||||
|
||||
bool PlotView::SceneCacheMatches() const
|
||||
{
|
||||
if (!m_sceneValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SceneCacheKey& cached = m_sceneKey;
|
||||
const SceneCacheKey key = MakeSceneCacheKey();
|
||||
return (key.viewMin == cached.viewMin) && (key.viewMax == cached.viewMax) &&
|
||||
(key.laneScroll == cached.laneScroll) && (key.laneCount == cached.laneCount) &&
|
||||
(key.areaX == cached.areaX) && (key.areaY == cached.areaY) &&
|
||||
(key.areaW == cached.areaW) && (key.areaH == cached.areaH) &&
|
||||
(key.scale == cached.scale) && (key.haloFile == cached.haloFile) &&
|
||||
(key.haloSignal == cached.haloSignal) && (key.contentHash == cached.contentHash);
|
||||
}
|
||||
|
||||
SignalRef PlotView::SceneHaloRef() const
|
||||
{
|
||||
// 只有拖拽中的信号光晕是“整个拖拽过程都不变”的,可以安全地绘入静态场景缓存;
|
||||
// 悬停光晕随鼠标移动,必须画在缓存之外(见 PaintHoverHighlight)。
|
||||
if (m_dragMode == PlotDragMode::MOVE_SIGNAL && m_dragSignal.IsValid())
|
||||
{
|
||||
return m_dragSignal;
|
||||
}
|
||||
return SignalRef();
|
||||
}
|
||||
|
||||
std::uint64_t PlotView::SceneContentHash() const
|
||||
{
|
||||
// FNV-1a:把“视图几何之外的一切影响绘图区像素的内容”折叠成一个指纹。
|
||||
// 这样颜色/线宽/透明度/样式/重命名/信号增删等变化都会自动让缓存失效,
|
||||
// 不需要在各处手工调用 InvalidateSceneCache(容易漏)。
|
||||
std::uint64_t hash = 1469598103934665603ull;
|
||||
const auto mix = [&hash](std::uint64_t value)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= 1099511628211ull;
|
||||
};
|
||||
const auto mixFloat = [&mix](float value)
|
||||
{
|
||||
std::uint32_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
mix(bits);
|
||||
};
|
||||
const auto mixText = [&mix](const std::wstring& text)
|
||||
{
|
||||
mix(text.size());
|
||||
for (const wchar_t character : text)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(character));
|
||||
}
|
||||
};
|
||||
|
||||
mixFloat(m_plotRect.X);
|
||||
mixFloat(m_plotRect.Y);
|
||||
mixFloat(m_plotRect.Width);
|
||||
mixFloat(m_plotRect.Height);
|
||||
mixFloat(m_laneContentHeight);
|
||||
mix(m_scrollBarVisible ? 1ull : 0ull);
|
||||
|
||||
for (std::size_t laneIndex = 0; laneIndex < m_lanes.size(); ++laneIndex)
|
||||
{
|
||||
const LaneVisual& visual = m_lanes[laneIndex];
|
||||
mixFloat(visual.rect.Y);
|
||||
mixFloat(visual.rect.Height);
|
||||
mixFloat(static_cast<float>(visual.yMin));
|
||||
mixFloat(static_cast<float>(visual.yMax));
|
||||
|
||||
const std::vector<SignalRef>& signals = Tab().Lanes()[laneIndex].signals;
|
||||
mix(signals.size());
|
||||
for (const SignalRef& ref : signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
mix(static_cast<std::uint64_t>(ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||||
mix(resolved.visible ? 1ull : 0ull);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
mix(static_cast<std::uint64_t>(resolved.style->color));
|
||||
mixFloat(resolved.style->opacity);
|
||||
mixFloat(resolved.style->lineWidth);
|
||||
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
||||
// 图例文案参与绘制,重命名必须让缓存失效。
|
||||
mixText(resolved.data->Title());
|
||||
if (resolved.file != nullptr)
|
||||
{
|
||||
mixText(resolved.file->Title());
|
||||
}
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool PlotView::EnsureSceneCache(int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_sceneDc != nullptr && m_sceneWidth >= width && m_sceneHeight >= height)
|
||||
// 位图尺寸必须与当前缓存区域**严格一致**:贴图时按位图尺寸整块拷贝,
|
||||
// 若沿用更大的旧位图,缩小窗口后会把旧像素画到绘图区之外。
|
||||
if (m_sceneDc != nullptr && m_sceneWidth == width && m_sceneHeight == height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -2498,36 +2688,24 @@ namespace jdi
|
||||
|
||||
bool PlotView::CaptureSceneCache(gd::Graphics& g)
|
||||
{
|
||||
// 缓存覆盖**整个客户区**:拖拽期间工具栏/标签栏/面板也同样不变,
|
||||
// 宿主(Application)据此跳过这些区域的重复绘制(实测面板绘制就占 8ms/帧)。
|
||||
int clientWidth = 0;
|
||||
int clientHeight = 0;
|
||||
if (m_host != nullptr)
|
||||
// 缓存区域严格限定为绘图区本身(m_area):工具栏/标签栏/面板由宿主每帧绘制,
|
||||
// 因为它们各自带有悬停态;绘图区的波形才是真正昂贵的部分(大文件约 80ms/帧)。
|
||||
const int x = static_cast<int>(std::floor(m_area.X));
|
||||
const int y = static_cast<int>(std::floor(m_area.Y));
|
||||
const int right = static_cast<int>(std::ceil(m_area.X + m_area.Width));
|
||||
const int bottom = static_cast<int>(std::ceil(m_area.Y + m_area.Height));
|
||||
const int width = right - x;
|
||||
const int height = bottom - y;
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
RECT client = {};
|
||||
GetClientRect(m_host, &client);
|
||||
clientWidth = client.right - client.left;
|
||||
clientHeight = client.bottom - client.top;
|
||||
}
|
||||
if (clientWidth <= 0 || clientHeight <= 0)
|
||||
{
|
||||
// 无宿主句柄时退化为只缓存绘图区。
|
||||
const int x = static_cast<int>(std::floor(m_area.X)) - 2;
|
||||
const int y = static_cast<int>(std::floor(m_area.Y)) - 2;
|
||||
const int right = static_cast<int>(std::ceil(m_area.X + m_area.Width)) + 2;
|
||||
const int bottom = static_cast<int>(std::ceil(m_area.Y + m_area.Height)) + 2;
|
||||
m_sceneX = x;
|
||||
m_sceneY = y;
|
||||
clientWidth = right - x;
|
||||
clientHeight = bottom - y;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_sceneX = 0;
|
||||
m_sceneY = 0;
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureSceneCache(clientWidth, clientHeight))
|
||||
m_sceneX = x;
|
||||
m_sceneY = y;
|
||||
|
||||
if (!EnsureSceneCache(width, height))
|
||||
{
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
@@ -2539,8 +2717,7 @@ namespace jdi
|
||||
m_sceneValid = false;
|
||||
return false;
|
||||
}
|
||||
const BOOL ok = BitBlt(m_sceneDc, 0, 0, clientWidth, clientHeight, source, m_sceneX, m_sceneY,
|
||||
SRCCOPY);
|
||||
const BOOL ok = BitBlt(m_sceneDc, 0, 0, width, height, source, m_sceneX, m_sceneY, SRCCOPY);
|
||||
g.ReleaseHDC(source);
|
||||
|
||||
if (ok == FALSE)
|
||||
@@ -2554,26 +2731,6 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::CanReuseSceneCache() const
|
||||
{
|
||||
if (!m_sceneValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_dragMode != PlotDragMode::MOVE_SIGNAL && m_dragMode != PlotDragMode::CURSOR_MOVE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const SceneCacheKey& cached = m_sceneKey;
|
||||
const SceneCacheKey key = MakeSceneCacheKey();
|
||||
return (key.dragMode == cached.dragMode) && (key.viewMin == cached.viewMin) &&
|
||||
(key.viewMax == cached.viewMax) && (key.laneScroll == cached.laneScroll) &&
|
||||
(key.laneCount == cached.laneCount) && (key.areaX == cached.areaX) &&
|
||||
(key.areaY == cached.areaY) && (key.areaW == cached.areaW) &&
|
||||
(key.areaH == cached.areaH) && (key.scale == cached.scale);
|
||||
}
|
||||
|
||||
bool PlotView::BlitSceneCache(gd::Graphics& g)
|
||||
{
|
||||
if (!m_sceneValid || m_sceneDc == nullptr)
|
||||
@@ -2682,12 +2839,6 @@ namespace jdi
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
|
||||
|
||||
// 悬停高亮。
|
||||
if (laneIndex == m_hoverLane && m_dragMode == PlotDragMode::NONE)
|
||||
{
|
||||
UiFillRect(g, lane.rect, MixColor(m_theme->cardBg, m_theme->accent, 0.035));
|
||||
}
|
||||
|
||||
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
|
||||
// 刻度密度随网格高度自适应:所有网格高度相同 → 各网格的刻度数量一致,
|
||||
// 不会出现“有的网格只有 1 条标签、有的有 3 条”的不一致观感。
|
||||
@@ -2777,6 +2928,9 @@ namespace jdi
|
||||
const int waveSave = g.Save();
|
||||
g.SetClip(lane.rect, gd::CombineModeIntersect);
|
||||
|
||||
// 只有拖拽中的信号光晕可以进入静态场景缓存(悬停光晕见 PaintHoverHighlight)。
|
||||
const SignalRef haloRef = SceneHaloRef();
|
||||
|
||||
for (const SignalRef& ref : laneSignals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
@@ -2799,24 +2953,12 @@ namespace jdi
|
||||
const bool drawScatter = (mode == DrawMode::SCATTER || mode == DrawMode::LINE_SCATTER);
|
||||
|
||||
// 悬停命中的信号先画一层浅色光晕,作为“可拖拽”的视觉提示。
|
||||
const bool highlighted = (ref == m_hoverSignal) || (ref == m_dragSignal);
|
||||
if (highlighted && drawLine && !breaks.empty())
|
||||
// 这里只处理拖拽中的信号(其光晕在拖拽期间不变,可以进入静态场景缓存);
|
||||
// 悬停光晕随鼠标变化,改由 PaintHoverHighlight 在缓存贴图之后绘制。
|
||||
const bool highlighted = (ref == haloRef);
|
||||
if (highlighted)
|
||||
{
|
||||
gd::Pen halo(ToGdiColor(WithAlpha(m_theme->accent, 0x66)),
|
||||
(resolved.style->lineWidth + 4.0f) * m_scale);
|
||||
halo.SetLineJoin(gd::LineJoinRound);
|
||||
halo.SetStartCap(gd::LineCapRound);
|
||||
halo.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawLines(&halo, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
PaintSignalHalo(g, ref, resolved, points, breaks);
|
||||
}
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
|
||||
@@ -22,8 +22,8 @@ namespace jdi
|
||||
NONE = 0, /**< 未触发任何动作。 */
|
||||
OPEN_FILE, /**< 打开 CSV 文件。 */
|
||||
FIT_ALL, /**< 视图自适应到可见信号范围(同时退出缩放模式)。 */
|
||||
ZOOM_TIME, /**< 切换「在时间轴缩放」模式。 */
|
||||
ZOOM_VALUE, /**< 切换「在 Y 轴缩放」模式。 */
|
||||
ZOOM_TIME, /**< 切换「时间轴选区」模式(框选时间区间后放大)。 */
|
||||
ZOOM_VALUE, /**< 切换「Y 轴选区」模式(框选幅值区间后放大)。 */
|
||||
CURSOR, /**< 切换时间光标。 */
|
||||
MERGE_LANES, /**< 合并为单一网格。 */
|
||||
SPLIT_LANES, /**< 按信号拆分为多网格。 */
|
||||
|
||||
@@ -68,8 +68,8 @@ namespace jdi
|
||||
const Spec specs[] = {
|
||||
{ToolbarAction::OPEN_FILE, L"导入文件", true, false, false},
|
||||
{ToolbarAction::FIT_ALL, L"适应视图", false, true, false},
|
||||
{ToolbarAction::ZOOM_TIME, L"时间轴缩放", false, true, true},
|
||||
{ToolbarAction::ZOOM_VALUE, L"Y 轴缩放", false, true, true},
|
||||
{ToolbarAction::ZOOM_TIME, L"时间轴选区", false, true, true},
|
||||
{ToolbarAction::ZOOM_VALUE, L"Y 轴选区", false, true, true},
|
||||
{ToolbarAction::CURSOR, L"光标", false, true, true},
|
||||
{ToolbarAction::MERGE_LANES, L"合并网格", false, true, false},
|
||||
{ToolbarAction::SPLIT_LANES, L"拆分网格", false, true, false},
|
||||
|
||||
@@ -330,6 +330,9 @@ namespace jdi
|
||||
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
|
||||
double yMin = -1.0; /**< 手动 Y 轴下限。 */
|
||||
double yMax = 1.0; /**< 手动 Y 轴上限。 */
|
||||
bool viewAutoRange = false; /**< 是否由「选区 Y 轴自适应」产生且时间选区未再变化。 */
|
||||
double viewAutoMin = 0.0; /**< 应用「选区 Y 轴自适应」时的视图时间下限。 */
|
||||
double viewAutoMax = 0.0; /**< 应用「选区 Y 轴自适应」时的视图时间上限。 */
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -287,6 +287,13 @@ namespace jdi
|
||||
*/
|
||||
gd::RectF EditFieldRect(int index) const;
|
||||
|
||||
/**
|
||||
* @brief 计算 EDIT 项输入框的“自然宽度”(排版与绘制共用同一公式)。
|
||||
* @param [IN] item 菜单项。
|
||||
* @return 宽度(像素)。
|
||||
*/
|
||||
float NaturalEditFieldWidth(const MenuItem& item) const;
|
||||
|
||||
/**
|
||||
* @brief 进入指定项的内联编辑状态。
|
||||
* @param [IN] index 项目下标。
|
||||
|
||||
@@ -1048,6 +1048,8 @@ namespace jdi
|
||||
|
||||
for (Lane& lane : m_lanes)
|
||||
{
|
||||
// 网格的信号组成发生变化,“选区 Y 轴自适应”的结果不再对应当前视野。
|
||||
lane.viewAutoRange = false;
|
||||
std::vector<SignalRef>& refs = lane.signals;
|
||||
refs.erase(std::remove(refs.begin(), refs.end(), ref), refs.end());
|
||||
}
|
||||
|
||||
+49
-35
@@ -13,9 +13,10 @@ namespace jdi
|
||||
const float kSeparatorHeight = 9.0f; /**< 分隔线高度。 */
|
||||
const float kSwatchRowHeight = 32.0f; /**< 色块行高度。 */
|
||||
const float kPadV = 5.0f; /**< 菜单上下内边距。 */
|
||||
const float kPadH = 6.0f; /**< 菜单左右内边距。 */
|
||||
const float kMarkerColumn = 22.0f; /**< 选中标记列宽度。 */
|
||||
const float kRightPad = 14.0f; /**< 文本右侧留白。 */
|
||||
const float kPadH = 5.0f; /**< 菜单左右内边距。 */
|
||||
const float kItemPad = 8.0f; /**< 条目内文本的左右内缩(所有条目统一)。 */
|
||||
const float kMarkerColumn = 18.0f; /**< 条目**右侧**的选中标记列宽度(对勾画在这里)。 */
|
||||
const float kMarkerGap = 4.0f; /**< 文本与右侧标记列之间的间隔。 */
|
||||
const float kMinWidth = 148.0f; /**< 菜单最小宽度。 */
|
||||
const float kMaxWidth = 340.0f; /**< 菜单最大宽度。 */
|
||||
const float kCorner = 6.0f; /**< 菜单圆角半径。 */
|
||||
@@ -80,31 +81,32 @@ namespace jdi
|
||||
if (item.kind == MenuItemKind::SWATCHES)
|
||||
{
|
||||
const int count = std::max(0, std::min(item.swatchCount, 16));
|
||||
const float width = kPadH * 2.0f * m_scale + static_cast<float>(count) * kSwatchSize * m_scale +
|
||||
const float width = (kPadH + kItemPad) * 2.0f * m_scale +
|
||||
static_cast<float>(count) * kSwatchSize * m_scale +
|
||||
static_cast<float>(std::max(0, count - 1)) * kSwatchGap * m_scale;
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
}
|
||||
else if (item.kind == MenuItemKind::HEADER)
|
||||
{
|
||||
const float width = (kPadH + kMarkerColumn) * m_scale +
|
||||
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label) + kRightPad * m_scale;
|
||||
// 标题与普通项共用同一文字列与右侧标记列,保证整个菜单的左右边距一致。
|
||||
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
|
||||
kMarkerGap * m_scale +
|
||||
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label);
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
}
|
||||
else if (item.kind == MenuItemKind::NORMAL)
|
||||
{
|
||||
const float width = (kPadH + kMarkerColumn) * m_scale +
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kRightPad * m_scale;
|
||||
const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale +
|
||||
kMarkerGap * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, item.label);
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
}
|
||||
else if (item.kind == MenuItemKind::EDIT)
|
||||
{
|
||||
// 说明文字 + 输入框 + 右侧留白;输入框宽度按数值文本自适应(取一个上限)。
|
||||
const float fieldWidth =
|
||||
std::max(kEditFieldWidth * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
||||
const float width = (kPadH + kMarkerColumn) * m_scale +
|
||||
// 说明文字 + 输入框;输入框宽度按数值文本自适应(与 EditFieldRect 保持一致)。
|
||||
const float fieldWidth = NaturalEditFieldWidth(item);
|
||||
const float width = (kPadH + kItemPad) * 2.0f * m_scale +
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale +
|
||||
fieldWidth + kRightPad * m_scale;
|
||||
fieldWidth;
|
||||
contentWidth = std::max(contentWidth, width);
|
||||
}
|
||||
}
|
||||
@@ -144,7 +146,7 @@ namespace jdi
|
||||
std::vector<gd::RectF>& rects = m_swatchRects[index];
|
||||
rects.reserve(static_cast<std::size_t>(count));
|
||||
const float swatchY = cursorY + (itemHeight - kSwatchSize * m_scale) * 0.5f;
|
||||
const float startX = left + (kPadH + 4.0f) * m_scale;
|
||||
const float startX = left + (kPadH + kItemPad) * m_scale;
|
||||
for (int swatch = 0; swatch < count; ++swatch)
|
||||
{
|
||||
const float swatchX =
|
||||
@@ -157,27 +159,35 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
float PopupMenu::NaturalEditFieldWidth(const MenuItem& item) const
|
||||
{
|
||||
return std::max(kEditFieldWidth * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
|
||||
}
|
||||
|
||||
gd::RectF PopupMenu::EditFieldRect(int index) const
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_items.size()))
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
if (m_items[static_cast<std::size_t>(index)].kind != MenuItemKind::EDIT)
|
||||
const MenuItem& item = m_items[static_cast<std::size_t>(index)];
|
||||
if (item.kind != MenuItemKind::EDIT)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
// 输入框靠右对齐:宽度取固定下限与当前文本宽度中的较大者,但不得超过菜单可用宽度的一半。
|
||||
// 输入框靠右对齐到条目内容区右边界;宽度取自然宽度,但必须给说明文字留出自然宽度,
|
||||
// 避免菜单被最大宽度夹取后输入框把左侧标签挤成省略号。
|
||||
const gd::RectF& rect = m_itemRects[static_cast<std::size_t>(index)];
|
||||
const float maxWidth = std::max(40.0f * m_scale, rect.Width * 0.5f);
|
||||
float width = std::max(kEditFieldWidth * m_scale,
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, m_items[static_cast<std::size_t>(index)].value) +
|
||||
20.0f * m_scale);
|
||||
width = std::min(width, maxWidth);
|
||||
const float contentWidth = std::max(40.0f * m_scale, rect.Width - 2.0f * kItemPad * m_scale);
|
||||
const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale;
|
||||
float width = NaturalEditFieldWidth(item);
|
||||
width = std::min(width, std::max(40.0f * m_scale, contentWidth - labelWidth));
|
||||
width = std::min(width, contentWidth * 0.75f);
|
||||
|
||||
const float height = rect.Height - kEditFieldPadV * 2.0f * m_scale;
|
||||
return gd::RectF(rect.X + rect.Width - width - 4.0f * m_scale,
|
||||
return gd::RectF(rect.X + rect.Width - kItemPad * m_scale - width,
|
||||
rect.Y + (rect.Height - height) * 0.5f, width, height);
|
||||
}
|
||||
|
||||
@@ -745,15 +755,16 @@ namespace jdi
|
||||
if (item.kind == MenuItemKind::SEPARATOR)
|
||||
{
|
||||
const float lineY = rect.Y + rect.Height * 0.5f;
|
||||
UiDrawLine(g, rect.X + 4.0f * m_scale, lineY, rect.X + rect.Width - 4.0f * m_scale, lineY,
|
||||
m_theme->border, 1.0f);
|
||||
UiDrawLine(g, rect.X + kItemPad * m_scale, lineY,
|
||||
rect.X + rect.Width - kItemPad * m_scale, lineY, m_theme->border, 1.0f);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.kind == MenuItemKind::HEADER)
|
||||
{
|
||||
// 标题与普通项共用同一文字列,保证菜单内所有文本严格左对齐。
|
||||
UiText(g, item.label, m_fonts->uiSmallBold,
|
||||
gd::RectF(rect.X + kMarkerColumn * m_scale, rect.Y, rect.Width, rect.Height),
|
||||
gd::RectF(rect.X + kItemPad * m_scale, rect.Y, rect.Width, rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
continue;
|
||||
}
|
||||
@@ -771,8 +782,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
const std::uint32_t labelColor = item.enabled ? m_theme->text : m_theme->textMuted;
|
||||
// 说明文字与普通项使用同一文字列(kMarkerColumn),保证菜单内所有文本左对齐。
|
||||
const float labelLeft = rect.X + kMarkerColumn * m_scale;
|
||||
// 说明文字与普通项使用同一文字列(kItemPad),保证菜单内所有文本左对齐。
|
||||
const float labelLeft = rect.X + kItemPad * m_scale;
|
||||
const gd::RectF fieldRect = EditFieldRect(static_cast<int>(index));
|
||||
const float labelWidth = std::max(8.0f, fieldRect.X - labelLeft - kEditFieldGap * m_scale);
|
||||
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, labelWidth), m_fonts->uiSmall,
|
||||
@@ -841,18 +852,21 @@ namespace jdi
|
||||
|
||||
if (item.checked)
|
||||
{
|
||||
// 单选标记:对勾。
|
||||
const float cx = rect.X + kMarkerColumn * m_scale * 0.5f + 2.0f * m_scale;
|
||||
// 单选标记:对勾固定画在条目**右侧**的标记列内,左侧只留统一的小边距,
|
||||
// 这样菜单文本左边距不会因为“可能存在的对勾”而被整体推右。
|
||||
const float markerRight = rect.X + rect.Width - kItemPad * m_scale;
|
||||
const float cx = markerRight - kMarkerColumn * m_scale * 0.5f;
|
||||
const float cy = rect.Y + rect.Height * 0.5f;
|
||||
const std::uint32_t markerColor = hovered ? m_theme->textOnAccent : m_theme->accent;
|
||||
UiDrawLine(g, cx - 4.0f * m_scale, cy, cx - 1.0f * m_scale, cy + 3.0f * m_scale,
|
||||
hovered ? m_theme->textOnAccent : m_theme->accent, 1.8f * m_scale);
|
||||
markerColor, 1.8f * m_scale);
|
||||
UiDrawLine(g, cx - 1.0f * m_scale, cy + 3.0f * m_scale, cx + 4.5f * m_scale,
|
||||
cy - 4.0f * m_scale, hovered ? m_theme->textOnAccent : m_theme->accent,
|
||||
1.8f * m_scale);
|
||||
cy - 4.0f * m_scale, markerColor, 1.8f * m_scale);
|
||||
}
|
||||
|
||||
const float textLeft = rect.X + kMarkerColumn * m_scale;
|
||||
const float textWidth = rect.Width - kMarkerColumn * m_scale - 4.0f * m_scale;
|
||||
const float textLeft = rect.X + kItemPad * m_scale;
|
||||
const float textWidth =
|
||||
rect.Width - 2.0f * kItemPad * m_scale - kMarkerColumn * m_scale - kMarkerGap * m_scale;
|
||||
UiText(g, UiEllipsize(g, item.label, m_fonts->uiSmall, textWidth), m_fonts->uiSmall,
|
||||
gd::RectF(textLeft, rect.Y, std::max(8.0f, textWidth), rect.Height), textColor,
|
||||
TextAlign::LEFT);
|
||||
|
||||
Reference in new issue
Block a user