分组折起热区与改名输入框修正 + 数据模型去冗余(单一名称)+ 新增 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 历史附录加“时效声明”。
This commit is contained in:
1 parent
5831a0ea0d
commit
0871eb0cba
22 files changed
+1268
-1010
No files matched your search
@@ -19,7 +19,7 @@ namespace jdi
|
||||
* 各块之间的行数可以不一致(较短的时间轴在数据区提前结束,其余位置留空)。
|
||||
* 只出现一个时间列时结果与旧的单时间轴行为完全一致。
|
||||
*
|
||||
* @param [IN] filePath CSV 文件完整路径(宽字符,支持中文路径)。
|
||||
* @param [IN] filePath 数据文件完整路径(宽字符,支持中文路径)。
|
||||
* @param [OUT] source 解析结果(按时间列切分为若干时间轴块)。
|
||||
* @param [OUT] error 失败原因描述(成功时清空)。
|
||||
* @param [IN] progress 解析进度回调,可为空。
|
||||
|
||||
+31
-44
@@ -380,18 +380,16 @@ namespace jdi
|
||||
/**
|
||||
* @brief 单条信号的元数据与采样值(纯数据,不含可见性配置)。
|
||||
*
|
||||
* ⚠️ 信号**只有两个名字字段,各有明确职责**,不要再引入第三个:
|
||||
* * `name` —— 源名称(列名 / 通道名),加载后**原样保留、永不修改**;
|
||||
* * `displayName` —— 用户在界面上重命名后的名称,为空表示“用源名称”。
|
||||
* 界面展示一律走 `Title()`。
|
||||
* ⚠️ 信号**只有一个名称字段** `name` —— 它就是界面展示用的名字,也是用户重命名后写入的地方
|
||||
*(加载时取列名 / 通道名,之后不再保留“原始名称”的第二份副本:应用从不靠名称定位信号,
|
||||
* 定位一律走 `SignalRef` 下标)。
|
||||
*
|
||||
* ⚠️ 信号的时间轴由 `axisIndex` 指向所属分组的时间轴列表,
|
||||
* 因此采样值本身不需要、也不应该携带时间信息。
|
||||
*/
|
||||
struct SignalData
|
||||
{
|
||||
std::wstring name; /**< 源名称(列名 / 通道名,原样保留)。 */
|
||||
std::wstring displayName; /**< 用户重命名后的显示名;为空时使用 name。 */
|
||||
std::wstring name; /**< 信号名称(加载时取列名 / 通道名;用户可在界面上重命名)。 */
|
||||
std::size_t axisIndex = 0; /**< 所属时间轴在分组内的下标。 */
|
||||
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
||||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||||
@@ -427,12 +425,6 @@ namespace jdi
|
||||
std::size_t coarseStride = 0; /**< 分组的采样跨度;0 表示未构建。 */
|
||||
|
||||
SignalStyle style; /**< 显示外观(全局共享,跨标签页同步)。 */
|
||||
|
||||
/**
|
||||
* @brief 获取用于界面展示的名称。
|
||||
* @return 用户重命名后的显示名,未重命名时返回源名称。
|
||||
*/
|
||||
const std::wstring& Title() const { return displayName.empty() ? name : displayName; }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -447,17 +439,10 @@ namespace jdi
|
||||
*/
|
||||
struct SignalGroup
|
||||
{
|
||||
std::wstring name; /**< 分组默认名称(源文件名,含扩展名)。 */
|
||||
std::wstring displayName; /**< 用户重命名后的分组名;为空时使用 name。 */
|
||||
std::wstring name; /**< 分组名(加载时取源文件名、含扩展名;用户可重命名)。 */
|
||||
std::vector<TimeAxis> axes; /**< 该分组使用的时间轴(至少一条)。 */
|
||||
std::vector<SignalData> signals; /**< 该分组包含的信号。 */
|
||||
|
||||
/**
|
||||
* @brief 获取用于界面展示的分组名。
|
||||
* @return 用户重命名后的显示名,未重命名时返回源文件名。
|
||||
*/
|
||||
const std::wstring& Title() const { return displayName.empty() ? name : displayName; }
|
||||
|
||||
/**
|
||||
* @brief 取某条信号所属的时间轴(下标越界时回退到第一条,保证永不返回悬空引用)。
|
||||
* @param [IN] signal 目标信号。
|
||||
@@ -631,21 +616,21 @@ namespace jdi
|
||||
const SignalGroup* GroupAt(int groupIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 修改某条信号在界面上的显示名称。
|
||||
* @brief 重命名某条信号(写入它的**唯一**名称字段 `SignalData::name`)。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] signalIndex 组内信号下标。
|
||||
* @param [IN] name 新的显示名;传入空串表示恢复使用源名称。
|
||||
* @return 修改成功返回 true。
|
||||
* @param [IN] name 新名称;空串(或仅空白)忽略,避免把名称抹空。
|
||||
* @return 名称确实发生变化返回 true。
|
||||
*/
|
||||
bool SetSignalDisplayName(int groupIndex, int signalIndex, const std::wstring& name);
|
||||
bool RenameSignal(int groupIndex, int signalIndex, const std::wstring& name);
|
||||
|
||||
/**
|
||||
* @brief 修改某个分组的显示名称。
|
||||
* @brief 重命名某个分组(写入它的**唯一**名称字段 `SignalGroup::name`)。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] name 新的分组名;传入空串表示恢复使用默认名称。
|
||||
* @return 修改成功返回 true。
|
||||
* @param [IN] name 新名称;空串(或仅空白)忽略,避免把名称抹空。
|
||||
* @return 名称确实发生变化返回 true。
|
||||
*/
|
||||
bool SetGroupDisplayName(int groupIndex, const std::wstring& name);
|
||||
bool RenameGroup(int groupIndex, const std::wstring& name);
|
||||
|
||||
/**
|
||||
* @brief 解析信号引用。
|
||||
@@ -1068,22 +1053,22 @@ namespace jdi
|
||||
void SetName(const std::wstring& name) { m_name = name; }
|
||||
|
||||
/**
|
||||
* @brief 依据数据集合调整配置容量(新增文件补默认配置,移除文件清理配置)。
|
||||
* @brief 依据数据集合调整配置容量(新增分组补默认配置,移除分组清理配置)。
|
||||
* @param [IN] dataSet 数据集合。
|
||||
*/
|
||||
void SyncToDataSet(const DataSet& dataSet);
|
||||
|
||||
/**
|
||||
* @brief 在数据层移除某文件之前,按旧下标修正本配置(不重建网格)。
|
||||
* @brief 在数据层移除某分组之前,按旧下标修正本配置(不重建网格)。
|
||||
* @param [IN] groupIndex 即将被移除的分组下标。
|
||||
* @note 调用顺序必须为:各标签页 RemapAfterGroupRemoval → 数据层 RemoveFile → 各标签页 SyncToDataSet。
|
||||
* @note 调用顺序必须为:各标签页 RemapAfterGroupRemoval → 数据层 RemoveGroup → 各标签页 SyncToDataSet。
|
||||
*/
|
||||
void RemapAfterGroupRemoval(int groupIndex);
|
||||
|
||||
/**
|
||||
* @brief 在数据层移除某条信号之前,按旧下标修正本配置(不重建网格)。
|
||||
*
|
||||
* 会删除该信号的可见性记录与网格归属,并把同一文件内位于其后的引用下标前移一位。
|
||||
* 会删除该信号的可见性记录与网格归属,并把同一分组内位于其后的引用下标前移一位。
|
||||
* 若某个网格因此变空则一并移除(与“取消勾选后空网格消失”的策略一致)。
|
||||
*
|
||||
* @param [IN] groupIndex 信号所属分组下标。
|
||||
@@ -1114,7 +1099,7 @@ namespace jdi
|
||||
void ToggleVisible(const SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 设置某个文件全部信号的可见性,并同步网格划分。
|
||||
* @brief 设置某个分组全部信号的可见性,并同步网格划分。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @param [IN] visible 目标可见状态。
|
||||
*/
|
||||
@@ -1133,14 +1118,14 @@ namespace jdi
|
||||
void SoloSignal(const SignalRef& ref);
|
||||
|
||||
/**
|
||||
* @brief 判断某文件是否存在可见信号。
|
||||
* @brief 判断某分组是否存在可见信号。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @return 存在可见信号返回 true。
|
||||
*/
|
||||
bool GroupHasVisible(int groupIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 判断某文件是否全部信号可见。
|
||||
* @brief 判断某分组是否全部信号可见。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @return 全部可见且至少有一条信号返回 true。
|
||||
*/
|
||||
@@ -1215,10 +1200,11 @@ namespace jdi
|
||||
*/
|
||||
int SplitLane(int laneIndex);
|
||||
|
||||
/** * @brief 在数据层插入新信号之后,修正本页引用该文件的信号下标。
|
||||
/**
|
||||
* @brief 在数据层插入新信号之后,修正本页引用该分组的信号下标。
|
||||
*
|
||||
* 数据层在下标 `insertPos` 处插入了一条信号,因此本页可见性表需插入一列,
|
||||
* 且所有指向该文件、下标不小于 `insertPos` 的信号引用都要后移一位。
|
||||
* 且所有指向该分组、下标不小于 `insertPos` 的信号引用都要后移一位。
|
||||
*
|
||||
* @param [IN] groupIndex 发生插入的分组下标。
|
||||
* @param [IN] insertPos 新信号的下标。
|
||||
@@ -1255,20 +1241,21 @@ namespace jdi
|
||||
*/
|
||||
void RenameLanes();
|
||||
|
||||
/** * @brief 查询某文件的分组是否处于折叠状态。
|
||||
/**
|
||||
* @brief 查询某分组的折叠状态。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
* @return 折叠状态。
|
||||
*/
|
||||
bool IsCollapsed(int groupIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 切换某文件分组的折叠状态。
|
||||
* @brief 切换某分组的折叠状态。
|
||||
* @param [IN] groupIndex 分组下标。
|
||||
*/
|
||||
void ToggleCollapsed(int groupIndex);
|
||||
|
||||
/**
|
||||
* @brief 设置「新出现的文件分组是否默认折起」。
|
||||
* @brief 设置「新出现的分组是否默认折起」。
|
||||
*
|
||||
* 只影响**之后**新出现的分组(例如新加载的文件);已存在的分组保持用户当前的选择。
|
||||
* 用户新建的标签页会把它设为 true,使信号列表默认紧凑。
|
||||
@@ -1278,7 +1265,7 @@ namespace jdi
|
||||
void SetGroupsCollapsedByDefault(bool collapsed) { m_groupsCollapsedByDefault = collapsed; }
|
||||
|
||||
/**
|
||||
* @brief 把当前所有文件分组设为折叠或展开。
|
||||
* @brief 把当前所有分组设为折叠或展开。
|
||||
* @param [IN] collapsed true = 全部折起,false = 全部展开。
|
||||
*/
|
||||
void SetAllCollapsed(bool collapsed)
|
||||
@@ -1339,10 +1326,10 @@ namespace jdi
|
||||
|
||||
private:
|
||||
std::wstring m_name; /**< 标签页名称。 */
|
||||
std::vector<std::vector<bool>> m_visible; /**< 信号可见性,按 [文件][信号] 索引(本页独立)。 */
|
||||
std::vector<bool> m_collapsed; /**< 文件分组折叠状态,按文件索引。 */
|
||||
std::vector<std::vector<bool>> m_visible; /**< 信号可见性,按 [分组][信号] 索引(本页独立)。 */
|
||||
std::vector<bool> m_collapsed; /**< 分组折叠状态,按分组下标。 */
|
||||
/**
|
||||
* @brief 新出现的(首次见到的)文件分组是否默认折起。
|
||||
* @brief 新出现的(首次见到的)分组是否默认折起。
|
||||
*
|
||||
* 用户新建的标签页把它设为 true(信号列表默认紧凑);启动时的首个标签页保持 false,
|
||||
* 这样加载完文件就能直接勾选信号。
|
||||
|
||||
@@ -196,27 +196,6 @@ namespace jdi
|
||||
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);
|
||||
|
||||
/**
|
||||
* @brief 计算输入框文本的绘制起点(已考虑超出框宽时的水平滚动)。
|
||||
* @param [IN] rect 输入框矩形。
|
||||
* @param [IN] edit 编辑模型。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] scale 缩放比例。
|
||||
* @param [OUT] innerWidth 框内可用宽度输出。
|
||||
* @return 文本绘制起点横坐标。
|
||||
*/
|
||||
float UiEditTextLeft(const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale,
|
||||
float& innerWidth);
|
||||
|
||||
/**
|
||||
* @brief 把输入框内的横坐标换算为字符偏移(就近取整,用于鼠标定位插入符)。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] text 当前文本。
|
||||
* @param [IN] localX 相对文本起点的横坐标(可超出 [0, 文本宽度])。
|
||||
* @return 字符偏移。
|
||||
*/
|
||||
std::size_t UiEditOffsetAt(HFONT font, const std::wstring& text, float localX);
|
||||
|
||||
/**
|
||||
* @brief 在超出可用宽度时对文本做尾部省略截断(不绘制任何内容)。
|
||||
* @param [IN] text 原始文本。
|
||||
@@ -226,38 +205,6 @@ namespace jdi
|
||||
*/
|
||||
std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth);
|
||||
|
||||
/**
|
||||
* @brief 处理输入框内的鼠标按下:命中则把插入符定位到最近的字符边界并开始拖拽框选。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] rect 输入框矩形。
|
||||
* @param [IN,OUT] edit 编辑模型。
|
||||
* @param [IN] scale 缩放比例。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 命中输入框并开始拖拽返回 true。
|
||||
*/
|
||||
bool UiEditFieldMouseDown(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理输入框内的鼠标拖拽移动(扩展选区)。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] rect 输入框矩形。
|
||||
* @param [IN,OUT] edit 编辑模型。
|
||||
* @param [IN] scale 缩放比例。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @return 选区发生变化返回 true。
|
||||
*/
|
||||
bool UiEditFieldMouseMove(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x);
|
||||
|
||||
/**
|
||||
* @brief 在矩形中心绘制一个加号图标。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 图标布局矩形。
|
||||
* @param [IN] argb 线条颜色。
|
||||
* @param [IN] strokeWidth 线条宽度。
|
||||
*/
|
||||
void UiDrawPlusIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth);
|
||||
|
||||
/**
|
||||
* @brief 用 GDI 一次性绘制若干条**不透明 1px 实线**(每条线段相互独立)。
|
||||
*
|
||||
@@ -311,138 +258,6 @@ namespace jdi
|
||||
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
|
||||
std::uint32_t argb);
|
||||
|
||||
/**
|
||||
* @brief 在矩形中心绘制一个叉号(关闭)图标。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 图标布局矩形。
|
||||
* @param [IN] argb 线条颜色。
|
||||
* @param [IN] strokeWidth 线条宽度。
|
||||
*/
|
||||
void UiDrawCrossIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth);
|
||||
|
||||
/**
|
||||
* @brief 在矩形中心绘制一个实心三角形指示器。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 图标布局矩形。
|
||||
* @param [IN] argb 填充颜色。
|
||||
* @param [IN] direction 三角形朝向。
|
||||
*/
|
||||
void UiDrawTriangleIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, IconDirection direction);
|
||||
|
||||
/**
|
||||
* @brief 将滑块轨道上的横坐标换算为取值比例。
|
||||
* @param [IN] trackRect 轨道矩形。
|
||||
* @param [IN] x 横坐标。
|
||||
* @return 取值比例,范围 0-1。
|
||||
*/
|
||||
double UiSliderRatioFromX(const gd::RectF& trackRect, float x);
|
||||
|
||||
/**
|
||||
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] track 轨道矩形。
|
||||
* @param [IN] ratio 当前值比例(0-1)。
|
||||
* @param [IN] fillColor 已填充部分颜色。
|
||||
* @param [IN] active 是否处于悬停或拖拽状态(影响圆钮半径)。
|
||||
* @param [IN] scale DPI 缩放比例。
|
||||
*/
|
||||
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||||
bool active, float scale);
|
||||
|
||||
/**
|
||||
* @brief 绘制单行内联输入框(含选区高亮、插入符与水平滚动)。
|
||||
*
|
||||
* 初始状态为“整体选中”时以强调色底衬显示全部内容,首字符键入即整体替换,
|
||||
* 交互观感与常见登录界面的账号/密码输入框一致;文本超出框宽时自动横向滚动,
|
||||
* 保证插入符始终可见。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 输入框矩形。
|
||||
* @param [IN] edit 输入框编辑模型。
|
||||
* @param [IN] font 文本字体。
|
||||
* @param [IN] scale DPI 缩放比例。
|
||||
*/
|
||||
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
|
||||
float scale);
|
||||
|
||||
/**
|
||||
* @brief 只绘制输入框的**内容**(选区高亮 / 文本 / 插入符),不绘制底板与边框。
|
||||
*
|
||||
* 用于"宿主已经画好了外框"的场景(例如信号面板的「筛选信号」输入框):若直接调用
|
||||
* UiDrawEditField,会在外框内部再套一个带边框的圆角框,形成一圈多余的内圈。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 输入框矩形(内容区,几何与 UiDrawEditField 完全一致)。
|
||||
* @param [IN] edit 输入框编辑模型。
|
||||
* @param [IN] font 文本字体。
|
||||
* @param [IN] scale DPI 缩放比例。
|
||||
*/
|
||||
void UiDrawEditFieldContent(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font,
|
||||
float scale);
|
||||
|
||||
/**
|
||||
* @brief 取值胶囊底色上的高对比度文字色与描边色。
|
||||
*/
|
||||
struct UiInk
|
||||
{
|
||||
std::uint32_t text = 0xFF073642u; /**< 文字色(不透明)。 */
|
||||
std::uint32_t outline = 0x99FDF6E3u; /**< 描边色(半透明,恒与文字色反相)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 颜色的相对亮度(0-1,逐通道线性加权)。
|
||||
* @param [IN] color 0xAARRGGBB 颜色值。
|
||||
* @return 相对亮度。
|
||||
*/
|
||||
double UiColorLuminance(std::uint32_t color);
|
||||
|
||||
/**
|
||||
* @brief 依据胶囊底色求高对比度的文字色与描边色(对底色配色作线性响应)。
|
||||
*
|
||||
* 求值分两步:
|
||||
* 1. **亮度**:按"取最大对比"的一侧选定亮文字或暗文字。这是唯一的阈值判断,但它
|
||||
* 本身就是"任意底色下都保持高对比"的必要条件 —— 任何连续的亮度取值都会在中性灰
|
||||
* 附近同时远离两侧极值,对比度必然塌陷。
|
||||
* 2. **配色**:把该侧的极值色与**底色的互补色**按固定比例线性混合。互补色的相对亮度
|
||||
* 恰为 `1 - 底色亮度`,因此文字/描边的亮度是底色亮度的**线性**函数,色相也随信号
|
||||
* 配色连续变化(不再是固定的黑/白两色)。
|
||||
*
|
||||
* 描边色取相反的一侧,保证"描边—文字—底色"三段式对比。
|
||||
*
|
||||
* @param [IN] background 胶囊底色(0xAARRGGBB)。
|
||||
* @return 文字色与描边色。
|
||||
*/
|
||||
UiInk UiContrastInk(std::uint32_t background);
|
||||
|
||||
/**
|
||||
* @brief 绘制一个带边框的复选框。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 复选框矩形。
|
||||
* @param [IN] state 选中状态:0 未选,1 全选,2 部分选。
|
||||
* @param [IN] radius 圆角半径。
|
||||
*/
|
||||
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius);
|
||||
|
||||
/**
|
||||
* @brief 绘制颜色样块:叠加不透明度填充,透明度过低时补充描边以保证可辨识。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 样块矩形。
|
||||
* @param [IN] color 基础颜色(0xAARRGGBB,其 Alpha 通道会被不透明度覆盖)。
|
||||
* @param [IN] opacity 不透明度(0-1)。
|
||||
* @param [IN] radius 圆角半径。
|
||||
* @param [IN] scale DPI 缩放比例,用于推导描边粗细。
|
||||
* @return 实际填充颜色的 0xAARRGGBB 值(供需要同步其他元素的调用方使用)。
|
||||
*/
|
||||
std::uint32_t UiDrawSwatch(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float opacity,
|
||||
float radius, float scale);
|
||||
|
||||
/**
|
||||
* @brief 判断给定不透明度是否低到需要为样块补充描边。
|
||||
* @param [IN] opacity 不透明度(0-1)。
|
||||
* @return 需要描边返回 true。
|
||||
*/
|
||||
bool UiSwatchNeedsOutline(float opacity);
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __PAINT_UTIL_HPP__
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ColorPickerPopup.hpp"
|
||||
|
||||
#include "UiControls.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
+20
-12
@@ -40,7 +40,7 @@ namespace jdi
|
||||
|
||||
/**
|
||||
* @brief 依据全部可见信号重新生成网格划分。
|
||||
* @param [IN] visible 各信号的可见性(按 [文件][信号] 索引)。
|
||||
* @param [IN] visible 各信号的可见性(按 [分组][信号] 索引)。
|
||||
* @param [IN] lanes 网格集合(原地更新)。
|
||||
* @param [IN] mode 网格划分策略。
|
||||
*/
|
||||
@@ -865,7 +865,7 @@ namespace jdi
|
||||
return &m_groups[static_cast<std::size_t>(groupIndex)];
|
||||
}
|
||||
|
||||
bool DataSet::SetSignalDisplayName(int groupIndex, int signalIndex, const std::wstring& name)
|
||||
bool DataSet::RenameSignal(int groupIndex, int signalIndex, const std::wstring& name)
|
||||
{
|
||||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||||
{
|
||||
@@ -878,18 +878,22 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 统一去除首尾空白;结果为空则视为恢复源名称。
|
||||
// 统一去除首尾空白;结果为空则忽略(名称是信号的唯一标识性字段,不允许抹空)。
|
||||
std::wstring trimmed = Trim(name);
|
||||
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
|
||||
if (trimmed == signal.displayName)
|
||||
if (trimmed.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
signal.displayName = std::move(trimmed);
|
||||
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
|
||||
if (trimmed == signal.name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
signal.name = std::move(trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::SetGroupDisplayName(int groupIndex, const std::wstring& name)
|
||||
bool DataSet::RenameGroup(int groupIndex, const std::wstring& name)
|
||||
{
|
||||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||||
{
|
||||
@@ -897,12 +901,16 @@ namespace jdi
|
||||
}
|
||||
|
||||
std::wstring trimmed = Trim(name);
|
||||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||||
if (trimmed == group.displayName)
|
||||
if (trimmed.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
group.displayName = std::move(trimmed);
|
||||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||||
if (trimmed == group.name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
group.name = std::move(trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1135,7 +1143,7 @@ namespace jdi
|
||||
* `current` / `next`(float,各 ≈103MB)、极值表与位移表(≈数百 MB),
|
||||
* 于是**每条线程常驻数百 MB、4 条线程就是 2GB 以上**,而 `thread_local`
|
||||
* 只在**线程退出**时才析构 → 用户报告"启用预览后内存 1.42GB→4.21GB,
|
||||
* 关掉预览也不下降,直到关闭整个文件分组才释放"。
|
||||
* 关掉预览也不下降,直到关闭整个分组才释放"。
|
||||
*
|
||||
* 因此只对"中/小规模"(≤ 上限)保留容量;**超大计算**改为一次性分配、用完即释放 ——
|
||||
* 这类整段计算本身是秒级的一次性操作(结果会被曲线缓存复用),多一次分配完全可接受,
|
||||
@@ -1978,7 +1986,7 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 网格归属:删掉该信号本身,并把同一文件内位于其后的引用下标前移一位。
|
||||
// 网格归属:删掉该信号本身,并把同一分组内位于其后的引用下标前移一位。
|
||||
for (Lane& lane : m_lanes)
|
||||
{
|
||||
std::vector<SignalRef>& refs = lane.signals;
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace jdi
|
||||
* 不透明度低于该值时,色块自身与浅色背景的对比度已不足,需要额外描边才能辨识;
|
||||
* 高于该值时按需求不绘制描边,保持与预设色块一致的纯净外观。
|
||||
*/
|
||||
const float kSwatchOutlineOpacity = 0.45f;
|
||||
|
||||
/**
|
||||
* @brief 矩形面积超过该阈值(逻辑像素²)且为不透明色时,改用 GDI 填充。
|
||||
@@ -530,234 +529,6 @@ namespace jdi
|
||||
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)
|
||||
{
|
||||
@@ -797,157 +568,4 @@ namespace jdi
|
||||
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
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PopupMenu.hpp"
|
||||
|
||||
#include "UiControls.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
|
||||
Reference in new issue
Block a user