Files
DeepSeek-V4.1-Flash bdd703b88b 主题自适应配色 + 预览曲线连续配色 + 样式区复位按钮 + 均值滤波面板重做 + 面板缓存环带残留 + 调试层序 + 自由标记选取与性能
① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
   `MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
   axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
   `ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
   目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
   (旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
   菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
   (单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
   (点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
   collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
   ⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
   `UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
   纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
   `MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
   单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。

验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
2026-10-30 03:10:51 +08:00

716 lines
22 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "PaintUtil.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <functional>
#include <mutex>
#include <unordered_map>
#include <utility>
#include <string>
namespace jdi
{
namespace
{
/**
* @brief 矩形面积超过该阈值(逻辑像素²)且为不透明色时,改用 GDI 填充。
*
* GDI+ 的 FillRectangle 约 2.2ns/像素;面积过小时 GDI 的 GetHDC/ReleaseHDC
* 固定开销反而不划算,因此在阈值以下仍走 GDI+。
*/
const float kGdiFillAreaThreshold = 20000.0f;
}
// 圆角矩形路径是公开的几何构建器:`UiFillRoundRect` / `UiDrawRoundRect` / 需要把位图或渐变
// **裁剪进同一个圆角形状**的调用方共用它,保证"填充的形状"与"描边的形状"逐点一致。
void UiBuildRoundRectPath(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();
}
namespace
{
/**
* @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;
/** 文本的像素宽度与字符单元高度(后者与 `DT_VCENTER` 的居中口径一致)。 */
struct TextExtent
{
float width = 0.0f; /**< 像素宽度。 */
float height = 0.0f; /**< 字符单元高度。 */
};
std::unordered_map<MeasureKey, TextExtent, MeasureKeyHash> g_measureCache;
std::mutex g_measureMutex;
/**
* @brief 取文本的宽 / 高(带缓存)。
*
* ⚠️ 必须走 GDI(`GetDC` + `GetTextExtentPoint32W` + `ReleaseDC`),单次约 10µs;
* 而布局与绘制每帧会把同一批字符串**重复测量成百上千次**,因此按 (字体, 文本) 缓存。
* 高度也一并缓存:`DrawUiText` 的快速路径要用它做垂直居中(见那里的说明)。
*/
TextExtent MeasureText(HFONT font, const std::wstring& text)
{
if (text.empty() || font == nullptr)
{
return TextExtent();
}
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 TextExtent();
}
HGDIOBJ oldFont = SelectObject(hdc, font);
SIZE size = {};
GetTextExtentPoint32W(hdc, text.c_str(), static_cast<int>(text.size()), &size);
SelectObject(hdc, oldFont);
ReleaseDC(nullptr, hdc);
TextExtent extent;
extent.width = static_cast<float>(size.cx);
extent.height = static_cast<float>(size.cy);
{
std::lock_guard<std::mutex> lock(g_measureMutex);
if (g_measureCache.size() >= kMeasureCacheLimit)
{
g_measureCache.clear();
}
g_measureCache.emplace(key, extent);
}
return extent;
}
} // 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 tinyHeight = static_cast<int>(std::lround(10.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);
uiTiny = CreateUiFont(L"Segoe UI", tinyHeight, false);
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 && uiTiny != nullptr &&
title != nullptr && mono != nullptr;
}
void UiFonts::Destroy()
{
const HFONT resources[] = {ui, uiBold, uiSmall, uiSmallBold, uiTiny, title, mono, monoSmall};
for (const HFONT font : resources)
{
if (font != nullptr)
{
DeleteObject(font);
}
}
ui = nullptr;
uiBold = nullptr;
uiSmall = nullptr;
uiSmallBold = nullptr;
uiTiny = 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;
UiBuildRoundRectPath(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;
UiBuildRoundRectPath(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;
}
UiBuildRoundRectPath(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);
}
namespace
{
/** @brief 当前安装的文本几何收集器(`nullptr` = 不收集,调试开关关闭)。 */
UiTextRectSink* g_textRectSink = nullptr;
/** @brief 当前文本矩形的坐标偏移(离屏位图渲染时非零)。 */
float g_textRectOffsetX = 0.0f;
float g_textRectOffsetY = 0.0f;
/**
* @brief 回报一段文本的墨迹矩形(仅在收集器已安装时执行)。
*
* 入参 `rect` 是**布局矩形**(可能整行宽);这里按对齐方式与实测宽度/高度把它
* 收窄成文字真正占据的那一块 —— 否则每个文本 box 都会跟整行一样宽,边界失去意义。
*
* @param [IN] text 文本内容。
* @param [IN] font 字体句柄。
* @param [IN] rect 布局矩形。
* @param [IN] align 水平对齐方式。
*/
void ReportTextRect(const std::wstring& text, HFONT font, const gd::RectF& rect, TextAlign align)
{
if (g_textRectSink == nullptr || text.empty() || font == nullptr)
{
return;
}
const TextExtent extent = MeasureText(font, text);
if (extent.width <= 0.0f)
{
return;
}
const float width = std::min(extent.width, rect.Width);
const float height = std::min(std::max(extent.height, 1.0f), rect.Height);
float x = rect.X;
if (align == TextAlign::CENTER)
{
x = rect.X + (rect.Width - width) * 0.5f;
}
else if (align == TextAlign::RIGHT)
{
x = rect.X + rect.Width - width;
}
const float y = rect.Y + (rect.Height - height) * 0.5f;
g_textRectSink->OnTextRect(
gd::RectF(x + g_textRectOffsetX, y + g_textRectOffsetY, width, height));
}
/**
* @brief `UiText` 与 `UiTextClipped` 的公共实现。
*
* ⚠️ `clip == nullptr` 时**不加任何 GDI 裁剪**(与历史行为完全一致);
* 提供裁剪时在同一个 HDC 上 `SaveDC`/`IntersectClipRect` 并完整还原 ——
* 这是唯一能让 `DrawTextW` 受限于指定范围的办法(GDI+ 的 `SetClip` 对 GDI 文本无效)。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] text 文本内容。
* @param [IN] font 字体句柄。
* @param [IN] rect 文本布局矩形。
* @param [IN] argb 文本颜色。
* @param [IN] align 水平对齐方式。
* @param [IN] clip 裁剪矩形(nullptr = 不裁剪)。
*/
void DrawUiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
std::uint32_t argb, TextAlign align, const gd::RectF* clip)
{
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f)
{
return;
}
if (clip != nullptr && (clip->Width <= 0.0f || clip->Height <= 0.0f))
{
return; // 裁剪区为空:任何文字都落在界外
}
HDC hdc = g.GetHDC();
if (hdc == nullptr)
{
return;
}
// GDI 文本不受 GDI+ 裁剪区约束:需要限制范围时显式裁剪,结束前完整还原 DC 状态。
int saved = 0;
if (clip != nullptr)
{
saved = SaveDC(hdc);
if (saved != 0)
{
IntersectClipRect(hdc, static_cast<int>(std::floor(clip->X)),
static_cast<int>(std::floor(clip->Y)),
static_cast<int>(std::ceil(clip->X + clip->Width)),
static_cast<int>(std::ceil(clip->Y + clip->Height)));
}
}
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));
// ⚠️ 这里必须继续用 `DrawTextW`:试过改用 `ExtTextOutW` 的"快速路径"(约快 2~4 倍),
// 但同一段文字的**光栅化结果并不相同**(实测面板区域 10% 的像素有差异,
// 亚像素边缘与基线差 1px)—— 全体界面文字观感都会变,因此放弃。
// 列表卡顿改由"行位图缓存 + 动画期间不预热网格图层"解决(见 `SignalPanel::PaintRowCached`)。
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);
// 调试:回报文字真正占据的矩形(在恢复 DC 状态之前算,此时字体仍被选中)。
ReportTextRect(text, font, rect, align);
SetTextColor(hdc, oldColor);
SetBkMode(hdc, oldMode);
SelectObject(hdc, oldFont);
if (saved != 0)
{
RestoreDC(hdc, saved);
}
g.ReleaseHDC(hdc);
}
} // namespace
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
TextAlign align)
{
DrawUiText(g, text, font, rect, argb, align, nullptr);
}
void UiTextClipped(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
std::uint32_t argb, TextAlign align, const gd::RectF& clipRect)
{
DrawUiText(g, text, font, rect, argb, align, &clipRect);
}
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
{
// 记忆化(键 = 字体 + 文本,值 = 宽 / 高):布局代码每帧会把同一批字符串
// **重复测量成百上千次**,而 `GetTextExtentPoint32W` 单次约 10µs。
return MeasureText(font, text).width;
}
float UiMeasureTextHeight(HFONT font, const std::wstring& text)
{
return MeasureText(font, text).height;
}
void UiSetTextRectSink(UiTextRectSink* sink)
{
g_textRectSink = sink;
}
UiTextRectSink* UiGetTextRectSink()
{
return g_textRectSink;
}
void UiSetTextLayer(int layer)
{
if (g_textRectSink != nullptr)
{
g_textRectSink->OnTextLayer(layer);
}
}
int UiGetTextLayer()
{
return (g_textRectSink != nullptr) ? g_textRectSink->TextLayer() : 0;
}
void UiSetTextRectOffset(float dx, float dy)
{
g_textRectOffsetX = dx;
g_textRectOffsetY = dy;
}
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 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);
// 调试:回报文字真正占据的矩形(起点由调用方给出,宽度取实测值)。
if (g_textRectSink != nullptr)
{
const float width = UiMeasureTextWidth(font, text);
if (width > 0.0f)
{
const float height = std::max(1.0f, static_cast<float>(metrics.tmHeight));
g_textRectSink->OnTextRect(
gd::RectF(std::floor(x) + g_textRectOffsetX,
rect.Y + (rect.Height - height) * 0.5f + g_textRectOffsetY, width, height));
}
}
SetTextColor(hdc, oldColor);
SetBkMode(hdc, oldMode);
SelectObject(hdc, oldFont);
g.ReleaseHDC(hdc);
}
void UiPaintResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float scale)
{
if (rect.Width <= 0.0f || rect.Height <= 0.0f)
{
return;
}
const float inset = 2.5f * scale;
const gd::RectF box(rect.X + inset, rect.Y + inset, rect.Width - inset * 2.0f,
rect.Height - inset * 2.0f);
if (box.Width <= 1.0f || box.Height <= 1.0f)
{
return;
}
const gd::Color color = ToGdiColor(argb);
gd::Pen pen(color, std::max(1.0f, 1.5f * scale));
pen.SetStartCap(gd::LineCapRound);
pen.SetEndCap(gd::LineCapRound);
// 缺口留在右上角(从 300° 到 20° 处断开),与常见的"重置/刷新"图标一致。
g.DrawArc(&pen, box.X, box.Y, box.Width, box.Height, 300.0f, 300.0f);
// 箭头:位于缺口末端(约 300° 方向),指向前进方向。
const float cx = box.X + box.Width * 0.5f;
const float cy = box.Y + box.Height * 0.5f;
const float radius = box.Width * 0.5f;
const float angle = 300.0f * 3.14159265f / 180.0f;
const float tipX = cx + std::cos(angle) * radius;
const float tipY = cy + std::sin(angle) * radius;
const float size = std::max(2.5f, 3.2f * scale);
gd::PointF arrow[3];
arrow[0] = gd::PointF(tipX, tipY);
arrow[1] = gd::PointF(tipX - size, tipY - size * 0.25f);
arrow[2] = gd::PointF(tipX - size * 0.25f, tipY - size);
gd::SolidBrush brush(color);
g.FillPolygon(&brush, arrow, 3);
}
} // namespace jdi