Files
Just-DataInspector/Utilities/Src/PaintUtil.cpp
T
DeepSeek V4.1 Flash fe72afc83c 绘图交互完善 + 稠密区域拖拽性能修复(VERY_BIG_DATA 2ms/大格不再卡顿)
一、功能修正
- 修复「光标数据点在上下视界外不绘制」的问题,并让上下越界与左右越界一样用三角表示:
  新增 `DataMarker` / `PaintDataMarker()`,统一处理「可视区内画圆点、越界则钉在网格边上
  画三角指示方向」的四向情形(左/右/上/下)。光标取值与中心轴取值都改走这一套,
  因此纵向被裁掉的数据点不再消失。
- 「边沿计数(中心轴)」的中心平均值曲线改为**跟随原信号的样式**:
  与原始信号同色但更透明,线宽同样跟随(lineWidth + 0.6,下限 1.4),
  原信号改颜色/线宽/透明度后曲线同步变化(曲线参与场景指纹,样式变化即失效重绘)。
  光标显示中心值时的胶囊也改用**原始信号颜色**(一一对应),并**始终按线段模式**着色,
  不再使用光标色。
- 「拆分当前网格」在可点击时追加尾缀「(N个信号)」(N = 该网格内信号数),
  只有 1 条信号(不可点击)时不加尾缀。

二、新增:左侧信号筛选框
- 信号列表上方新增「筛选信号」输入框:空内容时显示灰色占位提示,单击进入编辑,
  点击别处或回车结束编辑;列表内容随输入**实时**变化。
- 支持 `*` 通配符(`*` 等价于"任意字符",因此 `*` 单独出现等价于不过滤),忽略大小写。
- 筛选结果**仍以组为单位**,组默认展开,只显示匹配的信号;组内无匹配信号时整组不显示。
- 复用统一的 `TextEdit` 编辑模型,因此光标/选区/Ctrl+A/C/X/V 等行为与其它输入框一致。

三、性能:修复 VERY_BIG_DATA.CSV 在 2ms/大格密集区域拖拽卡顿
以「显示全部 5 条信号 + 时间轴缩放到 2ms 一大格」为场景逐段打点定位后修复:
- **主因:稠密包络的锯齿描边光栅化**。抽稀时每列产生 min/max 两个顶点交替的锯齿,
  2000+ 条近乎满幅的斜线段在 GDI+ 抗锯齿下要光栅化几十万像素(实测每网格 14ms,
  5 网格约 70ms/帧)。
  改为**包络带一次填充**:逐列记录上/下包络,收段时拼成「上包络正向 + 下包络反向」的
  闭合多边形,用一条 `GraphicsPath` + `FillPath` 绘制(显式 `FillModeWinding`,
  自交包络不会挖洞)。基准测试(1224 列):14.26ms → **1.69ms(约 8.4 倍)**,
  且**保留抗锯齿**、静态帧与拖拽帧完全一致(不是"交互降级")。
  逐像素对比确认观感等价(墨量覆盖率 90.8% vs 91.3%)。
  包络带同时用于静态绘制与 MOVE_SIGNAL 的拖拽虚影。
- **大面积不透明填充改走 GDI**:`UiFillRect` 在面积 ≥ 20000px² 且颜色不透明时使用
  `GetHDC + FillRect`(GDI+ FillRectangle 约 2.2ns/像素,整窗背景 1.7M 像素要 0.9ms、
  面板背景 1.9ms)。半透明填充仍走 GDI+(GDI 不做 alpha 混色);小面积仍走 GDI+
  (避免 GetHDC 固定开销)。顺序安全:GetHDC 会先刷入已排队的 GDI+ 内容。
- **`UiTextWidth` 不再触发 GDI+ 批次刷新**:改为复用 `UiMeasureTextWidth`
  (`GetDC(nullptr)`),`UiEllipsize` 逐字符回退时的十几次刷新被完全消除。

实测(125% 缩放、1725x996、5 信号、2ms 一大格):
  绘图区绘制 47~87ms → **3.6~4.5ms**;整帧最大 196.8ms → **约 17~21ms(≈50~60fps)**,
  与空闲帧基本持平,即"拖拽与不拖拽一样流畅"。

验证(VERY_BIG_DATA.CSV;面板行坐标因新增筛选框整体下移,复选框客户区 (61,250) 起、行距 33):
- 筛选框:输入 `B` 后只剩「编码器B相」「电流钳采集(ABSx10)」,组仍展开;清空即恢复。
- 拆分:合并 3 条信号后右键显示「拆分当前网格(3个信号)」;单信号网格该项置灰无尾缀。
- 光标/中心轴:竖线光标取值与中心值胶囊均按四向越界正确显示三角/圆点。
- 稠密区域:5 信号全选 + 缩放到 2ms 一大格,左键拖拽信号、左键平移、中键平移均流畅;
  渲染观感与修改前等价(叠图对比)。
- make clean && make all 无警告无错误。
2026-10-09 17:07:52 +08:00

684 lines
21 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>
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;
}
}
bool UiFonts::Create(UINT targetDpi)
{
Destroy();
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 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 UiTextWidth(gd::Graphics& g, const std::wstring& text, HFONT font)
{
// 直接复用 UiMeasureTextWidth:后者用 GetDC(nullptr) 取度量 HDC,
// 不会触发 **Graphics::GetHDC() 的 GDI+ 批次刷新**(实测每次约 5µs)。
// UiEllipsize 会反复调用本函数(逐字符回退时每步一次),
// 若走 GetHDC 路线,一个长名称就要刷新十几次,是拖拽掉帧的隐性来源之一。
(void)g;
return UiMeasureTextWidth(font, text);
}
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
{
if (text.empty() || font == nullptr)
{
return 0.0f;
}
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);
return static_cast<float>(size.cx);
}
std::wstring UiEllipsize(gd::Graphics& g, const std::wstring& text, HFONT font, float maxWidth)
{
if (text.empty() || font == nullptr || maxWidth <= 0.0f)
{
return std::wstring();
}
if (UiTextWidth(g, text, font) <= maxWidth)
{
return text;
}
const std::wstring ellipsis = L"…";
const float ellipsisWidth = UiTextWidth(g, ellipsis, font);
if (ellipsisWidth > maxWidth)
{
return std::wstring();
}
std::wstring result = text;
while (!result.empty())
{
result.pop_back();
if (UiTextWidth(g, result, font) + 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;
}
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);
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);
// 选区高亮绘制在文本之下,整段选中时呈现“已全选”观感;
// 叠加上 GDI+ 裁剪,高亮同样不会越过输入框边界。
if (edit.HasSelection() && selEndX > selBeginX)
{
const gd::RectF highlight(textLeft + selBeginX, rect.Y + 3.0f * scale, selEndX - selBeginX,
rect.Height - 6.0f * scale);
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