修复列表内联编辑(组名/信号名)看不到插入符与选区;把 UI 基础层迁入 Components/Shared
1) 行位图缓存键漏了插入符与选区:信号列表整行走 PaintRowCached,RowCacheKey
在编辑态只混合“是否编辑 + 文本” ⇒ 左右方向键与鼠标框选虽然改变了 TextEdit
状态(caret/anchor),键却不变 ⇒ 贴回旧位图。表现就是“无法部分选择、
鼠标框选无反应、光标不动”,而绘图区的光标时间输入(无行缓存)一直正常。
修法:GROUP_TITLE / SIGNAL_NAME 两个分支编辑态把 Caret / SelectionBegin /
SelectionEnd 一并 mix 进键;新增 SignalPanel::EditFieldFont() 统一
绘制(uiSmall / ui)与鼠标命中、插入符换算所用字体。
2) Theme / PaintUtil / TextEdit / PopupMenu / ColorPickerPopup 由
Utilities/{Inc,Src} git mv 到 Components/Shared/{Inc,Src}:
Utilities 只保留纯数据/逻辑模块(DataModel、DataTable、各 *Loader、MatH5、
Utf8Util),Shared 成为自洽的 UI 基础层(Theme → PaintUtil → UiControls →
PopupMenu / ColorPickerPopup)。依赖方向仍是 Components → Shared → Utilities;
PaintUtil 一并迁移是因为它依赖 Theme(否则构成 Utilities → Shared 反向依赖)。
Makefile 无需改动(include 平铺,两个目录都已在搜索路径),但需清理 Build/*
里对应的旧 .o/.d。
验证(全部离屏,不占用鼠标/键盘/屏幕):
- 新增 .preflight/editcheck 20/20;A/B 修复前 6 项如实失败(键不变 + 屏幕不重绘);
- 既有 .preflight/check 137/137;
- make release / make debug 零警告。
This commit is contained in:
1 parent
f44f8021e3
commit
1e50494c41
15 files changed
+93
-4
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1,592 +0,0 @@
|
||||
#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;
|
||||
|
||||
/** 文本的像素宽度与字符单元高度(后者与 `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 `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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,417 +0,0 @@
|
||||
#include "TextEdit.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
/** @brief 判断字符是否为 UTF-16 高位代理。 */
|
||||
bool IsHighSurrogate(wchar_t c)
|
||||
{
|
||||
return c >= 0xD800 && c <= 0xDBFF;
|
||||
}
|
||||
|
||||
/** @brief 判断字符是否为 UTF-16 低位代理。 */
|
||||
bool IsLowSurrogate(wchar_t c)
|
||||
{
|
||||
return c >= 0xDC00 && c <= 0xDFFF;
|
||||
}
|
||||
|
||||
/** @brief 判断字符是否符合数值输入(数字与科学计数法符号)。 */
|
||||
bool IsNumberChar(wchar_t c)
|
||||
{
|
||||
return (c >= L'0' && c <= L'9') || c == L'.' || c == L'-' || c == L'+' || c == L'e' || c == L'E';
|
||||
}
|
||||
|
||||
/** @brief 判断字符是否为十六进制数字。 */
|
||||
bool IsHexDigit(wchar_t c)
|
||||
{
|
||||
return (c >= L'0' && c <= L'9') || (c >= L'a' && c <= L'f') || (c >= L'A' && c <= L'F');
|
||||
}
|
||||
|
||||
/** @brief 判断字符是否为十进制数字。 */
|
||||
bool IsDigit(wchar_t c)
|
||||
{
|
||||
return c >= L'0' && c <= L'9';
|
||||
}
|
||||
}
|
||||
|
||||
bool TextEdit::AcceptsCharacter(EditFilter filter, wchar_t character)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case EditFilter::NUMBER:
|
||||
return IsNumberChar(character);
|
||||
case EditFilter::HEX:
|
||||
return IsHexDigit(character) || character == L'#';
|
||||
case EditFilter::BYTE:
|
||||
return IsDigit(character);
|
||||
case EditFilter::ANY:
|
||||
default:
|
||||
return character >= L' ' && character != 0x7F;
|
||||
}
|
||||
}
|
||||
|
||||
void TextEdit::Begin(const std::wstring& text, bool selectAll, EditFilter filter, std::size_t maxLength)
|
||||
{
|
||||
m_active = true;
|
||||
m_text = text;
|
||||
m_filter = filter;
|
||||
m_maxLength = maxLength;
|
||||
m_caret = m_text.size();
|
||||
m_anchor = selectAll ? 0 : m_text.size();
|
||||
m_fresh = selectAll && !m_text.empty();
|
||||
}
|
||||
|
||||
void TextEdit::End()
|
||||
{
|
||||
m_active = false;
|
||||
m_fresh = false;
|
||||
m_dragging = false;
|
||||
m_text.clear();
|
||||
m_caret = 0;
|
||||
m_anchor = 0;
|
||||
m_filter = EditFilter::ANY;
|
||||
m_maxLength = 0;
|
||||
}
|
||||
|
||||
std::size_t TextEdit::ClampOffset(std::size_t offset) const
|
||||
{
|
||||
if (offset > m_text.size())
|
||||
{
|
||||
offset = m_text.size();
|
||||
}
|
||||
// 避免光标落在代理对中间(否则渲染与删除都会错位)。
|
||||
if (offset > 0 && offset < m_text.size() && IsLowSurrogate(m_text[offset]) &&
|
||||
IsHighSurrogate(m_text[offset - 1]))
|
||||
{
|
||||
--offset;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
std::size_t TextEdit::StepLeft(std::size_t offset) const
|
||||
{
|
||||
if (offset == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
--offset;
|
||||
if (offset > 0 && IsLowSurrogate(m_text[offset]) && IsHighSurrogate(m_text[offset - 1]))
|
||||
{
|
||||
--offset;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
std::size_t TextEdit::StepRight(std::size_t offset) const
|
||||
{
|
||||
if (offset >= m_text.size())
|
||||
{
|
||||
return m_text.size();
|
||||
}
|
||||
++offset;
|
||||
if (offset < m_text.size() && IsLowSurrogate(m_text[offset]) && IsHighSurrogate(m_text[offset - 1]))
|
||||
{
|
||||
++offset;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
bool TextEdit::ReplaceSelection(const std::wstring& replacement)
|
||||
{
|
||||
const bool hadSelection = HasSelection();
|
||||
const std::size_t begin = SelectionBegin();
|
||||
const std::size_t end = SelectionEnd();
|
||||
|
||||
std::wstring merged = m_text.substr(0, begin) + replacement + m_text.substr(end);
|
||||
if (m_maxLength > 0 && merged.size() > m_maxLength)
|
||||
{
|
||||
merged.resize(m_maxLength);
|
||||
}
|
||||
|
||||
const bool changed = (merged != m_text) || hadSelection;
|
||||
m_text = std::move(merged);
|
||||
m_caret = ClampOffset(begin + replacement.size());
|
||||
m_anchor = m_caret;
|
||||
m_fresh = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool TextEdit::Insert(wchar_t character)
|
||||
{
|
||||
if (!m_active || character == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!AcceptsCharacter(m_filter, character))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return ReplaceSelection(std::wstring(1, character));
|
||||
}
|
||||
|
||||
bool TextEdit::Backspace()
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (HasSelection())
|
||||
{
|
||||
// 存在选区时退格清除整个选区。
|
||||
return ReplaceSelection(std::wstring());
|
||||
}
|
||||
if (m_caret == 0)
|
||||
{
|
||||
m_fresh = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t previous = StepLeft(m_caret);
|
||||
const std::size_t removed = m_caret - previous;
|
||||
m_text.erase(previous, removed);
|
||||
m_caret = previous;
|
||||
m_anchor = previous;
|
||||
m_fresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextEdit::DeleteForward()
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (HasSelection())
|
||||
{
|
||||
// 存在选区时删除键清除整个选区。
|
||||
return ReplaceSelection(std::wstring());
|
||||
}
|
||||
if (m_caret >= m_text.size())
|
||||
{
|
||||
m_fresh = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t next = StepRight(m_caret);
|
||||
m_text.erase(m_caret, next - m_caret);
|
||||
m_anchor = m_caret;
|
||||
m_fresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextEdit::MoveCaret(int delta, bool extendSelection)
|
||||
{
|
||||
if (!m_active || delta == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 未按 Shift 且存在选区时,左右键先「收拢」到选区的对应边缘(常规输入框行为):
|
||||
// 左键跳到选区左端、右键跳到选区右端,而不是在光标处再移动一格。
|
||||
if (!extendSelection && HasSelection())
|
||||
{
|
||||
const std::size_t target = (delta < 0) ? SelectionBegin() : SelectionEnd();
|
||||
m_caret = target;
|
||||
m_anchor = target;
|
||||
m_fresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t offset = m_caret;
|
||||
const int steps = (delta > 0) ? delta : -delta;
|
||||
for (int index = 0; index < steps; ++index)
|
||||
{
|
||||
offset = (delta > 0) ? StepRight(offset) : StepLeft(offset);
|
||||
}
|
||||
offset = ClampOffset(offset);
|
||||
|
||||
if (offset == m_caret)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_caret = offset;
|
||||
if (!extendSelection)
|
||||
{
|
||||
m_anchor = offset;
|
||||
m_fresh = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_fresh = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextEdit::MoveHome(bool extendSelection)
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 未按 Shift 时 Home 必须收拢已有选区(例如整段选中后按 Home 应取消全选)。
|
||||
if (!extendSelection)
|
||||
{
|
||||
const bool changed = HasSelection() || (m_caret != 0);
|
||||
m_caret = 0;
|
||||
m_anchor = 0;
|
||||
m_fresh = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
if (m_caret == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_caret = 0;
|
||||
m_fresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextEdit::MoveEnd(bool extendSelection)
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 同上:End 在未按 Shift 时收拢到行尾。
|
||||
if (!extendSelection)
|
||||
{
|
||||
const bool changed = HasSelection() || (m_caret != m_text.size());
|
||||
m_caret = m_text.size();
|
||||
m_anchor = m_caret;
|
||||
m_fresh = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
if (m_caret == m_text.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_caret = m_text.size();
|
||||
m_fresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextEdit::SelectAll()
|
||||
{
|
||||
m_anchor = 0;
|
||||
m_caret = m_text.size();
|
||||
m_fresh = !m_text.empty();
|
||||
}
|
||||
|
||||
bool TextEdit::DeselectToEnd()
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool changed = HasSelection() || (m_caret != m_text.size());
|
||||
m_caret = m_text.size();
|
||||
m_anchor = m_caret;
|
||||
m_fresh = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void TextEdit::BeginDrag(std::size_t offset)
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::size_t target = ClampOffset(offset);
|
||||
m_caret = target;
|
||||
m_anchor = target;
|
||||
m_fresh = false;
|
||||
m_dragging = true;
|
||||
}
|
||||
|
||||
bool TextEdit::UpdateDrag(std::size_t offset)
|
||||
{
|
||||
if (!m_active || !m_dragging)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t target = ClampOffset(offset);
|
||||
if (target == m_caret)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_caret = target;
|
||||
m_fresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextEdit::CopyToClipboard(HWND owner) const
|
||||
{
|
||||
if (m_text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::wstring payload =
|
||||
HasSelection() ? m_text.substr(SelectionBegin(), SelectionEnd() - SelectionBegin()) : m_text;
|
||||
return CopyTextToClipboard(owner, payload);
|
||||
}
|
||||
|
||||
bool TextEdit::CutToClipboard(HWND owner)
|
||||
{
|
||||
if (m_text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const bool copied = CopyToClipboard(owner);
|
||||
if (!copied)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (HasSelection())
|
||||
{
|
||||
return ReplaceSelection(std::wstring());
|
||||
}
|
||||
m_text.clear();
|
||||
m_caret = 0;
|
||||
m_anchor = 0;
|
||||
m_fresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextEdit::PasteFromClipboard(HWND owner)
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring pasted;
|
||||
if (!PasteTextFromClipboard(owner, pasted))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 按当前过滤策略清洗粘贴内容,避免数值框被灌入非法字符。
|
||||
std::wstring cleaned;
|
||||
cleaned.reserve(pasted.size());
|
||||
for (const wchar_t c : pasted)
|
||||
{
|
||||
if (AcceptsCharacter(m_filter, c))
|
||||
{
|
||||
cleaned.push_back(c);
|
||||
}
|
||||
}
|
||||
if (cleaned.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return ReplaceSelection(cleaned);
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
@@ -1,83 +0,0 @@
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
// Solarised Light 基础色板。
|
||||
const std::uint32_t kBase03 = 0x002B36u; // #002B36
|
||||
const std::uint32_t kBase02 = 0x073642u; // #073642
|
||||
const std::uint32_t kBase01 = 0x586E75u; // #586E75
|
||||
const std::uint32_t kBase00 = 0x657B83u; // #657B83
|
||||
const std::uint32_t kBase0 = 0x839496u; // #839496
|
||||
const std::uint32_t kBase1 = 0x93A1A1u; // #93A1A1
|
||||
const std::uint32_t kBase2 = 0xEEE8D5u; // #EEE8D5
|
||||
const std::uint32_t kBase3 = 0xFDF6E3u; // #FDF6E3
|
||||
|
||||
// Solarised 强调色。
|
||||
const std::uint32_t kYellow = 0xB58900u; // #B58900
|
||||
const std::uint32_t kOrange = 0xCB4B16u; // #CB4B16
|
||||
const std::uint32_t kRed = 0xDC322Fu; // #DC322F
|
||||
const std::uint32_t kMagenta = 0xD33682u; // #D33682
|
||||
const std::uint32_t kViolet = 0x6C71C4u; // #6C71C4
|
||||
const std::uint32_t kBlue = 0x268BD2u; // #268BD2
|
||||
const std::uint32_t kCyan = 0x2AA198u; // #2AA198
|
||||
const std::uint32_t kGreen = 0x859900u; // #859900
|
||||
|
||||
/** @brief 波形默认配色,按视觉对比度排序。 */
|
||||
const std::uint32_t kSignalPalette[] = {
|
||||
MakeArgb(kBlue), MakeArgb(kGreen), MakeArgb(kRed), MakeArgb(kViolet),
|
||||
MakeArgb(kYellow), MakeArgb(kCyan), MakeArgb(kMagenta), MakeArgb(kOrange),
|
||||
MakeArgb(kBase01), MakeArgb(kBase0),
|
||||
};
|
||||
|
||||
/** @brief 全局唯一主题实例。 */
|
||||
const Theme kSolarizedLight = []
|
||||
{
|
||||
Theme theme;
|
||||
theme.windowBg = MakeArgb(kBase3);
|
||||
theme.toolbarBg = MakeArgb(kBase2);
|
||||
theme.panelBg = MakeArgb(kBase2);
|
||||
theme.cardBg = MakeArgb(kBase3);
|
||||
theme.statusBg = MakeArgb(kBase2);
|
||||
theme.plotBg = MakeArgb(kBase3);
|
||||
theme.border = MakeArgb(0xD9D1BCu);
|
||||
theme.borderStrong = MakeArgb(kBase1);
|
||||
|
||||
theme.text = MakeArgb(kBase00);
|
||||
theme.textStrong = MakeArgb(kBase02);
|
||||
theme.textMuted = MakeArgb(kBase1);
|
||||
theme.textOnAccent = MakeArgb(kBase3);
|
||||
|
||||
theme.accent = MakeArgb(kBlue);
|
||||
theme.accentSoft = MakeArgb(0xCFE4F3u);
|
||||
theme.hover = MakeArgb(0xE4DCC6u);
|
||||
theme.pressed = MakeArgb(0xD8CFB6u);
|
||||
theme.selection = MakeArgb(0xDCEAF6u);
|
||||
|
||||
// 网格线需要在绘图区底色(kBase3)上清晰可辨,同时又要与网格边界
|
||||
// (axisLine/borderStrong 的蓝灰)明显区分——因此采用偏暖的加深米色。
|
||||
theme.gridWeak = MakeArgb(0xDDD3B8u);
|
||||
theme.gridStrong = MakeArgb(0xC3B896u);
|
||||
theme.axisLine = MakeArgb(kBase1);
|
||||
theme.cursor = MakeArgb(kOrange);
|
||||
theme.readoutBg = MakeArgb(0xF6EFDCu);
|
||||
theme.danger = MakeArgb(kRed);
|
||||
theme.warning = MakeArgb(kYellow);
|
||||
return theme;
|
||||
}();
|
||||
}
|
||||
|
||||
const Theme& SolarizedLight()
|
||||
{
|
||||
return kSolarizedLight;
|
||||
}
|
||||
|
||||
const std::uint32_t* SignalPalette(int& count)
|
||||
{
|
||||
count = static_cast<int>(sizeof(kSignalPalette) / sizeof(kSignalPalette[0]));
|
||||
return kSignalPalette;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user