Files
Just-DataInspector/Utilities/Src/PaintUtil.cpp
T
DeepSeek V4.1 Flash 0871eb0cba 分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 Components/Shared 组件 + 过时注释/命名大清理 + 二进制瘦身 25.6%
## 一、UI 交互修复(用户报告)

① 分组折起热区(用户要求):点分组**名称**不再折起,只有**折叠三角**或**「组X」标签**
   才折起。RowPart 新增 GROUP_TITLE;RowHitPart() 为分组行判出三角 / 复选框 / 关闭 ✕ /
   「组X」标签矩形 / 其余 BODY;OnLButtonDown 只在这两个热区调用 ToggleGroupFromRow()。
   附带:双击「组X」不再进入改名(该区域现为折起热区),双击分组名仍可改名。
② 分组改名输入框不再吞掉「N 项」:RowNameRect() 对分组行的右边界改为「项数左边缘 − 6*scale」;
   新增 GroupLabelText()/GroupStatsText() 作为文本与宽度的唯一来源(PaintGroupRow 与
   RowNameRect 共用),避免名称可用宽度与实际绘制文本漂移。

## 二、数据模型去冗余(用户指出的重点)

- SignalData / SignalGroup **只保留一个名称字段** name:删除 displayName、删除 Title()
  (31 处调用点改为直接用 name);DataSet::SetSignalDisplayName / SetGroupDisplayName
  → RenameSignal / RenameGroup,且**空串 / 纯空白一律拒绝**(名称不再有回退值)。
  依据:信号靠 SignalRef 下标定位、从不按名字匹配 ⇒ 两个字段是同一语义的两份副本。
  收益:每条信号 / 每个分组少一个 std::wstring(24B + 堆分配)与一条 Title() 分支。
- 删除 Application::m_loadingBytes(全仓既不读也不写的死成员,注释却称“用于展示体积”)。

## 三、新增 Components/Shared 组件(通用 UI 控件)

- 从 Utilities/PaintUtil.* 迁出全部「控件类」绘制/命中:图标(+ / ✕ / 三角)、复选框、
  滑块(UiSliderRatioFromX / UiDrawSlider)、内联输入框(UiDrawEditField /
  UiDrawEditFieldContent / UiEditTextLeft / UiEditOffsetAt / UiEditFieldMouseDown /
  UiEditFieldMouseMove)、颜色样块(UiDrawSwatch / UiSwatchNeedsOutline)、
  取值胶囊配色(UiInk / UiColorLuminance / UiContrastInk)。
- PaintUtil.* 只保留低层绘制原语(矩形 / 圆角 / 直线 / 批量实线 / 颜色混合 / 文本与测量)。
- 依赖方向固定:Components/* → Components/Shared → Utilities(不得反向,Utilities 不再含控件)。
- Makefile 与 .preflight/perf/build.ps1 已登记新目录;调用方改 include "UiControls.hpp"。

## 四、过时注释 / 命名批量修正(承接上一轮未做完的部分)

- 注释:文件下标→分组下标、文件内信号下标→组内信号下标、所属文件下标→所属分组下标;
  措辞统一 文件分组→分组、移除该文件→移除该分组、打开 CSV 文件→打开数据文件 等一批。
- 文不对题的形参名:VisibleIndexRange(const TimeAxis& file,…) → axis;
  Application::LoadFileAsync(…, SignalGroup& file,…) → group。
- 函数名里的 File 语义(上一轮只改了 RowKind::FILE):PaintFileRow→PaintGroupRow、
  FileCloseRect→GroupCloseRect、OpenFileMenu→OpenGroupMenu、FileGroupLabelWidth→GroupLabelWidth、
  StyleEditField::FILE_NAME→GROUP_TITLE;局部变量 fileRow/fileHeight/isFile →
  groupRow/groupHeight/isGroup。
- 修复 DataModel.hpp 两处被压扁的注释块(/**<TAB><TAB>* @brief …)与 Application::NewTab
  上方重复的文档块。
- ⚠️ 踩坑:GROUP_NAME 撞上 nb30.h(windows.h 引入)的 #define GROUP_NAME 0x80,
  导致枚举编译失败,改名 GROUP_TITLE 解决。

## 五、二进制瘦身:1 013 248 → 754 176 B(−25.6%,省 259 KB)

- 真因不是 PlotView.cpp,而是 Resources/JustDataInspector.ico 里 **256×256@32bpp 以未压缩
  BMP 存储的条目(270 376 B,占整包 27%)**。改为 PNG 存储(Vista+ 标准做法,观感零变化)
  ⇒ 该条目 270 376 → 11 280 B、整个 ico 302 430 → 43 334 B(小尺寸仍保留 BMP)。
- 实测结论:“拆分大文件 ⇒ 瘦身”不成立(同一批机器码仍要进二进制);
  -ffunction-sections/-fdata-sections/-Wl,--gc-sections 在本工程反而 +9 216 B(已回退)。
- ⚠️ .o 体积受节对齐/重定位表主导,判断“代码搬去哪了”要用 nm,不要看 .o 字节数。

## 六、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)

- panelanim 156/156(连跑 5 次稳定;新增/更新:点分组名不折起也不滚动、扫掠定位「组X」热区、
  筛选态与普通态折叠三角逐像素一致、改名输入框右边界早于项数文本);verify 49/49;
  datacheck 69/69(含“重命名写入唯一名称字段且空名被拒”);edgecheck 46/46。
- A/B:回退两处 UI 修复 ⇒ 10 项如实失败;恢复后全绿。
- make release / make debug 零警告。

## 七、本轮未完成(已在报告与 agent_mem.md 中如实记录)

- PlotView.cpp(13 695 行 / 544 KB)按功能拆分为多个协作文件:未执行。
  前置改造是把第 13–888 行的匿名命名空间(~870 行常量与工具函数)改成具名命名空间并逐个
  inline,再逐个文件搬移并反复验证;时间不足以安全完成,故主动不做(附可执行方案)。
- SignalPanel.cpp / Application.cpp / DataModel.cpp 的多文件化、Lane 抽出 LanePanelState、
  SetStatusMessage 模态阻塞解耦:均未执行。

## 八、文档

- 新增 Documents/refactor_report_2026-10-16.md(本轮详细报告);
  删除 Documents/refactor_report_2026-10-15.md(有效内容已并入 agent_mem.md 与本报告)。
- Documents/agent_mem.md 新增 §9.37 与 §11.2 第 85–90 条;更新数据层架构摘要(单一名称字段);
  §12 历史附录加“时效声明”。
2026-10-16 03:25:48 +08:00

572 lines
17 KiB
C++
Raw 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 颜色样块补充描边的不透明度阈值。
*
* 不透明度低于该值时,色块自身与浅色背景的对比度已不足,需要额外描边才能辨识;
* 高于该值时按需求不绘制描边,保持与预设色块一致的纯净外观。
*/
/**
* @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;
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 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 `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));
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);
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)
{
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 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);
}
} // namespace jdi