一、命名(用户要求) - 实验性功能里的「阶数 N」全部改为「迭代次数 N」(面板滑块标签、均值滤波/中心轴 的数值框标签、构建提示语)。 - 相关标识符与函数参数统一改名:order → iterations(Lane::MeanFilterItem::iterations、 Lane::weakBoostIterations、ComputeWeakWaveBoost/ComputeMovingAverageIterated 的形参、 MeanFilterCurveFor/WeakBoostCurveFor/WeakBoostValueAt/EdgeCenterCountsFor、 各曲线缓存项的字段、EdgePanelLayout::rowIterations/sliderIterations、 PlotEditTarget::MEAN_FILTER_ITERATIONS/EDGE_CENTER_ITERATIONS、 PlotDragMode::BOOST_ITERATIONS、kWeakBoostIterationsMin/Max/Label、 DerivedSignalRequest::iterations 等)。网格拖拽重排里的局部 order 为避免歧义 改名为 laneOrder;`std::memory_order_*`、`rowOrder`→`rowIterations` 一并核对无误。 - 导出信号尾缀统一为「N次迭代」: `<原信号> (N点均值 M次迭代)`、`<原信号> (弱波增强 X% M次迭代)`; 弱波增强此前缺失该信息,现已补上(两种派生信号都恒定带迭代次数)。 状态提示语同步为「… M 次迭代信号。」。 二、越界三角与光标读数的样式统一(用户要求) - 新增 `kMarkerTriangleGap`:网格内数据点越界时,方向三角与数值标注之间**留出间隙** (此前三角与胶囊贴在一起,看起来像胶囊上多出的尖角);中心轴/均值滤波/弱波增强的 派生取值标注同样处理。 - 光标时间读数(以及双光标 Δt 读数)改用与网格内数据点**完全一致**的越界样式: 三角画在胶囊**外侧**、尖端朝外、与胶囊之间留出同样的间隙;为此把读数胶囊在越界一侧 向内让出「三角深度 + 间隙」。旧的"三角压在胶囊上沿"的画法以及为它额外留出的叠放 间距(kCursorTipStackGap)一并删除。 三、大文件放大后"平移 / 改配色非常卡"的根因与修复(用户要求,流畅性优先) 用离屏探针(`.preflight/perf3`)在 VERY_BIG_DATA(1.77GB、2580 万采样 × 5 信号、 1224×796 绘图区)上先量化,实测到的三个根因: 1. **包络带"梳状化"**:当信号的振荡周期与像素列宽度相当时(编码器方波在 2ms/大格下 约 3 个采样点一列),逐列包络会在"全高"与"极窄"之间交替——实测 1224 列里有 988 处 全高跳变。这种多边形让 GDI+ 的扫描线填充耗时暴涨:同一条 2448 顶点的路径 **27.6ms → 1.1ms(±1 列极值扩展后快 25 倍)**。这正是"某些放大等级下平移特别卡" (实测 2ms/大格平移 87ms,其余等级 17~20ms)的原因。 → `EmitEnvelopeBand` 在拼装包络带时对列窗口做 ±1 列的极值扩展(纵向最多各多 1 像素, 肉眼不可辨),一次性消除该病理。 2. **派生预览曲线按折线逐段描边**:均值滤波/弱波增强曲线在可见采样数超过像素列数时 仍按"每 2 像素一个顶点"的抗锯齿折线绘制,而这类曲线常叠在方波上(大量近垂直跳变), 实测单条 12~42ms,5 个网格合计上百毫秒。 → 稠密时改用**逐像素列的包络带**(复用 `EmitEnvelopeBand`)一次 FillPath,实测 0.56ms / 1.53ms(分别快 22 倍 / 28 倍),视觉与细折线一致(列宽就是 1 像素)。 3. **悬停光晕用宽笔描整条折线**:实测悬停一帧多花 35ms。 → 包络带模式下改为"把带整体外扩后填充一次"(`PaintBandHalo`),悬停帧恢复正常。 另外修掉两个相关的固定开销: - `UiMeasureTextWidth` 增加 (字体, 文本) 记忆化缓存:它是布局代码的热点(浮动面板几何 按「网格数 × 面板种类」重复计算,每帧重复测量同一批字符串成百上千次),实测单个面板 的绘制因此从 3.2ms 降到 1ms 量级。字体重建(DPI 变化)时由 `UiClearMeasureCache` 作废。 实测(VERY_BIG_DATA 全选 5 信号,绘图区全重绘 = 平移一帧): - 20ms/大格(用户报告的场景):平移 20.1ms → **11.3ms**; - 2ms/大格(此前最糟):平移 86.7ms → **12.3ms**; - 网格 1 开弱波增强:63.9ms → **17.6ms**;5 个网格全开:136.6ms → **31.9ms**; - 悬停信号:整帧 49.6ms → **15.5ms**(与不悬停持平,光晕几乎免费)。 仍存的已知热点:0.5ms/大格附近(可见约 1000 个采样点、走**逐点描边**路径)平移约 28~39ms —— 该区间采样比像素还稀疏、无法用包络带替代,且用户明确要求保留抗锯齿, 本次未动。 四、验证 - 离屏 harness(窗口 SW_HIDE、不占用鼠标/键盘/屏幕)**355 项检查全部通过**, 新增 `[AD]`:越界读数胶囊向内让位、三角在外侧、三角与胶囊间有真实间隙、 胶囊上方不再有内容;取值标注与三角之间保留 `kMarkerTriangleGap` 的间隙(用 散点模式排除波形干扰后逐像素断言)。`[X]/[Y]/[AA]/[AB]/[Z]` 等既有用例同步通过。 - 性能结论来自 `.preflight/perf3`(离屏探针 + 两套微基准),已随任务清理。
899 lines
28 KiB
C++
899 lines
28 KiB
C++
#include "PaintUtil.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
#include <functional>
|
||
#include <mutex>
|
||
#include <unordered_map>
|
||
#include <utility>
|
||
#include <string>
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace
|
||
{
|
||
/**
|
||
* @brief 颜色样块补充描边的不透明度阈值。
|
||
*
|
||
* 不透明度低于该值时,色块自身与浅色背景的对比度已不足,需要额外描边才能辨识;
|
||
* 高于该值时按需求不绘制描边,保持与预设色块一致的纯净外观。
|
||
*/
|
||
const float kSwatchOutlineOpacity = 0.45f;
|
||
|
||
/**
|
||
* @brief 矩形面积超过该阈值(逻辑像素²)且为不透明色时,改用 GDI 填充。
|
||
*
|
||
* GDI+ 的 FillRectangle 约 2.2ns/像素;面积过小时 GDI 的 GetHDC/ReleaseHDC
|
||
* 固定开销反而不划算,因此在阈值以下仍走 GDI+。
|
||
*/
|
||
const float kGdiFillAreaThreshold = 20000.0f;
|
||
|
||
/**
|
||
* @brief 构造圆角矩形路径。
|
||
* @param [IN,OUT] path 目标路径对象。
|
||
* @param [IN] rect 目标矩形。
|
||
* @param [IN] radius 圆角半径。
|
||
*/
|
||
void BuildRoundRectPath(gd::GraphicsPath& path, const gd::RectF& rect, float radius)
|
||
{
|
||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
float diameter = radius * 2.0f;
|
||
if (diameter > rect.Width)
|
||
{
|
||
diameter = rect.Width;
|
||
}
|
||
if (diameter > rect.Height)
|
||
{
|
||
diameter = rect.Height;
|
||
}
|
||
if (diameter < 1.0f)
|
||
{
|
||
path.AddRectangle(rect);
|
||
return;
|
||
}
|
||
|
||
const float right = rect.X + rect.Width;
|
||
const float bottom = rect.Y + rect.Height;
|
||
|
||
path.AddArc(rect.X, rect.Y, diameter, diameter, 180.0f, 90.0f);
|
||
path.AddArc(right - diameter, rect.Y, diameter, diameter, 270.0f, 90.0f);
|
||
path.AddArc(right - diameter, bottom - diameter, diameter, diameter, 0.0f, 90.0f);
|
||
path.AddArc(rect.X, bottom - diameter, diameter, diameter, 90.0f, 90.0f);
|
||
path.CloseFigure();
|
||
}
|
||
|
||
/**
|
||
* @brief 创建指定字号的界面字体。
|
||
* @param [IN] face 字体名称。
|
||
* @param [IN] pixelHeight 字符高度(像素)。
|
||
* @param [IN] bold 是否加粗。
|
||
* @return 字体句柄,失败返回 nullptr。
|
||
*/
|
||
HFONT CreateUiFont(const wchar_t* face, int pixelHeight, bool bold)
|
||
{
|
||
LOGFONTW logFont = {};
|
||
logFont.lfHeight = -pixelHeight;
|
||
logFont.lfWeight = bold ? FW_SEMIBOLD : FW_NORMAL;
|
||
logFont.lfCharSet = DEFAULT_CHARSET;
|
||
logFont.lfOutPrecision = OUT_TT_PRECIS;
|
||
logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS;
|
||
logFont.lfQuality = CLEARTYPE_QUALITY;
|
||
logFont.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
|
||
wcsncpy(logFont.lfFaceName, face, LF_FACESIZE - 1);
|
||
logFont.lfFaceName[LF_FACESIZE - 1] = L'\0';
|
||
|
||
HFONT font = CreateFontIndirectW(&logFont);
|
||
if (font == nullptr)
|
||
{
|
||
font = static_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
||
}
|
||
return font;
|
||
}
|
||
} // namespace
|
||
|
||
namespace
|
||
{
|
||
/**
|
||
* @brief 文本宽度测量的缓存键(字体句柄 + 文本)。
|
||
*/
|
||
struct MeasureKey
|
||
{
|
||
HFONT font = nullptr;
|
||
std::wstring text;
|
||
bool operator==(const MeasureKey& other) const
|
||
{
|
||
return font == other.font && text == other.text;
|
||
}
|
||
};
|
||
struct MeasureKeyHash
|
||
{
|
||
std::size_t operator()(const MeasureKey& key) const
|
||
{
|
||
std::size_t hash = std::hash<const void*>()(key.font);
|
||
for (const wchar_t character : key.text)
|
||
{
|
||
hash = hash * 131u + static_cast<std::size_t>(character);
|
||
}
|
||
return hash;
|
||
}
|
||
};
|
||
/** 上限保护:超过时整体清空(缓存只是加速手段,清空后重新填充即可)。 */
|
||
constexpr std::size_t kMeasureCacheLimit = 4096;
|
||
std::unordered_map<MeasureKey, float, MeasureKeyHash> g_measureCache;
|
||
std::mutex g_measureMutex;
|
||
} // namespace
|
||
|
||
void UiClearMeasureCache()
|
||
{
|
||
std::lock_guard<std::mutex> lock(g_measureMutex);
|
||
g_measureCache.clear();
|
||
}
|
||
|
||
bool UiFonts::Create(UINT targetDpi)
|
||
{
|
||
Destroy();
|
||
// 字体句柄会被重新创建,旧句柄可能被系统复用:必须作废按句柄缓存的文本宽度,
|
||
// 否则复用同一句柄值的新字体会命中旧宽度(布局错位)。
|
||
UiClearMeasureCache();
|
||
dpi = (targetDpi == 0) ? 96u : targetDpi;
|
||
|
||
const double scale = static_cast<double>(dpi) / 96.0;
|
||
const int uiHeight = static_cast<int>(std::lround(14.0 * scale));
|
||
const int smallHeight = static_cast<int>(std::lround(12.0 * scale));
|
||
const int titleHeight = static_cast<int>(std::lround(15.0 * scale));
|
||
const int monoHeight = static_cast<int>(std::lround(13.0 * scale));
|
||
|
||
ui = CreateUiFont(L"Segoe UI", uiHeight, false);
|
||
uiBold = CreateUiFont(L"Segoe UI Semibold", uiHeight, true);
|
||
uiSmall = CreateUiFont(L"Segoe UI", smallHeight, false);
|
||
uiSmallBold = CreateUiFont(L"Segoe UI Semibold", smallHeight, true);
|
||
title = CreateUiFont(L"Segoe UI Semibold", titleHeight, true);
|
||
mono = CreateUiFont(L"Consolas", monoHeight, false);
|
||
monoSmall = CreateUiFont(L"Consolas", smallHeight, false);
|
||
|
||
return ui != nullptr && uiBold != nullptr && uiSmall != nullptr && title != nullptr && mono != nullptr;
|
||
}
|
||
|
||
void UiFonts::Destroy()
|
||
{
|
||
const HFONT resources[] = {ui, uiBold, uiSmall, uiSmallBold, title, mono, monoSmall};
|
||
for (const HFONT font : resources)
|
||
{
|
||
if (font != nullptr)
|
||
{
|
||
DeleteObject(font);
|
||
}
|
||
}
|
||
ui = nullptr;
|
||
uiBold = nullptr;
|
||
uiSmall = nullptr;
|
||
uiSmallBold = nullptr;
|
||
title = nullptr;
|
||
mono = nullptr;
|
||
monoSmall = nullptr;
|
||
}
|
||
|
||
void UiFillRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb)
|
||
{
|
||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 大面积且**不透明**的填充走 GDI 路线:GDI+ 的 FillRectangle 实测约 2.2ns/像素
|
||
// (整个窗口背景 1.7M 像素约 0.9ms,信号面板背景约 1.9ms),
|
||
// 而 GDI 的 FillRect 快一个数量级。半透明填充必须留在 GDI+(GDI 不做 alpha 混色)。
|
||
// 顺序安全:GetHDC() 会先把已排队的 GDI+ 内容刷入 DC,本次填充之后再发生的 GDI+ 绘制
|
||
// 会在 ReleaseHDC() 之后刷出、正确地覆盖在上面。
|
||
const float area = rect.Width * rect.Height;
|
||
if ((argb & 0xFF000000u) == 0xFF000000u && area >= kGdiFillAreaThreshold)
|
||
{
|
||
HDC hdc = g.GetHDC();
|
||
if (hdc != nullptr)
|
||
{
|
||
RECT rc;
|
||
rc.left = static_cast<LONG>(std::floor(rect.X));
|
||
rc.top = static_cast<LONG>(std::floor(rect.Y));
|
||
rc.right = static_cast<LONG>(std::ceil(rect.X + rect.Width));
|
||
rc.bottom = static_cast<LONG>(std::ceil(rect.Y + rect.Height));
|
||
HBRUSH brush = CreateSolidBrush(ToColorRef(argb));
|
||
if (brush != nullptr)
|
||
{
|
||
FillRect(hdc, &rc, brush);
|
||
DeleteObject(brush);
|
||
}
|
||
g.ReleaseHDC(hdc);
|
||
return;
|
||
}
|
||
}
|
||
|
||
gd::SolidBrush brush(ToGdiColor(argb));
|
||
g.FillRectangle(&brush, rect);
|
||
}
|
||
|
||
void UiFillRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb)
|
||
{
|
||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
gd::GraphicsPath path;
|
||
BuildRoundRectPath(path, rect, radius);
|
||
gd::SolidBrush brush(ToGdiColor(argb));
|
||
g.FillPath(&brush, &path);
|
||
}
|
||
|
||
void UiDrawRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb, float width)
|
||
{
|
||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
gd::GraphicsPath path;
|
||
BuildRoundRectPath(path, rect, radius);
|
||
gd::Pen pen(ToGdiColor(argb), width);
|
||
g.DrawPath(&pen, &path);
|
||
}
|
||
|
||
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
|
||
std::uint32_t argb)
|
||
{
|
||
if (rects.empty())
|
||
{
|
||
return;
|
||
}
|
||
gd::GraphicsPath path;
|
||
for (const gd::RectF& rect : rects)
|
||
{
|
||
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||
{
|
||
continue;
|
||
}
|
||
BuildRoundRectPath(path, rect, radius);
|
||
}
|
||
if (path.GetPointCount() > 0)
|
||
{
|
||
gd::SolidBrush brush(ToGdiColor(argb));
|
||
g.FillPath(&brush, &path);
|
||
}
|
||
}
|
||
|
||
std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb)
|
||
{
|
||
const unsigned int alpha = (argb >> 24) & 0xFFu;
|
||
if (alpha >= 0xFFu)
|
||
{
|
||
return argb | 0xFF000000u;
|
||
}
|
||
if (alpha == 0u)
|
||
{
|
||
return background | 0xFF000000u;
|
||
}
|
||
const double ratio = static_cast<double>(alpha) / 255.0;
|
||
std::uint32_t result = 0xFF000000u;
|
||
for (int shift = 0; shift <= 16; shift += 8)
|
||
{
|
||
const double top = static_cast<double>((argb >> shift) & 0xFFu);
|
||
const double bottom = static_cast<double>((background >> shift) & 0xFFu);
|
||
const double mixed = bottom + (top - bottom) * ratio;
|
||
result |= (static_cast<std::uint32_t>(mixed + 0.5) & 0xFFu) << shift;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
void UiDrawSolidLines(gd::Graphics& g, const gd::RectF& clip, const float* segments,
|
||
std::size_t segmentCount, std::uint32_t argb, int width)
|
||
{
|
||
if (segments == nullptr || segmentCount == 0 || (argb & 0xFF000000u) != 0xFF000000u)
|
||
{
|
||
return;
|
||
}
|
||
if (width < 1)
|
||
{
|
||
width = 1;
|
||
}
|
||
|
||
const int clipLeft = static_cast<int>(std::floor(clip.X));
|
||
const int clipTop = static_cast<int>(std::floor(clip.Y));
|
||
const int clipRight = static_cast<int>(std::ceil(clip.X + clip.Width));
|
||
const int clipBottom = static_cast<int>(std::ceil(clip.Y + clip.Height));
|
||
if (clipRight <= clipLeft || clipBottom <= clipTop)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HDC hdc = g.GetHDC();
|
||
if (hdc == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// GDI 绘制不受 GDI+ 裁剪约束,这里显式设置并随后完整还原 DC 状态。
|
||
const int saved = SaveDC(hdc);
|
||
if (saved != 0)
|
||
{
|
||
IntersectClipRect(hdc, clipLeft, clipTop, clipRight, clipBottom);
|
||
}
|
||
|
||
HPEN pen = CreatePen(PS_SOLID, width, ToColorRef(argb));
|
||
if (pen != nullptr)
|
||
{
|
||
HGDIOBJ oldPen = SelectObject(hdc, pen);
|
||
for (std::size_t index = 0; index < segmentCount; ++index)
|
||
{
|
||
const float* segment = segments + index * 4;
|
||
MoveToEx(hdc, static_cast<int>(std::lround(segment[0])),
|
||
static_cast<int>(std::lround(segment[1])), nullptr);
|
||
LineTo(hdc, static_cast<int>(std::lround(segment[2])),
|
||
static_cast<int>(std::lround(segment[3])));
|
||
}
|
||
SelectObject(hdc, oldPen);
|
||
DeleteObject(pen);
|
||
}
|
||
|
||
if (saved != 0)
|
||
{
|
||
RestoreDC(hdc, saved);
|
||
}
|
||
g.ReleaseHDC(hdc);
|
||
}
|
||
|
||
void UiDrawLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb, float width)
|
||
{
|
||
gd::Pen pen(ToGdiColor(argb), width);
|
||
pen.SetStartCap(gd::LineCapFlat);
|
||
pen.SetEndCap(gd::LineCapFlat);
|
||
g.DrawLine(&pen, x1, y1, x2, y2);
|
||
}
|
||
|
||
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
|
||
TextAlign align)
|
||
{
|
||
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HDC hdc = g.GetHDC();
|
||
if (hdc == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||
const int oldMode = SetBkMode(hdc, TRANSPARENT);
|
||
const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb));
|
||
|
||
RECT rc;
|
||
rc.left = static_cast<LONG>(std::floor(rect.X));
|
||
rc.top = static_cast<LONG>(std::floor(rect.Y));
|
||
rc.right = static_cast<LONG>(std::ceil(rect.X + rect.Width));
|
||
rc.bottom = static_cast<LONG>(std::ceil(rect.Y + rect.Height));
|
||
|
||
UINT flags = DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS;
|
||
switch (align)
|
||
{
|
||
case TextAlign::CENTER:
|
||
flags |= DT_CENTER;
|
||
break;
|
||
case TextAlign::RIGHT:
|
||
flags |= DT_RIGHT;
|
||
break;
|
||
case TextAlign::LEFT:
|
||
default:
|
||
flags |= DT_LEFT;
|
||
break;
|
||
}
|
||
|
||
DrawTextW(hdc, text.c_str(), static_cast<int>(text.size()), &rc, flags);
|
||
|
||
SetTextColor(hdc, oldColor);
|
||
SetBkMode(hdc, oldMode);
|
||
SelectObject(hdc, oldFont);
|
||
g.ReleaseHDC(hdc);
|
||
}
|
||
|
||
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
|
||
{
|
||
if (text.empty() || font == nullptr)
|
||
{
|
||
return 0.0f;
|
||
}
|
||
|
||
// 记忆化:文本宽度只能走 GDI(GetDC + GetTextExtentPoint32W + ReleaseDC),
|
||
// 单次约 10µs;而布局代码每帧会把同一批字符串**重复测量成百上千次**——
|
||
// 例如浮动面板的几何是按「网格数 × 面板种类」反复重算的(5 网格 × 4 类 = 20 次,
|
||
// 每次十来个字符串),实测光这一项就占 3ms 以上。字体与文本都相同的测量结果
|
||
// 必然一致,因此按 (字体, 文本) 做一层缓存;字体被重建时由 `UiClearMeasureCache` 作废。
|
||
const MeasureKey key{font, text};
|
||
{
|
||
std::lock_guard<std::mutex> lock(g_measureMutex);
|
||
const auto found = g_measureCache.find(key);
|
||
if (found != g_measureCache.end())
|
||
{
|
||
return found->second;
|
||
}
|
||
}
|
||
|
||
HDC hdc = GetDC(nullptr);
|
||
if (hdc == nullptr)
|
||
{
|
||
return 0.0f;
|
||
}
|
||
|
||
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||
SIZE size = {};
|
||
GetTextExtentPoint32W(hdc, text.c_str(), static_cast<int>(text.size()), &size);
|
||
SelectObject(hdc, oldFont);
|
||
ReleaseDC(nullptr, hdc);
|
||
|
||
const float width = static_cast<float>(size.cx);
|
||
{
|
||
std::lock_guard<std::mutex> lock(g_measureMutex);
|
||
if (g_measureCache.size() >= kMeasureCacheLimit)
|
||
{
|
||
g_measureCache.clear();
|
||
}
|
||
g_measureCache.emplace(key, width);
|
||
}
|
||
return width;
|
||
}
|
||
|
||
std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth)
|
||
{
|
||
if (text.empty() || font == nullptr || maxWidth <= 0.0f)
|
||
{
|
||
return std::wstring();
|
||
}
|
||
if (UiMeasureTextWidth(font, text) <= maxWidth)
|
||
{
|
||
return text;
|
||
}
|
||
|
||
const std::wstring ellipsis = L"…";
|
||
const float ellipsisWidth = UiMeasureTextWidth(font, ellipsis);
|
||
if (ellipsisWidth > maxWidth)
|
||
{
|
||
return std::wstring();
|
||
}
|
||
|
||
std::wstring result = text;
|
||
while (!result.empty())
|
||
{
|
||
result.pop_back();
|
||
if (UiMeasureTextWidth(font, result) + ellipsisWidth <= maxWidth)
|
||
{
|
||
return result + ellipsis;
|
||
}
|
||
}
|
||
return ellipsis;
|
||
}
|
||
|
||
void UiDrawPlusIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth)
|
||
{
|
||
const float cx = rect.X + rect.Width * 0.5f;
|
||
const float cy = rect.Y + rect.Height * 0.5f;
|
||
const float arm = std::min(rect.Width, rect.Height) * 0.3f;
|
||
UiDrawLine(g, cx - arm, cy, cx + arm, cy, argb, strokeWidth);
|
||
UiDrawLine(g, cx, cy - arm, cx, cy + arm, argb, strokeWidth);
|
||
}
|
||
|
||
void UiDrawCrossIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth)
|
||
{
|
||
const float cx = rect.X + rect.Width * 0.5f;
|
||
const float cy = rect.Y + rect.Height * 0.5f;
|
||
const float arm = std::min(rect.Width, rect.Height) * 0.3f;
|
||
UiDrawLine(g, cx - arm, cy - arm, cx + arm, cy + arm, argb, strokeWidth);
|
||
UiDrawLine(g, cx + arm, cy - arm, cx - arm, cy + arm, argb, strokeWidth);
|
||
}
|
||
|
||
void UiDrawTriangleIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, IconDirection direction)
|
||
{
|
||
const float cx = rect.X + rect.Width * 0.5f;
|
||
const float cy = rect.Y + rect.Height * 0.5f;
|
||
const float arm = std::min(rect.Width, rect.Height) * 0.28f;
|
||
|
||
gd::PointF points[3];
|
||
switch (direction)
|
||
{
|
||
case IconDirection::LEFT:
|
||
points[0] = gd::PointF(cx + arm * 0.7f, cy - arm);
|
||
points[1] = gd::PointF(cx - arm * 0.7f, cy);
|
||
points[2] = gd::PointF(cx + arm * 0.7f, cy + arm);
|
||
break;
|
||
case IconDirection::RIGHT:
|
||
points[0] = gd::PointF(cx - arm * 0.7f, cy - arm);
|
||
points[1] = gd::PointF(cx + arm * 0.7f, cy);
|
||
points[2] = gd::PointF(cx - arm * 0.7f, cy + arm);
|
||
break;
|
||
case IconDirection::UP:
|
||
points[0] = gd::PointF(cx - arm, cy + arm * 0.7f);
|
||
points[1] = gd::PointF(cx, cy - arm * 0.7f);
|
||
points[2] = gd::PointF(cx + arm, cy + arm * 0.7f);
|
||
break;
|
||
case IconDirection::DOWN:
|
||
default:
|
||
points[0] = gd::PointF(cx - arm, cy - arm * 0.7f);
|
||
points[1] = gd::PointF(cx, cy + arm * 0.7f);
|
||
points[2] = gd::PointF(cx + arm, cy - arm * 0.7f);
|
||
break;
|
||
}
|
||
|
||
gd::SolidBrush brush(ToGdiColor(argb));
|
||
g.FillPolygon(&brush, points, 3);
|
||
}
|
||
|
||
double UiSliderRatioFromX(const gd::RectF& trackRect, float x)
|
||
{
|
||
if (trackRect.Width <= 0.0f)
|
||
{
|
||
return 0.0;
|
||
}
|
||
const double ratio = static_cast<double>(x - trackRect.X) / static_cast<double>(trackRect.Width);
|
||
if (ratio < 0.0)
|
||
{
|
||
return 0.0;
|
||
}
|
||
if (ratio > 1.0)
|
||
{
|
||
return 1.0;
|
||
}
|
||
return ratio;
|
||
}
|
||
double UiColorLuminance(std::uint32_t color)
|
||
{
|
||
const double r = static_cast<double>((color >> 16) & 0xFFu);
|
||
const double g = static_cast<double>((color >> 8) & 0xFFu);
|
||
const double b = static_cast<double>(color & 0xFFu);
|
||
return (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
|
||
}
|
||
|
||
UiInk UiContrastInk(std::uint32_t background)
|
||
{
|
||
// 极值文字/描边色(Solarized base3 / base02 / base03),亮度差很大,
|
||
// 因此任意底色下文字与底色之间都有充足对比。
|
||
const std::uint32_t kInkLight = 0xFFFDF6E3u;
|
||
const std::uint32_t kInkDark = 0xFF073642u;
|
||
const std::uint32_t kOutlineLight = 0xFFFDF6E3u;
|
||
const std::uint32_t kOutlineDark = 0xFF002B36u;
|
||
/** 向底色互补色混入的比例:线性响应信号配色,同时把对比度损失限制在 15% 以内。 */
|
||
const double kTint = 0.15;
|
||
|
||
const double luminance = UiColorLuminance(background);
|
||
// 底色越亮 → 文字越暗(取最大对比的一侧)。
|
||
const bool darkText = (luminance >= 0.5);
|
||
// 底色的互补色:相对亮度恰为 1 - 底色亮度。
|
||
const std::uint32_t complement = 0xFF000000u | ((~background) & 0x00FFFFFFu);
|
||
|
||
UiInk ink;
|
||
ink.text = MixColor(darkText ? kInkDark : kInkLight, complement, kTint);
|
||
ink.text = WithAlpha(ink.text, 0xFFu);
|
||
ink.outline = MixColor(darkText ? kOutlineLight : kOutlineDark, complement, kTint);
|
||
ink.outline = WithAlpha(ink.outline, 0x99u);
|
||
return ink;
|
||
}
|
||
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius)
|
||
{
|
||
const Theme& theme = SolarizedLight();
|
||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||
|
||
if (state == 1)
|
||
{
|
||
UiFillRoundRect(g, rect, radius, theme.accent);
|
||
gd::Pen pen(ToGdiColor(theme.textOnAccent), std::max(1.4f, rect.Height * 0.11f));
|
||
pen.SetStartCap(gd::LineCapRound);
|
||
pen.SetEndCap(gd::LineCapRound);
|
||
g.DrawLine(&pen, rect.X + rect.Width * 0.22f, centerY, rect.X + rect.Width * 0.42f,
|
||
rect.Y + rect.Height * 0.72f);
|
||
g.DrawLine(&pen, rect.X + rect.Width * 0.42f, rect.Y + rect.Height * 0.72f,
|
||
rect.X + rect.Width * 0.78f, rect.Y + rect.Height * 0.28f);
|
||
}
|
||
else if (state == 2)
|
||
{
|
||
UiFillRoundRect(g, rect, radius, theme.accentSoft);
|
||
UiDrawRoundRect(g, rect, radius, theme.accent, 1.0f);
|
||
UiFillRect(g, gd::RectF(rect.X + rect.Width * 0.22f, centerY - rect.Height * 0.08f,
|
||
rect.Width * 0.56f, rect.Height * 0.16f),
|
||
theme.accent);
|
||
}
|
||
else
|
||
{
|
||
UiFillRoundRect(g, rect, radius, theme.cardBg);
|
||
UiDrawRoundRect(g, rect, radius, theme.borderStrong, 1.0f);
|
||
}
|
||
}
|
||
|
||
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||
bool active, float scale)
|
||
{
|
||
if (track.Width <= 0.0f || track.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (ratio < 0.0)
|
||
{
|
||
ratio = 0.0;
|
||
}
|
||
if (ratio > 1.0)
|
||
{
|
||
ratio = 1.0;
|
||
}
|
||
|
||
const float radius = track.Height * 0.5f;
|
||
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
||
|
||
// 已填充部分严格止于圆钮中心,最小值时不会留下超出圆钮的填充残留。
|
||
const float fillWidth = static_cast<float>(ratio) * track.Width;
|
||
if (fillWidth > 0.5f)
|
||
{
|
||
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
||
}
|
||
|
||
const float knobRadius = radius + (active ? 2.5f * scale : 0.8f * scale);
|
||
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
||
|
||
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
||
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
||
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||
knobRadius * 2.0f);
|
||
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||
knobRadius * 2.0f);
|
||
}
|
||
|
||
float UiEditTextLeft(const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale,
|
||
float& innerWidth)
|
||
{
|
||
innerWidth = std::max(4.0f, rect.Width - 12.0f * scale);
|
||
const float textLeft = rect.X + 6.0f * scale;
|
||
|
||
const std::wstring& text = edit.Text();
|
||
if (text.empty())
|
||
{
|
||
return textLeft;
|
||
}
|
||
|
||
const float textWidth = UiMeasureTextWidth(font, text);
|
||
if (textWidth <= innerWidth)
|
||
{
|
||
return textLeft;
|
||
}
|
||
|
||
// 保证插入符可见:插入符超出右边界时向左滚动,并夹到总可滚动范围内。
|
||
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
|
||
float scroll = 0.0f;
|
||
if (caretX > innerWidth)
|
||
{
|
||
scroll = caretX - innerWidth;
|
||
}
|
||
const float maximum = textWidth - innerWidth;
|
||
scroll = std::max(0.0f, std::min(scroll, maximum));
|
||
return textLeft - scroll;
|
||
}
|
||
|
||
std::size_t UiEditOffsetAt(HFONT font, const std::wstring& text, float localX)
|
||
{
|
||
if (text.empty() || font == nullptr)
|
||
{
|
||
return 0;
|
||
}
|
||
if (localX <= 0.0f)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
// 逐字符累加宽度,找到距离最近的字符边界(文本通常很短,线性扫描足够)。
|
||
float previous = 0.0f;
|
||
for (std::size_t index = 0; index < text.size(); ++index)
|
||
{
|
||
const float current = UiMeasureTextWidth(font, text.substr(0, index + 1));
|
||
if (localX < current)
|
||
{
|
||
// 落在本字符的左半 → 取左边界,右半 → 取右边界。
|
||
return (localX - previous <= current - localX) ? index : (index + 1);
|
||
}
|
||
previous = current;
|
||
}
|
||
return text.size();
|
||
}
|
||
|
||
void UiTextAtClipped(gd::Graphics& g, const std::wstring& text, HFONT font, float x, const gd::RectF& rect,
|
||
std::uint32_t argb, const gd::RectF& clipRect)
|
||
{
|
||
if (text.empty() || font == nullptr || clipRect.Width <= 0.0f || clipRect.Height <= 0.0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HDC hdc = g.GetHDC();
|
||
if (hdc == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||
const int oldMode = SetBkMode(hdc, TRANSPARENT);
|
||
const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb));
|
||
|
||
TEXTMETRICW metrics = {};
|
||
GetTextMetricsW(hdc, &metrics);
|
||
const int baselineY = static_cast<int>(
|
||
std::floor(rect.Y + (rect.Height - static_cast<float>(metrics.tmHeight)) * 0.5f));
|
||
|
||
RECT clip = {};
|
||
clip.left = static_cast<LONG>(std::floor(clipRect.X));
|
||
clip.top = static_cast<LONG>(std::floor(clipRect.Y));
|
||
clip.right = static_cast<LONG>(std::ceil(clipRect.X + clipRect.Width));
|
||
clip.bottom = static_cast<LONG>(std::ceil(clipRect.Y + clipRect.Height));
|
||
|
||
// ETO_CLIPPED 让 GDI 在指定矩形内裁剪,长文本不会再画到输入框之外。
|
||
ExtTextOutW(hdc, static_cast<int>(std::floor(x)), baselineY, ETO_CLIPPED, &clip, text.c_str(),
|
||
static_cast<UINT>(text.size()), nullptr);
|
||
|
||
SetTextColor(hdc, oldColor);
|
||
SetBkMode(hdc, oldMode);
|
||
SelectObject(hdc, oldFont);
|
||
g.ReleaseHDC(hdc);
|
||
}
|
||
|
||
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale)
|
||
{
|
||
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const Theme& theme = SolarizedLight();
|
||
const float corner = 3.0f * scale;
|
||
|
||
UiFillRoundRect(g, rect, corner, theme.cardBg);
|
||
UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f);
|
||
|
||
UiDrawEditFieldContent(g, rect, edit, font, scale);
|
||
}
|
||
|
||
void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
|
||
float scale)
|
||
{
|
||
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const Theme& theme = SolarizedLight();
|
||
const std::wstring& text = edit.Text();
|
||
const float innerLeft = rect.X + 6.0f * scale;
|
||
|
||
float innerWidth = 0.0f;
|
||
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
|
||
const gd::RectF clipRect(innerLeft, rect.Y + 1.0f * scale, innerWidth, rect.Height - 2.0f * scale);
|
||
|
||
if (text.empty())
|
||
{
|
||
// 空内容仍绘制插入符,保持焦点可见。
|
||
UiDrawLine(g, textLeft, rect.Y + 4.0f * scale, textLeft, rect.Y + rect.Height - 4.0f * scale,
|
||
theme.accent, 1.4f * scale);
|
||
return;
|
||
}
|
||
|
||
// 光标与选区端点对应的像素位置(以文本左端为原点)。
|
||
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
|
||
const float selBeginX =
|
||
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionBegin())) : 0.0f;
|
||
const float selEndX =
|
||
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionEnd())) : 0.0f;
|
||
|
||
const bool fullSelection =
|
||
edit.HasSelection() && edit.SelectionBegin() == 0 && edit.SelectionEnd() == text.size();
|
||
|
||
const int saveState = g.Save();
|
||
g.SetClip(clipRect, gd::CombineModeIntersect);
|
||
|
||
// 选区高亮绘制在文本之下,整段选中时呈现“已全选”观感。
|
||
// ⚠️ 这里**必须自己把矩形夹到内容区**:`UiFillRect` 对面积足够大的不透明矩形会走
|
||
// GDI `FillRect` 快路径,而 GDI 绘制**不受 GDI+ 裁剪区约束**,长文本的全选高亮
|
||
// (宽度等于整段文本宽度)因此会溢出到输入框之外。夹取之后矩形始终落在框内,
|
||
// 既保留快路径又不越界。
|
||
if (edit.HasSelection() && selEndX > selBeginX)
|
||
{
|
||
gd::RectF highlight(textLeft + selBeginX, rect.Y + 3.0f * scale, selEndX - selBeginX,
|
||
rect.Height - 6.0f * scale);
|
||
const float left = std::max(highlight.X, clipRect.X);
|
||
const float right = std::min(highlight.X + highlight.Width, clipRect.X + clipRect.Width);
|
||
const float top = std::max(highlight.Y, clipRect.Y);
|
||
const float bottom = std::min(highlight.Y + highlight.Height, clipRect.Y + clipRect.Height);
|
||
highlight = gd::RectF(left, top, right - left, bottom - top);
|
||
UiFillRect(g, highlight, theme.accent);
|
||
}
|
||
|
||
// 长文本用 ETO_CLIPPING 交给 GDI 裁剪,避免字符画到输入框之外。
|
||
UiTextAtClipped(g, text, font, textLeft, rect,
|
||
fullSelection ? theme.textOnAccent : theme.textStrong, clipRect);
|
||
|
||
if (!fullSelection && edit.HasSelection() && selEndX > selBeginX)
|
||
{
|
||
// 局部选区:在反白底衬上重绘被选中的片段,其余文字保持常规颜色。
|
||
const std::wstring selected =
|
||
text.substr(edit.SelectionBegin(), edit.SelectionEnd() - edit.SelectionBegin());
|
||
UiTextAtClipped(g, selected, font, textLeft + selBeginX, rect, theme.textOnAccent, clipRect);
|
||
}
|
||
|
||
// 插入符。
|
||
const float caretDrawX = textLeft + caretX;
|
||
UiDrawLine(g, caretDrawX, rect.Y + 4.0f * scale, caretDrawX, rect.Y + rect.Height - 4.0f * scale,
|
||
theme.textStrong, 1.4f * scale);
|
||
|
||
g.Restore(saveState);
|
||
}
|
||
|
||
bool UiEditFieldMouseDown(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x, int y)
|
||
{
|
||
if (!edit.Active() || font == nullptr || rect.Width <= 0.0f)
|
||
{
|
||
return false;
|
||
}
|
||
if (!UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 用与绘制完全相同的布局函数换算文本起点,保证点哪儿光标就落在哪儿。
|
||
float innerWidth = 0.0f;
|
||
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
|
||
edit.BeginDrag(UiEditOffsetAt(font, edit.Text(), static_cast<float>(x) - textLeft));
|
||
return true;
|
||
}
|
||
|
||
bool UiEditFieldMouseMove(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x)
|
||
{
|
||
if (!edit.Active() || !edit.Dragging() || font == nullptr || rect.Width <= 0.0f)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
float innerWidth = 0.0f;
|
||
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
|
||
return edit.UpdateDrag(UiEditOffsetAt(font, edit.Text(), static_cast<float>(x) - textLeft));
|
||
}
|
||
|
||
bool UiSwatchNeedsOutline(float opacity)
|
||
{
|
||
return !(opacity >= kSwatchOutlineOpacity);
|
||
}
|
||
|
||
std::uint32_t UiDrawSwatch(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float opacity,
|
||
float radius, float scale)
|
||
{
|
||
double alpha = static_cast<double>(opacity);
|
||
if (!(alpha > 0.0))
|
||
{
|
||
alpha = 0.0;
|
||
}
|
||
if (alpha > 1.0)
|
||
{
|
||
alpha = 1.0;
|
||
}
|
||
|
||
const std::uint32_t filled = WithAlpha(color, static_cast<unsigned int>(alpha * 255.0 + 0.5));
|
||
if (rect.Width > 0.0f && rect.Height > 0.0f)
|
||
{
|
||
UiFillRoundRect(g, rect, radius, filled);
|
||
|
||
// 透明度过低时色块本身难以辨识,补充一圈描边以保持可读性;可见时不绘制描边。
|
||
if (UiSwatchNeedsOutline(opacity))
|
||
{
|
||
UiDrawRoundRect(g, rect, radius, SolarizedLight().borderStrong,
|
||
std::max(1.0f, scale));
|
||
}
|
||
}
|
||
return filled;
|
||
}
|
||
|
||
} // namespace jdi
|