fix(plot): 默认线宽 1.0px、菜单文本左对齐、散点白边修复与大文件拖拽性能优化

1) 默认线宽改为 1.0px
- SignalStyle::lineWidth 初值 1.6f -> 1.0f (DataModel.hpp / DataModel.cpp)
- 新增 LaneScroll() const 重载,供 const 的 MakeSceneCacheKey() 读取

2) 网格右键菜单文本左对齐
- PopupMenu 的 HEADER / NORMAL / EDIT 三类条目统一使用 kMarkerColumn(22.0f)
  作为文本列,替换原先写死的 8.0f,使 "网格 N"、"Y 轴最大值"、
  "Y 轴最小值"、"选区 Y 轴自适应" 等所有条目左边缘严格对齐

3) 修复散点/点线模式下数据点重叠处的白色缝隙
- 根因: GDI+ GraphicsPath 默认 FillModeAlternate(奇偶填充),重叠圆的交叠区域
  被反向填充,视觉上表现为白色缺口/白边
- 主绘制与拖拽 ghost 的 GraphicsPath 均显式 SetFillMode(FillModeWinding)
- ghost 由逐点 FillEllipse 改为单个 GraphicsPath 一次 FillPath(批量绘制)

4) 大文件拖拽流畅性优化 (VERY_BIG_DATA.CSV 1.77GB / 5 信号)
- 新增拖拽静态场景缓存: 在 MOVE_SIGNAL / CURSOR_MOVE 期间把整个客户区
  (绘图区 + 标签页 + 工具栏 + 信号面板) 捕快到 m_sceneDc 位图,后续帧直接
  BitBlt,仅在 SceneCacheKey(dragMode/viewMin/viewMax/laneScroll/laneCount/
  区域/scale) 变化时重绘。Application::Render 在复用场景时跳过窗口背景与
  各子控件绘制,缓存位图由 ~PlotView() 释放
- 失效点: SetContext / SetScale / Layout / InvalidatePolylineCache
- 光标数值读数从 PaintLane 抽出为 PaintLaneReadouts(),在缓存 blit 之后绘制,
  避免读数被缓存导致拖动时显示旧值
- 拖拽 ghost 复用已有 PolylineCacheEntry,改用 TranslateTransform 偏移,
  不再重复构造折线顶点
- 实测: 拖拽帧耗时 134-146ms -> 22-25ms;散点模式拖拽约 24.6ms;
  非拖拽重绘维持约 118ms;空闲 CPU 0.00%

构建: make clean; make all 无警告无错误
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-07 10:22:57 +08:00
1 parent 04e98ee041
commit a3e638fa58
6 files changed
+407 -29

No files matched your search

+14 -5
View File
@@ -689,12 +689,21 @@ namespace jdi
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
m_theme.windowBg);
// 拖拽(拖动信号曲线 / 时间光标)期间视图与数据都不变:
// 静态场景缓存已包含工具栏/标签栏/面板/全部网格,直接跳过这些区域的重复绘制,
// 实测可省下约 13ms/帧(面板绘制就占 8ms)。
const bool reuseScene = m_plot.CanReuseSceneCache();
if (!reuseScene)
{
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_tabBar.Paint(g);
m_toolbar.Paint(g);
m_panel.Paint(g);
m_plot.Paint(g);
UiFillRect(g, m_splitterRect, m_theme.border);
+92
View File
@@ -66,6 +66,11 @@ namespace jdi
public:
PlotView();
/**
* @brief 析构:释放拖拽静态场景缓存的离屏位图。
*/
~PlotView();
/**
* @brief 设置宿主窗口句柄(用于剪贴板所有者)。
* @param [IN] host 宿主窗口句柄,允许为空。
@@ -247,6 +252,16 @@ namespace jdi
*/
bool CancelDrag();
/**
* @brief 判断本帧是否可以整体复用拖拽静态场景缓存。
*
* 拖动信号曲线 / 时间光标时视图与数据都不变,缓存内容仍然有效。
* 宿王可据此跳过工具栏/标签栏/面板的重复绘制(它们也在缓存内)。
*
* @return 可复用返回 true。
*/
bool CanReuseSceneCache() const;
/**
* @brief 退出全部缩放模式(时间轴缩放 / Y 轴缩放),使工具栏缩放按钮回到未选中态。
* @return 之前处于某个缩放模式返回 true。
@@ -722,6 +737,15 @@ namespace jdi
*/
void PaintEmptyState(gd::Graphics& g);
/**
* @brief 绘制全部网格的时间光标读数框。
*
* 读数随时间光标实时变化,因此不能进入拖拽静态场景缓存,必须每帧单独绘制。
*
* @param [IN,OUT] g 绘图上下文。
*/
void PaintLaneReadouts(gd::Graphics& g);
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
const DataSet* m_dataSet = nullptr; /**< 数据集合(只读访问)。 */
@@ -819,6 +843,74 @@ namespace jdi
mutable std::deque<PolylineCacheEntry> m_polylineCache; /**< 折线顶点缓存(按几何键复用)。 */
/**
* @brief 静态场景缓存的几何键。
*
* 只要这组参数完全一致,"背景 + 全部网格 + 时间轴"的渲染结果就逐像素相同,
* 可以直接复用离屏位图,无需重算百万级采样的包络与绘制。
*/
struct SceneCacheKey
{
int dragMode = -1; /**< 拖拽模式(拖拽中才允许复用)。 */
double viewMin = 0.0; /**< 时间轴下界。 */
double viewMax = 0.0; /**< 时间轴上界。 */
float laneScroll = 0.0f; /**< 网格滚动偏移。 */
int laneCount = 0; /**< 网格数量。 */
float areaX = 0.0f; /**< 绘图区左上角。 */
float areaY = 0.0f;
float areaW = 0.0f; /**< 绘图区尺寸。 */
float areaH = 0.0f;
float scale = 1.0f; /**< DPI 缩放。 */
};
/**
* @brief 构造当前场景的缓存键。
* @return 缓存键。
*/
SceneCacheKey MakeSceneCacheKey() const;
/**
* @brief 确保离屏缓存位图存在且尺寸匹配。
* @param [IN] width 位图宽度(像素)。
* @param [IN] height 位图高度(像素)。
* @return 可用返回 true。
*/
bool EnsureSceneCache(int width, int height);
/**
* @brief 把当前画布内容(背景 + 界面外框 + 全部网格 + 时间轴)拷贝进离屏缓存。
* @param [IN,OUT] g 当前绘图上下文(必须未设置裁剪区)。
* @return 拷贝成功返回 true。
*/
bool CaptureSceneCache(gd::Graphics& g);
/**
* @brief 把离屏缓存贴回画布。
* @param [IN,OUT] g 绘图上下文。
* @return 命中缓存并贴图成功返回 true。
*/
bool BlitSceneCache(gd::Graphics& g);
/**
* @brief 丢弃缓存标记(下次绘制会重新捕获)。
*/
void InvalidateSceneCache();
/**
* @brief 释放离屏缓存占用的 GDI 对象。
*/
void ReleaseSceneCache();
HBITMAP m_sceneBitmap = nullptr; /**< 离屏缓存位图。 */
HDC m_sceneDc = nullptr; /**< 离屏缓存设备上下文。 */
HGDIOBJ m_sceneOldBitmap = nullptr; /**< 离屏 DC 原选位图。 */
int m_sceneWidth = 0; /**< 离屏位图宽度。 */
int m_sceneHeight = 0; /**< 离屏位图高度。 */
int m_sceneX = 0; /**< 离屏缓存对应的客户区横坐标。 */
int m_sceneY = 0; /**< 离屏缓存对应的客户区纵坐标。 */
bool m_sceneValid = false; /**< 缓存内容是否有效。 */
SceneCacheKey m_sceneKey; /**< 缓存对应的场景参数。 */
/**
* @brief 查询坐标命中的图例条目。
* @param [IN] x 客户区横坐标。
+292 -18
View File
@@ -10,6 +10,7 @@ namespace jdi
{
namespace
{
const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */
@@ -95,6 +96,11 @@ namespace jdi
PlotView::PlotView() = default;
PlotView::~PlotView()
{
ReleaseSceneCache();
}
void PlotView::Setup(const UiFonts* fonts, const Theme* theme)
{
m_fonts = fonts;
@@ -110,12 +116,14 @@ namespace jdi
m_dragMode = PlotDragMode::NONE;
// 重新绑定数据源后旧的几何键不再有意义,必须清空折线缓存。
InvalidatePolylineCache();
InvalidateSceneCache();
RefreshLanes();
}
void PlotView::SetScale(float scale)
{
m_scale = (scale > 0.1f) ? scale : 1.0f;
InvalidateSceneCache();
}
bool PlotView::CursorEnabled() const
@@ -184,6 +192,8 @@ namespace jdi
void PlotView::Layout(const gd::RectF& area)
{
m_area = area;
// 布局变化(窗口缩放、面板宽度调整、网格数量变化)后静态场景不再有效。
InvalidateSceneCache();
m_labelBandWidth = 66.0f * m_scale;
m_timeAxisHeight = 34.0f * m_scale;
@@ -549,6 +559,7 @@ namespace jdi
void PlotView::InvalidatePolylineCache()
{
m_polylineCache.clear();
InvalidateSceneCache();
}
bool PlotView::FitTimeToLane(int laneIndex)
@@ -2339,16 +2350,51 @@ namespace jdi
return;
}
UiFillRect(g, m_area, m_theme->plotBg);
BuildLaneVisuals();
if (m_lanes.empty())
{
InvalidateSceneCache();
PaintEmptyState(g);
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))
{
PaintLaneReadouts(g);
PaintRubberBand(g);
PaintCursor(g, m_lanes);
PaintSignalInteraction(g);
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
m_editTarget == PlotEditTarget::LANE_Y_MIN)
{
PaintEditField(g);
}
PaintScrollBar(g);
// 绘图区外框(比网格内部分界线更重,形成明确整体边界)。
gd::Pen borderPen(ToGdiColor(m_theme->axisLine), 1.6f);
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();
const double span = viewMax - viewMin;
@@ -2356,6 +2402,9 @@ namespace jdi
std::vector<double> timeTicks;
BuildTicks(viewMin, viewMax, timeStep, timeTicks);
// 绘图区整体底色(缓存路径下该内容已在离屏位图中)。
UiFillRect(g, m_area, m_theme->plotBg);
// 波形区域底色。
UiFillRect(g, m_plotRect, m_theme->cardBg);
@@ -2368,6 +2417,19 @@ namespace jdi
}
PaintTimeAxis(g, timeTicks, timeStep);
// 此刻画布上正好是"绘图区底色 + 全部网格 + 时间轴",尚未绘制任何随鼠标变化的内容,
// 因此是捕获静态场景缓存的最佳时点(必须在裁剪区已全部恢复之后执行)。
if (sceneCacheable)
{
CaptureSceneCache(g);
}
else
{
InvalidateSceneCache();
}
PaintLaneReadouts(g);
PaintRubberBand(g);
PaintCursor(g, m_lanes);
PaintSignalInteraction(g);
@@ -2385,6 +2447,178 @@ namespace jdi
g.DrawRectangle(&borderPen, m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height);
}
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();
key.laneCount = static_cast<int>(m_lanes.size());
key.areaX = m_area.X;
key.areaY = m_area.Y;
key.areaW = m_area.Width;
key.areaH = m_area.Height;
key.scale = m_scale;
return key;
}
bool PlotView::EnsureSceneCache(int width, int height)
{
if (width <= 0 || height <= 0)
{
return false;
}
if (m_sceneDc != nullptr && m_sceneWidth >= width && m_sceneHeight >= height)
{
return true;
}
ReleaseSceneCache();
HDC screen = GetDC(nullptr);
if (screen == nullptr)
{
return false;
}
m_sceneDc = CreateCompatibleDC(screen);
m_sceneBitmap = CreateCompatibleBitmap(screen, width, height);
ReleaseDC(nullptr, screen);
if (m_sceneDc == nullptr || m_sceneBitmap == nullptr)
{
ReleaseSceneCache();
return false;
}
m_sceneOldBitmap = SelectObject(m_sceneDc, m_sceneBitmap);
m_sceneWidth = width;
m_sceneHeight = height;
return true;
}
bool PlotView::CaptureSceneCache(gd::Graphics& g)
{
// 缓存覆盖**整个客户区**:拖拽期间工具栏/标签栏/面板也同样不变,
// 宿主(Application)据此跳过这些区域的重复绘制(实测面板绘制就占 8ms/帧)。
int clientWidth = 0;
int clientHeight = 0;
if (m_host != nullptr)
{
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;
}
if (!EnsureSceneCache(clientWidth, clientHeight))
{
m_sceneValid = false;
return false;
}
HDC source = g.GetHDC();
if (source == nullptr)
{
m_sceneValid = false;
return false;
}
const BOOL ok = BitBlt(m_sceneDc, 0, 0, clientWidth, clientHeight, source, m_sceneX, m_sceneY,
SRCCOPY);
g.ReleaseHDC(source);
if (ok == FALSE)
{
m_sceneValid = false;
return false;
}
m_sceneKey = MakeSceneCacheKey();
m_sceneValid = true;
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)
{
return false;
}
HDC target = g.GetHDC();
if (target == nullptr)
{
return false;
}
const BOOL ok = BitBlt(target, m_sceneX, m_sceneY, m_sceneWidth, m_sceneHeight, m_sceneDc, 0, 0,
SRCCOPY);
g.ReleaseHDC(target);
return ok != FALSE;
}
void PlotView::InvalidateSceneCache()
{
m_sceneValid = false;
}
void PlotView::ReleaseSceneCache()
{
m_sceneValid = false;
if (m_sceneDc != nullptr)
{
if (m_sceneOldBitmap != nullptr)
{
SelectObject(m_sceneDc, m_sceneOldBitmap);
m_sceneOldBitmap = nullptr;
}
DeleteDC(m_sceneDc);
m_sceneDc = nullptr;
}
if (m_sceneBitmap != nullptr)
{
DeleteObject(m_sceneBitmap);
m_sceneBitmap = nullptr;
}
m_sceneWidth = 0;
m_sceneHeight = 0;
}
void PlotView::PaintRubberBand(gd::Graphics& g)
{
if (!m_dragMoved || m_rubberRect.Width <= 0.0f || m_rubberRect.Height <= 0.0f)
@@ -2616,9 +2850,11 @@ namespace jdi
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
// 所有散点合并到一条 GraphicsPath 后只调用一次 FillPath:
// 逐点 FillEllipse 每次都要完整走一遍 GDI+ 的状态检查与光栅化入口,
// 实测 2500 点需要约 51ms;合并后约 1/9(实测基准 63ms → 7ms),
// 且仍是抗锯齿圆形,观感与逐点绘制完全一致。
// 实测 2500 点需要约 51ms;合并后约 1/9(实测基准 63ms → 7ms)。
// **必须显式设为缠绕填充规则**:GraphicsPath 默认是交替(奇偶)规则,
// 两个重叠的圆在交集处会互相抵消形成空洞,表现为"数据点重叠处出现异常白边"。
gd::GraphicsPath scatterPath;
scatterPath.SetFillMode(gd::FillModeWinding);
for (std::size_t k = 0; k < points.size(); k += scatterStride)
{
scatterPath.AddEllipse(points[k].X - radius, points[k].Y - radius, radius * 2.0f,
@@ -2705,15 +2941,43 @@ namespace jdi
if (lane.rect.Height >= 24.0f * m_scale)
{
PaintLaneLegend(g, laneIndex, lane, clipTop, clipBottom);
if (Tab().CursorEnabled() && lane.rect.Height >= 40.0f * m_scale)
{
PaintLaneReadout(g, laneIndex, lane, clipTop, clipBottom);
}
}
g.Restore(saveState);
}
void PlotView::PaintLaneReadouts(gd::Graphics& g)
{
// 读数框随时间光标实时变化,因此**不能**进入拖拽静态场景缓存,
// 必须每帧单独绘制(否则拖动光标时读数会停在旧值上)。
if (m_tab == nullptr || !Tab().CursorEnabled())
{
return;
}
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
const LaneVisual& lane = m_lanes[index];
if (lane.rect.Height < 24.0f * m_scale || lane.rect.Height < 40.0f * m_scale)
{
continue;
}
const float clipTop = std::max(lane.rect.Y, m_plotRect.Y);
const float clipBottom =
std::min(lane.rect.Y + lane.rect.Height, m_plotRect.Y + m_plotRect.Height);
if (clipBottom <= clipTop)
{
continue;
}
const int saveState = g.Save();
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
PaintLaneReadout(g, static_cast<int>(index), lane, clipTop, clipBottom);
g.Restore(saveState);
}
}
SignalRef PlotView::LegendAtPoint(int x, int y) const
{
const float fx = static_cast<float>(x);
@@ -3176,15 +3440,18 @@ namespace jdi
g.SetClip(gd::RectF(m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height),
gd::CombineModeIntersect);
std::vector<gd::PointF> points;
std::vector<std::size_t> breaks;
BuildPolyline(resolved, sourceVisual, points, breaks);
for (gd::PointF& point : points)
// 虚影的几何与源网格里的信号完全一致,直接复用折线顶点缓存,
// 再用平移变换让整体跟随鼠标:避免每帧重新构建并逐点搬运上万顶点。
const PolylineCacheEntry* ghostSource = AcquirePolyline(m_dragSignal, resolved, sourceVisual);
if (ghostSource == nullptr)
{
point.X += offsetX;
point.Y += offsetY;
g.Restore(saveState);
return;
}
const std::vector<gd::PointF>& points = ghostSource->points;
const std::vector<std::size_t>& breaks = ghostSource->breaks;
g.TranslateTransform(offsetX, offsetY);
const DrawMode mode = resolved.style->mode;
const bool drawLine = (mode != DrawMode::SCATTER);
@@ -3223,11 +3490,18 @@ namespace jdi
m_scale);
const std::size_t scatterStride =
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
gd::SolidBrush brush(ToGdiColor(ghostColor));
// 与主绘制一致:合并成一条路径一次填充,并显式使用缠绕规则避免重叠处出现空洞。
gd::GraphicsPath ghostPath;
ghostPath.SetFillMode(gd::FillModeWinding);
for (std::size_t k = 0; k < points.size(); k += scatterStride)
{
g.FillEllipse(&brush, points[k].X - radius, points[k].Y - radius, radius * 2.0f,
radius * 2.0f);
ghostPath.AddEllipse(points[k].X - radius, points[k].Y - radius, radius * 2.0f,
radius * 2.0f);
}
gd::SolidBrush brush(ToGdiColor(ghostColor));
if (ghostPath.GetPointCount() > 0)
{
g.FillPath(&brush, &ghostPath);
}
}
+3 -1
View File
@@ -92,7 +92,7 @@ namespace jdi
std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */
std::uint32_t customColor = 0xFF6C71C4; /**< 自定义颜色记忆值(切换预设色后仍保留)。 */
float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */
float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
float lineWidth = 1.0f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
};
@@ -549,6 +549,8 @@ namespace jdi
bool& ViewInitialized() { return m_viewInitialized; }
/// @brief 网格纵向滚动偏移量(可读写)。
float& LaneScroll() { return m_laneScroll; }
/// @brief 网格纵向滚动偏移量(只读)。
float LaneScroll() const { return m_laneScroll; }
/// @brief 时间光标是否启用(可读写)。
bool& CursorEnabled() { return m_cursorEnabled; }
/// @brief 时间光标是否启用(只读)。
+1 -1
View File
@@ -432,7 +432,7 @@ namespace jdi
: 0xFF268BD2u;
signal.style.customColor = signal.style.color;
signal.style.opacity = 1.0f;
signal.style.lineWidth = 1.6f;
signal.style.lineWidth = 1.0f;
signal.style.mode = DrawMode::LINE;
}
+5 -4
View File
@@ -86,7 +86,7 @@ namespace jdi
}
else if (item.kind == MenuItemKind::HEADER)
{
const float width = (kPadH + 8.0f) * m_scale +
const float width = (kPadH + kMarkerColumn) * m_scale +
UiMeasureTextWidth(m_fonts->uiSmallBold, item.label) + kRightPad * m_scale;
contentWidth = std::max(contentWidth, width);
}
@@ -102,7 +102,7 @@ namespace jdi
const float fieldWidth =
std::max(kEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->uiSmall, item.value) + 20.0f * m_scale);
const float width = (kPadH + 8.0f) * m_scale +
const float width = (kPadH + kMarkerColumn) * m_scale +
UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale +
fieldWidth + kRightPad * m_scale;
contentWidth = std::max(contentWidth, width);
@@ -753,7 +753,7 @@ namespace jdi
if (item.kind == MenuItemKind::HEADER)
{
UiText(g, item.label, m_fonts->uiSmallBold,
gd::RectF(rect.X + 8.0f * m_scale, rect.Y, rect.Width, rect.Height),
gd::RectF(rect.X + kMarkerColumn * m_scale, rect.Y, rect.Width, rect.Height),
m_theme->textMuted, TextAlign::LEFT);
continue;
}
@@ -771,7 +771,8 @@ namespace jdi
}
const std::uint32_t labelColor = item.enabled ? m_theme->text : m_theme->textMuted;
const float labelLeft = rect.X + 8.0f * m_scale;
// 说明文字与普通项使用同一文字列(kMarkerColumn),保证菜单内所有文本左对齐。
const float labelLeft = rect.X + kMarkerColumn * 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,