修复列表内联编辑(组名/信号名)看不到插入符与选区;把 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
@@ -0,0 +1,335 @@
|
||||
#ifndef __COLOR_PICKER_POPUP_HPP__
|
||||
#define __COLOR_PICKER_POPUP_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 浮动式 RGB 自定义颜色拾取器。
|
||||
*
|
||||
* 与弹出菜单一致,不创建独立窗口,而是作为宿主绘制的最后一步直接画到后备缓冲区,
|
||||
* 以浮动面板的形式出现在触发控件附近;取值变化实时生效,点击面板外部即收起。
|
||||
*/
|
||||
class ColorPickerPopup
|
||||
{
|
||||
public:
|
||||
ColorPickerPopup();
|
||||
|
||||
/**
|
||||
* @brief 绑定字体与主题资源。
|
||||
* @param [IN] fonts 字体集合,需在弹出面板生命周期内保持有效。
|
||||
* @param [IN] theme 主题配色,需在弹出面板生命周期内保持有效。
|
||||
*/
|
||||
void Setup(const UiFonts* fonts, const Theme* theme);
|
||||
|
||||
/**
|
||||
* @brief 设置 DPI 缩放比例。
|
||||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||||
*/
|
||||
void SetScale(float scale);
|
||||
|
||||
/**
|
||||
* @brief 在指定位置打开拾色面板。
|
||||
* @param [IN] bounds 宿主客户区矩形,用于把面板限制在窗口内。
|
||||
* @param [IN] anchor 触发控件矩形(客户区坐标),面板会紧贴其下方或上方展开。
|
||||
* @param [IN] color 初始颜色(0xAARRGGBB,仅取 RGB 分量)。
|
||||
*/
|
||||
void Open(const gd::RectF& bounds, const gd::RectF& anchor, std::uint32_t color);
|
||||
|
||||
/**
|
||||
* @brief 关闭拾色面板。
|
||||
*/
|
||||
void Close();
|
||||
|
||||
/**
|
||||
* @brief 判断拾色面板是否处于打开状态。
|
||||
* @return 打开返回 true。
|
||||
*/
|
||||
bool IsOpen() const { return m_open; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前取色结果。
|
||||
* @return 0xAARRGGBB 颜色值(Alpha 固定为不透明)。
|
||||
*/
|
||||
std::uint32_t CurrentColor() const;
|
||||
|
||||
/**
|
||||
* @brief 取出并清除“取值已变化”标记。
|
||||
* @param [OUT] color 变化后的颜色。
|
||||
* @return 自上次调用以来取值发生过变化返回 true。
|
||||
*/
|
||||
bool TakeChanged(std::uint32_t& color);
|
||||
|
||||
/**
|
||||
* @brief 绘制拾色面板(应在宿主绘制的最后一步调用)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void Paint(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标移动(含滑块拖拽)。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnMouseMove(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键按下。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 事件已被消费返回 true。
|
||||
*/
|
||||
bool OnLButtonDown(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键抬起。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnLButtonUp();
|
||||
|
||||
/**
|
||||
* @brief 处理键盘按键(Esc 收起、回车提交输入)。
|
||||
* @param [IN] key 虚拟键码。
|
||||
* @return 事件已被消费返回 true。
|
||||
*/
|
||||
bool OnKeyDown(WPARAM key);
|
||||
|
||||
/**
|
||||
* @brief 处理字符输入。
|
||||
* @param [IN] character 输入字符('\r' 提交、'\b' 退格、0x03 复制)。
|
||||
* @return 界面状态发生变化返回 true。
|
||||
*/
|
||||
bool OnChar(wchar_t character);
|
||||
|
||||
/**
|
||||
* @brief 判断面板是否处于内联文本输入状态。
|
||||
* @return 正在输入返回 true。
|
||||
*/
|
||||
bool IsEditing() const { return m_editField != EditField::NONE; }
|
||||
|
||||
/**
|
||||
* @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。
|
||||
* @return 发生改变返回 true;不在编辑态返回 false。
|
||||
*/
|
||||
bool CollapseEditSelectionToEnd();
|
||||
|
||||
/**
|
||||
* @brief 编辑态下在输入框内按下鼠标左键:定位插入符并开始拖拽框选。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 命中输入框返回 true。
|
||||
*/
|
||||
bool OnEditMouseDown(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 编辑态下拖拽鼠标:扩展选区。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @return 选区发生变化返回 true。
|
||||
*/
|
||||
bool OnEditMouseMove(int x);
|
||||
|
||||
/**
|
||||
* @brief 编辑态下松开鼠标左键:结束拖拽框选。
|
||||
* @return 之前处于拖拽中返回 true。
|
||||
*/
|
||||
bool OnEditMouseUp();
|
||||
|
||||
/**
|
||||
* @brief 设置剪贴板所有者窗口句柄(用于输入框的复制/粘贴)。
|
||||
* @param [IN] owner 宿主窗口句柄,允许为空。
|
||||
*/
|
||||
void SetOwnerWindow(HWND owner) { m_owner = owner; }
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在拾色面板内部。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在内部返回 true。
|
||||
*/
|
||||
bool ContainsPoint(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 查询该坐标应使用的系统光标。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 系统光标句柄;面板未打开时返回空指针。
|
||||
*/
|
||||
HCURSOR QueryCursor(int x, int y) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 面板内可进行内联文本输入的字段。
|
||||
*/
|
||||
enum class EditField : int
|
||||
{
|
||||
NONE = 0, /**< 未处于编辑状态。 */
|
||||
R = 1, /**< 红色分量(0-255)。 */
|
||||
G = 2, /**< 绿色分量(0-255)。 */
|
||||
B = 3, /**< 蓝色分量(0-255)。 */
|
||||
HEX = 4 /**< 十六进制色号。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 面板内正在拖拽的控件。
|
||||
*/
|
||||
enum class DragTarget : int
|
||||
{
|
||||
NONE = 0, /**< 未拖拽。 */
|
||||
SLIDER = 1, /**< RGB 分量滑块(具体下标见 `m_draggingSlider`)。 */
|
||||
GAMUT = 2, /**< 色域方块(饱和度-明度平面)。 */
|
||||
HUE = 3 /**< 色相条。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 依据锚点矩形与客户区计算面板矩形与内部控件矩形。
|
||||
* @param [IN] bounds 宿主客户区矩形。
|
||||
* @param [IN] anchor 触发控件矩形。
|
||||
*/
|
||||
void BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor);
|
||||
|
||||
/**
|
||||
* @brief 进入指定字段的内联输入状态。
|
||||
* @param [IN] field 目标字段。
|
||||
* @return 状态发生变化返回 true。
|
||||
*/
|
||||
bool BeginEdit(EditField field);
|
||||
|
||||
/**
|
||||
* @brief 结束内联输入状态。
|
||||
* @param [IN] commit 是否提交输入值。
|
||||
* @return 状态发生变化返回 true。
|
||||
*/
|
||||
bool EndEdit(bool commit);
|
||||
|
||||
/**
|
||||
* @brief 计算当前输入框的矩形。
|
||||
* @return 输入框矩形。
|
||||
*/
|
||||
gd::RectF EditFieldRect() const;
|
||||
|
||||
/**
|
||||
* @brief 把“文本区矩形”扩成**编辑态输入框**矩形(向外扩出输入框的内边距)。
|
||||
*
|
||||
* ⚠️ 数值区正好 3 个字符宽、色号正好 6+1 个字符宽,若直接拿文本区当输入框,
|
||||
* 输入框内部的 6px 内边距会把最后一个字符裁掉。
|
||||
*
|
||||
* @param [IN] textRect 文本区矩形(数值区或色号区)。
|
||||
* @return 输入框矩形。
|
||||
*/
|
||||
gd::RectF PaddedEditRect(const gd::RectF& textRect) const;
|
||||
|
||||
/**
|
||||
* @brief 查询坐标命中的滑块下标。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 滑块下标(0=R,1=G,2=B),未命中返回 -1。
|
||||
*/
|
||||
int SliderAtPoint(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 计算通道标签(R/G/B)的矩形。
|
||||
*
|
||||
* 标签**右边界**到轨道左端的距离与轨道右端到数值区的距离相等
|
||||
* (用户要求:滑块两侧留白一致,三个字母纵向对齐)。
|
||||
*
|
||||
* @param [IN] index 通道下标(0=R,1=G,2=B)。
|
||||
* @return 标签矩形;下标非法时返回空矩形。
|
||||
*/
|
||||
gd::RectF ChannelLabelRect(int index) const;
|
||||
|
||||
/**
|
||||
* @brief 应用滑块拖拽位置到对应通道。
|
||||
* @param [IN] index 滑块下标。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
*/
|
||||
void ApplySlider(int index, float x);
|
||||
|
||||
/**
|
||||
* @brief 在色域方块内按下/拖拽:按位置取出饱和度与明度。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
*/
|
||||
void ApplyGamut(float x, float y);
|
||||
|
||||
/**
|
||||
* @brief 在色相条内按下/拖拽:按纵向位置取出色相。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
*/
|
||||
void ApplyHue(float y);
|
||||
|
||||
/**
|
||||
* @brief 当前有效色相(0-1)。灰度/黑色时 HSV 的色相无定义,沿用上一次的有效值。
|
||||
* @return 色相。
|
||||
*/
|
||||
double CurrentHue() const;
|
||||
|
||||
/**
|
||||
* @brief RGB 分量变化后刷新派生的色相与饱和度/明度选择状态。
|
||||
*/
|
||||
void RefreshSelectionFromChannels();
|
||||
|
||||
/**
|
||||
* @brief 销毁色域方块缓存位图(尺寸或色相变化时重建)。
|
||||
*/
|
||||
void InvalidateGamutCache();
|
||||
|
||||
/**
|
||||
* @brief 确保色域方块缓存与当前色相/缩放匹配(必要时重建)。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
bool EnsureGamutCache();
|
||||
|
||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||
HWND m_owner = nullptr; /**< 剪贴板所有者窗口句柄。 */
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
|
||||
bool m_open = false; /**< 是否处于打开状态。 */
|
||||
bool m_changed = false; /**< 取值是否发生变化。 */
|
||||
int m_draggingSlider = -1; /**< 正在拖拽的滑块下标,-1 表示未拖拽。 */
|
||||
int m_hoverSlider = -1; /**< 当前悬停的滑块下标。 */
|
||||
DragTarget m_drag = DragTarget::NONE; /**< 正在拖拽的控件。 */
|
||||
std::array<double, 3> m_channels = {0.0, 0.0, 0.0}; /**< RGB 分量(0-1,唯一数据源)。 */
|
||||
double m_hue = 0.0; /**< 记忆色相(0-1);灰度/黑色时保留上次取值。 */
|
||||
/**
|
||||
* @brief 色域方块上的选点位置(饱和度/明度,均为 0-1)。
|
||||
*
|
||||
* 必须**与 RGB 分开保存**:黑色/灰色的饱和度无定义,若每帧都从 RGB 反推,
|
||||
* 把选点拖到方块右边界之外(明度归零)时选点会瞬间弹回最左侧。
|
||||
*/
|
||||
double m_sat = 0.0;
|
||||
double m_val = 0.0;
|
||||
|
||||
gd::RectF m_rect; /**< 面板整体矩形。 */
|
||||
gd::RectF m_titleRect; /**< 标题区域。 */
|
||||
std::array<gd::RectF, 3> m_tracks; /**< 滑块轨道矩形。 */
|
||||
std::array<gd::RectF, 3> m_values; /**< 数值文本矩形。 */
|
||||
gd::RectF m_previewRect; /**< 预览色块矩形。 */
|
||||
gd::RectF m_hexRect; /**< 十六进制文本矩形。 */
|
||||
gd::RectF m_gamutRect; /**< 色域方块矩形(横轴 S,纵轴 V)。 */
|
||||
gd::RectF m_hueRect; /**< 色相条矩形(纵轴色相)。 */
|
||||
|
||||
/** 色域方块缓存位图:按当前色相预渲染,避免每帧重画数十条渐变。 */
|
||||
std::unique_ptr<gd::Bitmap> m_gamutCache;
|
||||
double m_gamutCacheHue = -1.0; /**< 缓存对应的色相。 */
|
||||
int m_gamutCacheSize = 0; /**< 缓存位图边长(像素)。 */
|
||||
|
||||
EditField m_editField = EditField::NONE; /**< 当前处于输入状态的字段。 */
|
||||
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __COLOR_PICKER_POPUP_HPP__
|
||||
@@ -0,0 +1,263 @@
|
||||
#ifndef __PAINT_UTIL_HPP__
|
||||
#define __PAINT_UTIL_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "TextEdit.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 文本水平对齐方式(垂直方向始终居中)。
|
||||
*/
|
||||
enum class TextAlign : int
|
||||
{
|
||||
LEFT = 0, /**< 左对齐。 */
|
||||
CENTER = 1, /**< 水平居中。 */
|
||||
RIGHT = 2 /**< 右对齐。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 图标朝向。
|
||||
*/
|
||||
enum class IconDirection : int
|
||||
{
|
||||
LEFT = 0, /**< 指向左侧。 */
|
||||
RIGHT = 1, /**< 指向右侧。 */
|
||||
UP = 2, /**< 指向上方。 */
|
||||
DOWN = 3 /**< 指向下方。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 按 DPI 创建并缓存整套界面字体(GDI 字体句柄)。
|
||||
*/
|
||||
struct UiFonts
|
||||
{
|
||||
HFONT ui = nullptr; /**< 常规界面字体。 */
|
||||
HFONT uiBold = nullptr; /**< 加粗界面字体。 */
|
||||
HFONT uiSmall = nullptr; /**< 小号界面字体。 */
|
||||
HFONT uiSmallBold = nullptr;/**< 小号加粗界面字体。 */
|
||||
HFONT uiTiny = nullptr; /**< 微型界面字体(图例文字:必须能放进 GX 徽标同款的托底)。 */
|
||||
HFONT title = nullptr; /**< 标题字体。 */
|
||||
HFONT mono = nullptr; /**< 等宽数值字体。 */
|
||||
HFONT monoSmall = nullptr; /**< 小号等宽字体。 */
|
||||
UINT dpi = 96; /**< 创建时所使用的 DPI。 */
|
||||
|
||||
/**
|
||||
* @brief 依据 DPI 创建全部字体。
|
||||
* @param [IN] dpi 目标 DPI(96 对应 100% 缩放)。
|
||||
* @return 创建成功返回 true。
|
||||
*/
|
||||
bool Create(UINT dpi);
|
||||
|
||||
/**
|
||||
* @brief 释放全部字体句柄。
|
||||
*/
|
||||
void Destroy();
|
||||
|
||||
~UiFonts() { Destroy(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 将 0xAARRGGBB 颜色值转换为 GDI COLORREF。
|
||||
* @param [IN] argb 颜色值。
|
||||
* @return GDI 颜色值。
|
||||
*/
|
||||
inline COLORREF ToColorRef(std::uint32_t argb)
|
||||
{
|
||||
return RGB((argb >> 16) & 0xFFu, (argb >> 8) & 0xFFu, argb & 0xFFu);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断点是否落在矩形内部。
|
||||
* @param [IN] rect 目标矩形。
|
||||
* @param [IN] x 横坐标。
|
||||
* @param [IN] y 纵坐标。
|
||||
* @return 落在内部返回 true。
|
||||
*/
|
||||
inline bool UiPointInRect(const gd::RectF& rect, float x, float y)
|
||||
{
|
||||
return x >= rect.X && x <= (rect.X + rect.Width) && y >= rect.Y && y <= (rect.Y + rect.Height);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 填充矩形区域。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 目标矩形。
|
||||
* @param [IN] argb 填充颜色。
|
||||
*/
|
||||
void UiFillRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb);
|
||||
|
||||
/**
|
||||
* @brief 填充圆角矩形区域。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 目标矩形。
|
||||
* @param [IN] radius 圆角半径。
|
||||
* @param [IN] argb 填充颜色。
|
||||
*/
|
||||
void UiFillRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb);
|
||||
|
||||
/**
|
||||
* @brief 描边圆角矩形。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 目标矩形。
|
||||
* @param [IN] radius 圆角半径。
|
||||
* @param [IN] argb 线条颜色。
|
||||
* @param [IN] width 线条宽度。
|
||||
*/
|
||||
void UiDrawRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb, float width);
|
||||
|
||||
/**
|
||||
* @brief 构造圆角矩形路径(与 `UiFillRoundRect` / `UiDrawRoundRect` 使用完全相同的几何)。
|
||||
*
|
||||
* 需要把一张位图 / 渐变**裁剪进圆角形状**时使用:
|
||||
* `g.SetClip(&path)` 之后再填充,就不会在圆角之外漏出直角矩形的角。
|
||||
*
|
||||
* @param [IN,OUT] path 目标路径对象(不会被清空,直接追加)。
|
||||
* @param [IN] rect 目标矩形。
|
||||
* @param [IN] radius 圆角半径。
|
||||
*/
|
||||
void UiBuildRoundRectPath(gd::GraphicsPath& path, const gd::RectF& rect, float radius);
|
||||
|
||||
/**
|
||||
* @brief 绘制一条直线。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] x1 起点横坐标。
|
||||
* @param [IN] y1 起点纵坐标。
|
||||
* @param [IN] x2 终点横坐标。
|
||||
* @param [IN] y2 终点纵坐标。
|
||||
* @param [IN] argb 线条颜色。
|
||||
* @param [IN] width 线条宽度。
|
||||
*/
|
||||
void UiDrawLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb, float width);
|
||||
|
||||
/**
|
||||
* @brief 在指定矩形内绘制单行文本(垂直居中,超长自动省略)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] text 文本内容。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] rect 文本布局矩形。
|
||||
* @param [IN] argb 文本颜色。
|
||||
* @param [IN] align 水平对齐方式。
|
||||
*/
|
||||
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
|
||||
TextAlign align = TextAlign::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 在**显式裁剪矩形**内绘制单行文本(除裁剪范围外与 `UiText` 完全一致)。
|
||||
*
|
||||
* `UiText` 走 `DrawTextW`,而 **GDI 文本不遵循 GDI+ 的 `SetClip`**:滚动类列表在滚出
|
||||
* 视口的行上仍会把文字画到相邻区域(例如把组名/信号名画到上方筛选框上)。
|
||||
* 本函数在同一个 HDC 上做 `SaveDC` + `IntersectClipRect` 并在结束后完整还原,
|
||||
* 因此对齐、垂直居中与省略号规则都与 `UiText` 一模一样,只是多了 GDI 级裁剪。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] text 文本内容。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] rect 文本布局矩形。
|
||||
* @param [IN] argb 文本颜色。
|
||||
* @param [IN] align 水平对齐方式。
|
||||
* @param [IN] clipRect 裁剪矩形(超出部分不绘制)。
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* @brief 测量单行文本的像素宽度(无需 GDI+ 上下文,可在布局阶段调用)。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] text 文本内容。
|
||||
* @return 文本像素宽度。
|
||||
*/
|
||||
float UiMeasureTextWidth(HFONT font, const std::wstring& text);
|
||||
|
||||
/**
|
||||
* @brief 在指定裁剪矩形内以显式起点绘制一段文本(使用 GDI ExtTextOut 裁剪)。
|
||||
*
|
||||
* `UiText` 走 `DrawTextW`,**不遵循 GDI+ 裁剪区**;输入框在长内容、水平滚动的情况下
|
||||
* 会把字符画到框体之外。本函数用 `ETO_CLIPPING` 让 GDI 自己裁剪,彻底避免越界绘制。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] text 文本内容。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] x 文本起始横坐标。
|
||||
* @param [IN] rect 垂直居中参考矩形。
|
||||
* @param [IN] argb 文本颜色。
|
||||
* @param [IN] clipRect 裁剪矩形(超出部分不绘制)。
|
||||
*/
|
||||
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] text 原始文本。
|
||||
* @param [IN] font 字体句柄。
|
||||
* @param [IN] maxWidth 允许的最大像素宽度。
|
||||
* @return 截断后的文本。
|
||||
*/
|
||||
std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth);
|
||||
|
||||
/**
|
||||
* @brief 用 GDI 一次性绘制若干条**不透明 1px 实线**(每条线段相互独立)。
|
||||
*
|
||||
* 性能关键:GDI+ 的 `DrawLine` 实测每条约 0.1–0.19 ms(带抗锯齿的长线更慢),
|
||||
* 而一张网格通常要画几十条网格线 —— 逐条调用会让单帧多花十几毫秒。
|
||||
* 这里改用 GDI(一次取 HDC + 一支画笔 + 多个 LineTo),快一两个数量级,
|
||||
* 而且像素级网格线本来就不需要抗锯齿,观感反而更清晰。
|
||||
*
|
||||
* ⚠️ GDI 绘制**不受 GDI+ 裁剪区约束**,因此必须显式传入裁剪矩形
|
||||
* (内部用 `SaveDC` + `IntersectClipRect` 保证不越界)。
|
||||
* ⚠️ 只能用于**不透明**颜色(GDI 不做 alpha 混合);半透明色需调用方先与底色混合。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] clip 裁剪矩形(客户区坐标)。
|
||||
* @param [IN] segments 线段数组,每 4 个 float 为一条(x1,y1,x2,y2)。
|
||||
* @param [IN] segmentCount 线段数量。
|
||||
* @param [IN] argb 颜色(Alpha 必须为 0xFF,否则不做任何绘制)。
|
||||
* @param [IN] width 线宽(像素,>= 1;仅精确支持 1,更宽时按整数宽画)。
|
||||
*/
|
||||
void UiDrawSolidLines(gd::Graphics& g, const gd::RectF& clip, const float* segments,
|
||||
std::size_t segmentCount, std::uint32_t argb, int width = 1);
|
||||
|
||||
/**
|
||||
* @brief 把半透明颜色与已知不透明底色混合,得到可直接用于 GDI 的不透明颜色。
|
||||
* @param [IN] background 不透明底色(0xFFRRGGBB)。
|
||||
* @param [IN] argb 待叠加颜色(含 alpha)。
|
||||
* @return 混合后的不透明颜色。
|
||||
*/
|
||||
std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb);
|
||||
|
||||
/**
|
||||
* @brief 作废「文本宽度测量」的记忆化缓存。
|
||||
*
|
||||
* `UiMeasureTextWidth` 内部按 (字体句柄, 文本) 缓存测量结果以摊薄 GDI 调用开销;
|
||||
* 字体句柄被重建(DPI 变化等)时必须调用本函数,否则系统复用的同一句柄值会命中
|
||||
* 旧宽度,造成布局错位。
|
||||
*/
|
||||
void UiClearMeasureCache();
|
||||
|
||||
/**
|
||||
* @brief 一次性填充多个圆角矩形(同色同圆角)。
|
||||
*
|
||||
* 每帧要画几十个刻度数值底板时,逐个调用 `UiFillRoundRect` 会带来可观的 GDI+ 调用开销;
|
||||
* 合并成一条 GraphicsPath 后只 FillPath 一次(实测约快 3 倍)。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rects 矩形集合。
|
||||
* @param [IN] radius 圆角半径。
|
||||
* @param [IN] argb 填充色。
|
||||
*/
|
||||
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
|
||||
std::uint32_t argb);
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __PAINT_UTIL_HPP__
|
||||
@@ -0,0 +1,542 @@
|
||||
#ifndef __POPUP_MENU_HPP__
|
||||
#define __POPUP_MENU_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "TextEdit.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 弹出菜单的打开来源,用于把选择结果派发回正确的组件。
|
||||
*/
|
||||
enum class MenuScope : int
|
||||
{
|
||||
NONE = 0, /**< 无来源。 */
|
||||
SIGNAL_PANEL = 1, /**< 信号面板。 */
|
||||
TAB_BAR = 2, /**< 标签栏。 */
|
||||
PLOT_VIEW = 3 /**< 波形绘图区。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 弹出菜单项的显示类型。
|
||||
*/
|
||||
enum class MenuItemKind : int
|
||||
{
|
||||
NORMAL = 0, /**< 普通可点击项。 */
|
||||
SEPARATOR = 1, /**< 分隔线。 */
|
||||
HEADER = 2, /**< 分组标题(不可点击)。 */
|
||||
SWATCHES = 3, /**< 颜色色块行。 */
|
||||
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
|
||||
SUBMENU = 5, /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
|
||||
HEADER_ACTION = 6 /**< 分组标题 + **右侧图标按钮**(如「还原」),图标可点击。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 单个弹出菜单项。
|
||||
*/
|
||||
struct MenuItem
|
||||
{
|
||||
MenuItemKind kind = MenuItemKind::NORMAL; /**< 项目类型。 */
|
||||
std::wstring label; /**< 显示文本;EDIT 项为输入框左侧的说明文字。 */
|
||||
int id = 0; /**< 命令标识。 */
|
||||
bool enabled = true; /**< 是否可用。 */
|
||||
bool checked = false; /**< 是否处于选中(单选)状态。 */
|
||||
int swatchCount = 0; /**< 色块数量(仅色块行有效)。 */
|
||||
std::uint32_t swatches[16] = {}; /**< 色块颜色。 */
|
||||
int checkedSwatch = -1; /**< 被选中的色块下标。 */
|
||||
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
|
||||
/**
|
||||
* EDIT 项**进入编辑时的初始文本**;为空则使用 `value`。
|
||||
*
|
||||
* 用于「非编辑态的显示文本」与「编辑态的数值」不一致的场合,例如时间轴偏移:
|
||||
* 平时显示 `Δt:5s 60ms`,进入编辑后应显示以秒为单位的数值 `5.06`。
|
||||
*/
|
||||
std::wstring editValue;
|
||||
/**
|
||||
* EDIT 项**编辑态**在输入框最右侧提示的单位文本(如 `s`);为空则不提示。
|
||||
*
|
||||
* 非编辑态不显示(那时显示的是 `value` 的完整文案)。
|
||||
*/
|
||||
std::wstring editSuffix;
|
||||
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
|
||||
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
|
||||
/**
|
||||
* 普通项被点击后是否**保持菜单打开**。
|
||||
*
|
||||
* 用于「带复选框的切换项」(如镜像的水平/垂直):可以连续勾选多项,
|
||||
* 勾选状态通过 `UpdateItemLabel` 回写。默认 false(点击即关):与既有行为一致。
|
||||
*/
|
||||
bool keepOpen = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 打开菜单时的附加布局约束。
|
||||
*/
|
||||
struct MenuLayoutConstraint
|
||||
{
|
||||
/**
|
||||
* @brief 菜单允许的最大高度(**客户区像素**,0 = 不限制)。
|
||||
*
|
||||
* 超过该高度时菜单不再变高:**前导标题钉在顶部、最后一个分隔线及其之后的条目固定在底部**,
|
||||
* 中间的内容成为可滚动的条目区(鼠标滚轮 / 键盘方向键 / 滚动条)。
|
||||
*
|
||||
* 用于「条目数不定」的菜单:例如加载含上百个信号的 MF4 并把每个信号都放进独立网格时,
|
||||
* 「所属网格」菜单会有上百个条目(既超出屏幕、绘制开销也很大)。
|
||||
*/
|
||||
float maxHeight = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief 可滚动条目区**最多显示的标准行数**(0 = 不限制)。
|
||||
*
|
||||
* 比 `maxHeight` 更直观(用户要求“保留 10 条网格的上限”):无论条目多高,
|
||||
* 滚动视口高度都不会超过该行数×标准行高。与 `maxHeight` 同时给出时**取更严格的那个**。
|
||||
*/
|
||||
int maxScrollRows = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 菜单选择结果。
|
||||
*/
|
||||
struct MenuResult
|
||||
{
|
||||
bool valid = false; /**< 是否产生了有效选择。 */
|
||||
MenuScope scope = MenuScope::NONE; /**< 来源组件。 */
|
||||
int id = 0; /**< 被选中项的命令标识。 */
|
||||
int swatchIndex = -1; /**< 被选中的色块下标(非色块行为 -1)。 */
|
||||
gd::RectF itemRect; /**< 被点击条目的矩形(客户区坐标),供就近弹出输入框。 */
|
||||
bool editCommit = false; /**< 该结果来自菜单内联输入框的提交(菜单保持打开)。 */
|
||||
std::wstring editText; /**< 提交的输入文本(editCommit 为 true 时有效)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 与界面主题一致的自绘弹出菜单。
|
||||
*
|
||||
* 菜单不创建独立窗口,而是作为宿主窗口绘制流程的最上层内容直接绘制到后备缓冲区,
|
||||
* 因此外观与主界面完全统一,不会出现系统默认菜单样式。
|
||||
*/
|
||||
class PopupMenu
|
||||
{
|
||||
public:
|
||||
PopupMenu();
|
||||
|
||||
/**
|
||||
* @brief 绑定字体与主题资源。
|
||||
* @param [IN] fonts 字体集合,需在菜单生命周期内保持有效。
|
||||
* @param [IN] theme 主题配色,需在菜单生命周期内保持有效。
|
||||
*/
|
||||
void Setup(const UiFonts* fonts, const Theme* theme);
|
||||
|
||||
/**
|
||||
* @brief 设置 DPI 缩放比例。
|
||||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||||
*/
|
||||
void SetScale(float scale);
|
||||
|
||||
/**
|
||||
* @brief 在指定位置打开菜单。
|
||||
* @param [IN] scope 来源组件标识。
|
||||
* @param [IN] bounds 宿主客户区矩形,用于把菜单限制在窗口内。
|
||||
* @param [IN] x 锚点横坐标(客户区坐标)。
|
||||
* @param [IN] y 锚点纵坐标(客户区坐标)。
|
||||
* @param [IN] items 菜单项集合。
|
||||
* @param [IN] expandUp 为 true 时菜单**向上展开**,且 `(x, y)` 是菜单的**左下角**
|
||||
* (用于“二级面板贴在按钮左上方”这类锚定);为 false(默认)时向右下展开,
|
||||
* `(x, y)` 是菜单的左上角。
|
||||
* @param [IN] constraint 附加布局约束(最大高度 / 超长时可滚动),默认不限制。
|
||||
*/
|
||||
void Open(MenuScope scope, const gd::RectF& bounds, int x, int y,
|
||||
const std::vector<MenuItem>& items, bool expandUp = false,
|
||||
const MenuLayoutConstraint& constraint = MenuLayoutConstraint());
|
||||
|
||||
/**
|
||||
* @brief 关闭菜单。
|
||||
*/
|
||||
void Close();
|
||||
|
||||
/**
|
||||
* @brief 判断菜单是否处于打开状态。
|
||||
* @return 打开返回 true。
|
||||
*/
|
||||
bool IsOpen() const { return m_open; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前菜单的来源组件标识。
|
||||
* @return 来源标识。
|
||||
*/
|
||||
MenuScope Scope() const { return m_scope; }
|
||||
|
||||
/**
|
||||
* @brief 绘制菜单(应在宿主绘制的最后一步调用)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void Paint(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标移动。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnMouseMove(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键按下。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [OUT] result 在菜单外按下时返回被取消的选择结果。
|
||||
* @return 事件已被菜单消费返回 true。
|
||||
*/
|
||||
bool OnLButtonDown(int x, int y, MenuResult& result);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键抬起。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [OUT] result 选择结果。
|
||||
* @return 事件已被菜单消费返回 true。
|
||||
*/
|
||||
bool OnLButtonUp(int x, int y, MenuResult& result);
|
||||
|
||||
/**
|
||||
* @brief 处理键盘按键。
|
||||
* @param [IN] key 虚拟键码。
|
||||
* @param [OUT] result 选择结果。
|
||||
* @return 事件已被菜单消费返回 true。
|
||||
*/
|
||||
bool OnKeyDown(WPARAM key, MenuResult& result);
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在菜单内部。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在菜单内返回 true。
|
||||
*/
|
||||
bool ContainsPoint(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 菜单是否因内容过长而启用了滚动(条目区可滚动、前导标题与尾部条目固定)。
|
||||
* @return 启用滚动返回 true。
|
||||
*/
|
||||
bool IsScrollable() const { return m_open && m_scrollEnabled; }
|
||||
|
||||
/**
|
||||
* @brief 滚动条**轨道**矩形(仅可滚动菜单有效,其余返回空矩形)。
|
||||
*
|
||||
* 同时也是“可滚动条目区”的依据:条目区内的条目只在该矩形内可点。
|
||||
*/
|
||||
const gd::RectF& ScrollBarRect() const { return m_scrollBarRect; }
|
||||
|
||||
/**
|
||||
* @brief 滚动条**滑块**矩形(仅可滚动菜单有效)。
|
||||
* @return 滑块矩形;无需滚动时返回空矩形。
|
||||
*/
|
||||
gd::RectF ScrollBarThumbRect() const;
|
||||
|
||||
/**
|
||||
* @brief 由「滑块的顶部 Y」反推对应的滚动偏移(拖拽滚动条 / 点击轨道时用)。
|
||||
* @param [IN] thumbTop 期望的滑块顶部 Y(客户区坐标)。
|
||||
* @return 滚动偏移(已夹到合法范围)。
|
||||
*/
|
||||
float ScrollOffsetForThumbTop(float thumbTop) const;
|
||||
|
||||
/**
|
||||
* @brief 条目区当前的滚动偏移(仅可滚动菜单有效)。
|
||||
* @return 偏移量(像素,0 = 顶部)。
|
||||
*/
|
||||
float ScrollOffset() const { return m_scrollOffset; }
|
||||
|
||||
/**
|
||||
* @brief 可滚动条目区在菜单条目中的下标范围(半开区间 `[first, last)`)。
|
||||
*
|
||||
* 前导标题(`HEADER` / `HEADER_ACTION`)与末尾分隔线及其之后的条目**不参与滚动**。
|
||||
* @param [OUT] first 条目区首个条目下标。
|
||||
* @param [OUT] last 条目区末尾(不含)下标。
|
||||
* @return 菜单启用滚动返回 true;否则输出全区间。
|
||||
*/
|
||||
bool ScrollRegion(int& first, int& last) const;
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标滚轮:滚动可滚动菜单的条目区。
|
||||
* @param [IN] delta 滚轮增量(`WM_MOUSEWHEEL` 的 `GET_WHEEL_DELTA_WPARAM`)。
|
||||
* @return 滚动位置发生变化返回 true。
|
||||
*/
|
||||
bool OnMouseWheel(int delta);
|
||||
|
||||
/**
|
||||
* @brief 设置剪贴板所有者窗口句柄(供内联输入框复制/粘贴)。
|
||||
* @param [IN] host 宿主窗口句柄,允许为空。
|
||||
*/
|
||||
void SetHostWindow(HWND host) { m_host = host; }
|
||||
|
||||
/**
|
||||
* @brief 判断是否有菜单项正处于内联编辑状态。
|
||||
* @return 处于编辑状态返回 true。
|
||||
*/
|
||||
bool IsEditing() const { return m_editIndex >= 0; }
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在当前正在编辑的输入框内。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在输入框内返回 true。
|
||||
*/
|
||||
bool IsOnEditField(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 编辑态下按下左键:定位插入符并开始拖拽框选。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 命中输入框返回 true。
|
||||
*/
|
||||
bool OnEditMouseDown(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 编辑态下拖拽鼠标:扩展选区。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @return 选区发生变化返回 true。
|
||||
*/
|
||||
bool OnEditMouseMove(int x);
|
||||
|
||||
/**
|
||||
* @brief 编辑态下松开左键:结束拖拽框选。
|
||||
* @return 之前处于拖拽中返回 true。
|
||||
*/
|
||||
bool OnEditMouseUp();
|
||||
|
||||
/**
|
||||
* @brief 编辑态下处理字符输入。
|
||||
* @param [IN] character 字符。
|
||||
* @param [OUT] result 提交时返回 editCommit 结果。
|
||||
* @return 需要重绘返回 true。
|
||||
*/
|
||||
bool OnChar(wchar_t character, MenuResult& result);
|
||||
|
||||
/**
|
||||
* @brief 更新指定命令项显示的值文本(菜单保持打开,立即重新排布)。
|
||||
* @param [IN] id 命令标识。
|
||||
* @param [IN] value 新的值文本(非编辑态显示)。
|
||||
* @param [IN] editValue 新的**编辑初值**(为空时保持原值)。
|
||||
* ⚠️ 非编辑态文本与编辑态文本不一致的行(如时间轴偏移:`Δt:1h 1min 2s` ↔ `3662`)
|
||||
* 必须一并更新,否则下次进入编辑会拿到陈旧的初值。
|
||||
* @return 找到对应项并更新返回 true。
|
||||
*/
|
||||
bool UpdateItemValue(int id, const std::wstring& value,
|
||||
const std::wstring& editValue = std::wstring());
|
||||
|
||||
/**
|
||||
* @brief 更新指定普通项的文字与可用/选中状态(用于实时反映状态)。
|
||||
* @param [IN] id 命令标识。
|
||||
* @param [IN] label 新的文字。
|
||||
* @param [IN] checked 新的选中状态。
|
||||
* @param [IN] enabled 新的可用状态。
|
||||
* @return 找到对应项并更新返回 true。
|
||||
*/
|
||||
bool UpdateItemLabel(int id, const std::wstring& label, bool checked = false, bool enabled = true);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 计算单个菜单项的纵向高度。
|
||||
* @param [IN] item 菜单项。
|
||||
* @return 高度(像素)。
|
||||
*/
|
||||
float ItemHeight(const MenuItem& item) const;
|
||||
|
||||
/**
|
||||
* @brief 依据当前项目集合计算整体矩形与各项矩形。
|
||||
* @param [IN] bounds 宿主客户区矩形。
|
||||
* @param [IN] x 锚点横坐标。
|
||||
* @param [IN] y 锚点纵坐标。
|
||||
* @param [IN] expandUp 为 true 时以 `(x, y)` 为**左下角**向上展开。
|
||||
*/
|
||||
void BuildLayout(const gd::RectF& bounds, int x, int y, bool expandUp);
|
||||
|
||||
/**
|
||||
* @brief 查询坐标命中的菜单项下标。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [OUT] swatchIndex 命中的色块下标(非色块行为 -1)。
|
||||
* @return 项目下标,未命中返回 -1。
|
||||
*/
|
||||
int ItemAtPoint(int x, int y, int& swatchIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 将悬停项移动到下一个可用项。
|
||||
* @param [IN] step 步进(+1 向下,-1 向上)。
|
||||
*/
|
||||
void MoveHover(int step);
|
||||
|
||||
/**
|
||||
* @brief 确认当前悬停项并生成结果。
|
||||
* @param [OUT] result 选择结果。
|
||||
* @return 生成了有效选择返回 true。
|
||||
*/
|
||||
bool CommitHover(MenuResult& result);
|
||||
|
||||
/**
|
||||
* @brief 计算指定下标项的内联输入框矩形(EDIT 项)。
|
||||
* @param [IN] index 项目下标。
|
||||
* @return 输入框矩形,非法下标返回空矩形。
|
||||
*/
|
||||
gd::RectF EditFieldRect(int index) const;
|
||||
|
||||
/**
|
||||
* @brief 计算 EDIT 项**编辑态单位后缀**所占的宽度(0 表示无后缀)。
|
||||
*
|
||||
* 排版、命中测试与绘制共用:输入框正文区会向右让出这么多宽度,
|
||||
* 后缀文本绘制在它右侧(靠右对齐)。
|
||||
*
|
||||
* @param [IN] item 菜单项。
|
||||
* @return 后缀宽度(像素)。
|
||||
*/
|
||||
float EditSuffixWidth(const MenuItem& item) const;
|
||||
|
||||
/**
|
||||
* @brief 计算 `HEADER_ACTION` 项右侧**图标按钮**(如「还原」)的矩形。
|
||||
*
|
||||
* 图标紧贴该行右端(右边距与其它行一致、行内垂直居中),大小按缩放比例推导。
|
||||
* 鼠标悬停时该图标会高亮,点击即下发该项的命令。
|
||||
*
|
||||
* @param [IN] index 项目下标。
|
||||
* @return 图标矩形;非 `HEADER_ACTION` 项或非法下标返回空矩形。
|
||||
*/
|
||||
gd::RectF HeaderIconRect(int index) const;
|
||||
|
||||
/**
|
||||
* @brief 计算 EDIT 项输入框的“自然宽度”(排版与绘制共用同一公式)。
|
||||
* @param [IN] item 菜单项。
|
||||
* @return 宽度(像素)。
|
||||
*/
|
||||
float NaturalEditFieldWidth(const MenuItem& item) const;
|
||||
|
||||
/**
|
||||
* @brief 进入指定项的内联编辑状态。
|
||||
* @param [IN] index 项目下标。
|
||||
* @return 成功进入返回 true。
|
||||
*/
|
||||
bool BeginItemEdit(int index);
|
||||
|
||||
/**
|
||||
* @brief 结束内联编辑。
|
||||
* @param [IN] commit 是否提交(false 为放弃修改)。
|
||||
* @param [OUT] result 提交时输出 editCommit 结果。
|
||||
* @return 之前处于编辑状态返回 true。
|
||||
*/
|
||||
bool EndItemEdit(bool commit, MenuResult& result);
|
||||
|
||||
/**
|
||||
* @brief 依据当前项集合重新排布(用于值文本变化后)。
|
||||
*/
|
||||
void RebuildLayout();
|
||||
|
||||
/**
|
||||
* @brief 退出内联编辑状态(不产生任何结果)。
|
||||
*/
|
||||
void CloseEditState();
|
||||
|
||||
/**
|
||||
* @brief 展开指定项的二级子菜单(项不存在或非 SUBMENU 时等价于关闭)。
|
||||
* @param [IN] parentIndex 父项下标。
|
||||
*/
|
||||
void BuildSubLayout(int parentIndex);
|
||||
|
||||
/**
|
||||
* @brief 关闭二级子菜单并清空其状态。
|
||||
*/
|
||||
void CloseSubmenu();
|
||||
|
||||
/**
|
||||
* @brief 查询坐标命中的子菜单项下标。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 子菜单项下标,未命中返回 -1。
|
||||
*/
|
||||
int SubItemAtPoint(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制二级子菜单(含投影与全部条目)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void PaintSubmenu(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 键盘移动悬停项后,把该项滚动到可滚动区的可视范围内。
|
||||
* @param [IN] index 目标条目下标。
|
||||
* @return 滚动偏移发生变化返回 true。
|
||||
*/
|
||||
bool EnsureItemVisible(int index);
|
||||
|
||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
|
||||
bool m_open = false; /**< 是否处于打开状态。 */
|
||||
MenuScope m_scope = MenuScope::NONE; /**< 来源组件标识。 */
|
||||
std::vector<MenuItem> m_items; /**< 菜单项集合。 */
|
||||
|
||||
gd::RectF m_rect; /**< 菜单整体矩形。 */
|
||||
std::vector<gd::RectF> m_itemRects; /**< 各项矩形。 */
|
||||
/**
|
||||
* 同一次排布中,**带说明文字的 EDIT 项**共用的输入框宽度(0 表示没有这类项)。
|
||||
*
|
||||
* 宽度取这些项里最宽的自然宽度,并在排布时按“不能挤掉说明文字 / 不超过内容区”夹好。
|
||||
* 同一面板内的输入框因此一样宽、右边界对齐(用户要求「Y轴」与「时间轴」等宽对齐,
|
||||
* 而不是各自按自己的文本长度自适应)。
|
||||
*/
|
||||
float m_editFieldWidth = 0.0f;
|
||||
std::vector<std::vector<gd::RectF>> m_swatchRects; /**< 各项内的色块矩形。 */
|
||||
|
||||
int m_hoverIndex = -1; /**< 当前悬停项。 */
|
||||
int m_hoverIconIndex = -1; /**< 当前悬停的 `HEADER_ACTION` 图标所在项下标。 */
|
||||
int m_hoverSwatch = -1; /**< 当前悬停色块。 */
|
||||
int m_pressedIndex = -1; /**< 按下时的项目下标。 */
|
||||
int m_pressedSwatch = -1; /**< 按下时的色块下标。 */
|
||||
|
||||
HWND m_host = nullptr; /**< 剪贴板所有者窗口句柄。 */
|
||||
TextEdit m_edit; /**< 内联输入框编辑模型。 */
|
||||
int m_editIndex = -1; /**< 正在编辑的项目下标,-1 表示未编辑。 */
|
||||
|
||||
gd::RectF m_buildBounds; /**< 最近一次排布使用的宿主矩形。 */
|
||||
int m_buildX = 0; /**< 最近一次排布使用的锚点横坐标。 */
|
||||
int m_buildY = 0; /**< 最近一次排布使用的锚点纵坐标。 */
|
||||
bool m_buildExpandUp = false; /**< 最近一次排布是否为“向上展开(锚点为左下角)”。 */
|
||||
|
||||
/** 最近一次使用的布局约束(`RebuildLayout` 复用)。 */
|
||||
MenuLayoutConstraint m_constraint;
|
||||
/**
|
||||
* @brief 可滚动条目区的下标范围(半开区间 `[m_scrollFirst, m_pinnedIndex)`)。
|
||||
*
|
||||
* `m_scrollFirst` = 前导固定条目数(**「所属网格」这类标题必须始终留在最上方、不参与滚动**);
|
||||
* `m_pinnedIndex` = 末尾固定区起点(最后一个分隔线,典型「新建网格」钉在底部)。
|
||||
*/
|
||||
int m_scrollFirst = 0;
|
||||
int m_pinnedIndex = -1;
|
||||
bool m_scrollEnabled = false; /**< 条目区是否可滚动(内容超过约束上限时启用)。 */
|
||||
float m_scrollOffset = 0.0f; /**< 滚动区当前的滚动偏移(像素,0 = 顶部)。 */
|
||||
float m_scrollRegionHeight = 0.0f; /**< 滚动区的内容总高(像素)。 */
|
||||
gd::RectF m_scrollViewport; /**< 滚动区的可视矩形(也是滚动区条目的裁剪矩形)。 */
|
||||
gd::RectF m_scrollBarRect; /**< 滚动条轨道矩形(仅可滚动菜单有效)。 */
|
||||
bool m_scrollBarDragging = false; /**< 是否正在拖拽滚动条滑块。 */
|
||||
float m_scrollBarGrabOffset = 0.0f; /**< 拖拽时鼠标相对滑块顶部的偏移(像素)。 */
|
||||
|
||||
bool m_subOpen = false; /**< 二级子菜单是否展开。 */
|
||||
int m_subParentIndex = -1; /**< 展开子菜单的父项下标。 */
|
||||
gd::RectF m_subRect; /**< 子菜单整体矩形。 */
|
||||
std::vector<MenuItem> m_subItems; /**< 子菜单项集合。 */
|
||||
std::vector<gd::RectF> m_subItemRects; /**< 子菜单各项矩形。 */
|
||||
int m_subHoverIndex = -1; /**< 子菜单当前悬停项。 */
|
||||
int m_subPressedIndex = -1; /**< 子菜单按下时的项下标。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __POPUP_MENU_HPP__
|
||||
@@ -0,0 +1,241 @@
|
||||
#ifndef __TEXT_EDIT_HPP__
|
||||
#define __TEXT_EDIT_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 输入框的字符过滤策略。
|
||||
*/
|
||||
enum class EditFilter : int
|
||||
{
|
||||
ANY = 0, /**< 接受任意可打印字符(信号名、分组名、视图名)。 */
|
||||
NUMBER = 1, /**< 仅接受数字与科学计数法符号(-+.eE)。 */
|
||||
HEX = 2, /**< 仅接受 0-9a-fA-F 与 #(色号)。 */
|
||||
BYTE = 3 /**< 仅接受整数数字(0-255 分量)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 单行内联输入框的编辑模型。
|
||||
*
|
||||
* 提供与常规输入框一致的行为:光标移动、Shift 选区、全选、
|
||||
* 退格/删除清除选区、Ctrl+A/C/X/V、粘贴清洗,以及"首次键入整体替换"。
|
||||
* 渲染由 `PaintUtil::UiDrawEditField` 消费本对象完成(含水平滚动)。
|
||||
*/
|
||||
class TextEdit
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief 进入编辑状态。
|
||||
* @param [IN] text 初始文本。
|
||||
* @param [IN] selectAll 是否将初始文本整体选中(首次键入即整体替换)。
|
||||
* @param [IN] filter 字符过滤策略。
|
||||
* @param [IN] maxLength 允许的最大字符数(0 表示不限)。
|
||||
*/
|
||||
void Begin(const std::wstring& text, bool selectAll, EditFilter filter, std::size_t maxLength);
|
||||
|
||||
/**
|
||||
* @brief 退出编辑状态并清空内容。
|
||||
*/
|
||||
void End();
|
||||
|
||||
/**
|
||||
* @brief 判断是否处于编辑状态。
|
||||
* @return 处于编辑状态返回 true。
|
||||
*/
|
||||
bool Active() const { return m_active; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前文本。
|
||||
* @return 文本常量引用。
|
||||
*/
|
||||
const std::wstring& Text() const { return m_text; }
|
||||
|
||||
/**
|
||||
* @brief 获取光标位置(插入符所在偏移)。
|
||||
* @return 字符偏移。
|
||||
*/
|
||||
std::size_t Caret() const { return m_caret; }
|
||||
|
||||
/**
|
||||
* @brief 获取选区锚点位置。
|
||||
* @return 字符偏移。
|
||||
*/
|
||||
std::size_t Anchor() const { return m_anchor; }
|
||||
|
||||
/**
|
||||
* @brief 判断是否存在选区。
|
||||
* @return 存在非空选区返回 true。
|
||||
*/
|
||||
bool HasSelection() const { return m_caret != m_anchor; }
|
||||
|
||||
/**
|
||||
* @brief 获取选区起始偏移。
|
||||
* @return 较小的偏移值。
|
||||
*/
|
||||
std::size_t SelectionBegin() const { return (m_caret < m_anchor) ? m_caret : m_anchor; }
|
||||
|
||||
/**
|
||||
* @brief 获取选区结束偏移。
|
||||
* @return 较大的偏移值。
|
||||
*/
|
||||
std::size_t SelectionEnd() const { return (m_caret < m_anchor) ? m_anchor : m_caret; }
|
||||
|
||||
/**
|
||||
* @brief 判断是否处于"首次输入整体替换"状态。
|
||||
* @return 全部文本处于选中状态返回 true。
|
||||
*/
|
||||
bool Fresh() const { return m_fresh; }
|
||||
|
||||
/**
|
||||
* @brief 在光标处插入一个字符(存在选区时先替换选区)。
|
||||
* @param [IN] character 待插入字符。
|
||||
* @return 内容发生变化返回 true。
|
||||
*/
|
||||
bool Insert(wchar_t character);
|
||||
|
||||
/**
|
||||
* @brief 退格:存在选区时删除选区,否则删除光标前一个字符。
|
||||
* @return 内容发生变化返回 true。
|
||||
*/
|
||||
bool Backspace();
|
||||
|
||||
/**
|
||||
* @brief 删除:存在选区时删除选区,否则删除光标后一个字符。
|
||||
* @return 内容发生变化返回 true。
|
||||
*/
|
||||
bool DeleteForward();
|
||||
|
||||
/**
|
||||
* @brief 移动光标。
|
||||
* @param [IN] delta 位移量(负值向左)。
|
||||
* @param [IN] extendSelection 是否按住 Shift 扩展选区。
|
||||
* @return 光标或选区发生变化返回 true。
|
||||
*/
|
||||
bool MoveCaret(int delta, bool extendSelection);
|
||||
|
||||
/**
|
||||
* @brief 移动光标到行首。
|
||||
* @param [IN] extendSelection 是否按住 Shift 扩展选区。
|
||||
* @return 光标或选区发生变化返回 true。
|
||||
*/
|
||||
bool MoveHome(bool extendSelection);
|
||||
|
||||
/**
|
||||
* @brief 移动光标到行尾。
|
||||
* @param [IN] extendSelection 是否按住 Shift 扩展选区。
|
||||
* @return 光标或选区发生变化返回 true。
|
||||
*/
|
||||
bool MoveEnd(bool extendSelection);
|
||||
|
||||
/**
|
||||
* @brief 全选当前内容。
|
||||
*/
|
||||
void SelectAll();
|
||||
|
||||
/**
|
||||
* @brief 取消选区并把插入符移动到文本末尾(模拟在输入框右侧按下鼠标)。
|
||||
* @return 光标或选区发生变化返回 true。
|
||||
*/
|
||||
bool DeselectToEnd();
|
||||
|
||||
/**
|
||||
* @brief 开始鼠标拖拽框选:把锚点与插入符都放在给定偏移。
|
||||
* @param [IN] offset 起始偏移。
|
||||
*/
|
||||
void BeginDrag(std::size_t offset);
|
||||
|
||||
/**
|
||||
* @brief 拖拽过程中更新插入符(锚点保持不变 → 形成选区)。
|
||||
* @param [IN] offset 当前偏移。
|
||||
* @return 光标或选区发生变化返回 true。
|
||||
*/
|
||||
bool UpdateDrag(std::size_t offset);
|
||||
|
||||
/**
|
||||
* @brief 结束鼠标拖拽。
|
||||
*/
|
||||
void EndDrag() { m_dragging = false; }
|
||||
|
||||
/**
|
||||
* @brief 判断是否正在鼠标拖拽框选。
|
||||
* @return 正在拖拽返回 true。
|
||||
*/
|
||||
bool Dragging() const { return m_dragging; }
|
||||
|
||||
/**
|
||||
* @brief 复制当前内容到剪贴板(存在选区时只复制选区)。
|
||||
* @param [IN] owner 剪贴板所有者窗口句柄,允许为空。
|
||||
* @return 复制成功返回 true。
|
||||
*/
|
||||
bool CopyToClipboard(HWND owner) const;
|
||||
|
||||
/**
|
||||
* @brief 剪切当前内容到剪贴板(存在选区时只剪切选区)。
|
||||
* @param [IN] owner 剪贴板所有者窗口句柄,允许为空。
|
||||
* @return 内容发生变化返回 true。
|
||||
*/
|
||||
bool CutToClipboard(HWND owner);
|
||||
|
||||
/**
|
||||
* @brief 从剪贴板粘贴(替换当前选区)。
|
||||
* @param [IN] owner 剪贴板所有者窗口句柄,允许为空。
|
||||
* @return 内容发生变化返回 true。
|
||||
*/
|
||||
bool PasteFromClipboard(HWND owner);
|
||||
|
||||
/**
|
||||
* @brief 判断某字符是否符合指定过滤策略。
|
||||
* @param [IN] filter 过滤策略。
|
||||
* @param [IN] character 待检验字符。
|
||||
* @return 允许输入返回 true。
|
||||
*/
|
||||
static bool AcceptsCharacter(EditFilter filter, wchar_t character);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 用给定文本替换当前选区并更新光标。
|
||||
* @param [IN] replacement 替换文本。
|
||||
* @return 内容发生变化返回 true。
|
||||
*/
|
||||
bool ReplaceSelection(const std::wstring& replacement);
|
||||
|
||||
/**
|
||||
* @brief 把偏移量夹到合法范围并对齐到字符边界。
|
||||
* @param [IN] offset 原始偏移。
|
||||
* @return 合法偏移。
|
||||
*/
|
||||
std::size_t ClampOffset(std::size_t offset) const;
|
||||
|
||||
/**
|
||||
* @brief 向左移动一个字符(避免落在代理对中间)。
|
||||
* @param [IN] offset 起始偏移。
|
||||
* @return 新偏移。
|
||||
*/
|
||||
std::size_t StepLeft(std::size_t offset) const;
|
||||
|
||||
/**
|
||||
* @brief 向右移动一个字符(避免落在代理对中间)。
|
||||
* @param [IN] offset 起始偏移。
|
||||
* @return 新偏移。
|
||||
*/
|
||||
std::size_t StepRight(std::size_t offset) const;
|
||||
|
||||
bool m_active = false; /**< 是否处于编辑状态。 */
|
||||
bool m_fresh = false; /**< 是否处于“首次输入整体替换”状态。 */
|
||||
bool m_dragging = false; /**< 是否正在鼠标拖拽框选。 */
|
||||
std::wstring m_text; /**< 当前文本。 */
|
||||
std::size_t m_caret = 0; /**< 光标位置。 */
|
||||
std::size_t m_anchor = 0; /**< 选区锚点。 */
|
||||
EditFilter m_filter = EditFilter::ANY; /**< 字符过滤策略。 */
|
||||
std::size_t m_maxLength = 0; /**< 最大字符数,0 表示不限。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __TEXT_EDIT_HPP__
|
||||
@@ -0,0 +1,126 @@
|
||||
#ifndef __THEME_HPP__
|
||||
#define __THEME_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/// @brief GDI+ 命名空间短别名,便于在界面代码中引用绘图类型。
|
||||
namespace gd = Gdiplus;
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 将 0xAARRGGBB 颜色值转换为 GDI+ 颜色对象。
|
||||
* @param [IN] argb 颜色值。
|
||||
* @return GDI+ 颜色对象。
|
||||
*/
|
||||
inline gd::Color ToGdiColor(std::uint32_t argb)
|
||||
{
|
||||
return gd::Color(static_cast<BYTE>((argb >> 24) & 0xFFu), static_cast<BYTE>((argb >> 16) & 0xFFu),
|
||||
static_cast<BYTE>((argb >> 8) & 0xFFu), static_cast<BYTE>(argb & 0xFFu));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 由 0xRRGGBB 构造不透明的 0xAARRGGBB 颜色值。
|
||||
* @param [IN] rgb RGB 颜色值。
|
||||
* @return 不透明颜色值。
|
||||
*/
|
||||
inline std::uint32_t MakeArgb(std::uint32_t rgb)
|
||||
{
|
||||
return 0xFF000000u | (rgb & 0x00FFFFFFu);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 替换颜色值的 Alpha 通道。
|
||||
* @param [IN] argb 原颜色值。
|
||||
* @param [IN] alpha 新的 Alpha 值(0-255)。
|
||||
* @return 新的颜色值。
|
||||
*/
|
||||
inline std::uint32_t WithAlpha(std::uint32_t argb, unsigned int alpha)
|
||||
{
|
||||
return (argb & 0x00FFFFFFu) | ((static_cast<std::uint32_t>(alpha) & 0xFFu) << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 在两个颜色之间做线性插值。
|
||||
* @param [IN] fromColor 起始颜色。
|
||||
* @param [IN] toColor 结束颜色。
|
||||
* @param [IN] ratio 插值比例,0 返回起始色,1 返回结束色。
|
||||
* @return 插值后的颜色值。
|
||||
*/
|
||||
inline std::uint32_t MixColor(std::uint32_t fromColor, std::uint32_t toColor, double ratio)
|
||||
{
|
||||
if (ratio < 0.0)
|
||||
{
|
||||
ratio = 0.0;
|
||||
}
|
||||
if (ratio > 1.0)
|
||||
{
|
||||
ratio = 1.0;
|
||||
}
|
||||
|
||||
std::uint32_t result = 0;
|
||||
for (int shift = 0; shift <= 24; shift += 8)
|
||||
{
|
||||
const double a = static_cast<double>((fromColor >> shift) & 0xFFu);
|
||||
const double b = static_cast<double>((toColor >> shift) & 0xFFu);
|
||||
const std::uint32_t mixed = static_cast<std::uint32_t>(a + (b - a) * ratio + 0.5);
|
||||
result |= ((mixed & 0xFFu) << shift);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 界面主题色板(Solarised Light 派生)。
|
||||
*/
|
||||
struct Theme
|
||||
{
|
||||
std::uint32_t windowBg = 0; /**< 窗口底色。 */
|
||||
std::uint32_t toolbarBg = 0; /**< 顶部工具栏底色。 */
|
||||
std::uint32_t panelBg = 0; /**< 侧边面板底色。 */
|
||||
std::uint32_t cardBg = 0; /**< 面板内卡片底色。 */
|
||||
std::uint32_t statusBg = 0; /**< 底部状态栏底色。 */
|
||||
std::uint32_t plotBg = 0; /**< 绘图区底色。 */
|
||||
std::uint32_t border = 0; /**< 常规描边色。 */
|
||||
std::uint32_t borderStrong = 0; /**< 强调描边色。 */
|
||||
|
||||
std::uint32_t text = 0; /**< 正文文字色。 */
|
||||
std::uint32_t textStrong = 0; /**< 强调文字色。 */
|
||||
std::uint32_t textMuted = 0; /**< 次要文字色。 */
|
||||
std::uint32_t textOnAccent = 0; /**< 强调色之上的文字色。 */
|
||||
|
||||
std::uint32_t accent = 0; /**< 主题强调色。 */
|
||||
std::uint32_t accentSoft = 0; /**< 强调色的浅色版本。 */
|
||||
std::uint32_t hover = 0; /**< 悬停底色。 */
|
||||
std::uint32_t pressed = 0; /**< 按下底色。 */
|
||||
std::uint32_t selection = 0; /**< 选中底色。 */
|
||||
|
||||
std::uint32_t gridWeak = 0; /**< 次级网格线颜色。 */
|
||||
std::uint32_t gridStrong = 0; /**< 主要网格线颜色。 */
|
||||
std::uint32_t axisLine = 0; /**< 坐标轴线颜色。 */
|
||||
std::uint32_t cursor = 0; /**< 时间光标颜色(单/双光标共用同一样式)。 */
|
||||
std::uint32_t readoutBg = 0; /**< 数值读数底色。 */
|
||||
std::uint32_t danger = 0; /**< 警示色。 */
|
||||
std::uint32_t warning = 0; /**< 提示色。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 获取当前使用的主题(Solarised Light)。
|
||||
* @return 主题常引用。
|
||||
*/
|
||||
const Theme& SolarizedLight();
|
||||
|
||||
/**
|
||||
* @brief 获取波形默认配色表。
|
||||
* @param [OUT] count 配色数量输出。
|
||||
* @return 颜色数组首地址。
|
||||
*/
|
||||
const std::uint32_t* SignalPalette(int& count);
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __THEME_HPP__
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,592 @@
|
||||
#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
@@ -0,0 +1,417 @@
|
||||
#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
|
||||
@@ -0,0 +1,83 @@
|
||||
#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
|
||||
@@ -1077,6 +1077,14 @@ namespace jdi
|
||||
*/
|
||||
gd::RectF EditFieldRect();
|
||||
|
||||
/**
|
||||
* @brief 当前输入框绘制与命中/插入符换算所使用的字体。
|
||||
* @return 字体句柄(分组名用 `uiSmall`,其余用 `ui`)。
|
||||
* @note ⚠️ 绘制(`PaintGroupRow` / `PaintSignalRow`)与鼠标定位(`UiEditOffsetAt`)
|
||||
* **必须使用同一个字体**,否则点哪儿插入符就落到哪儿这个不变量会被破坏。
|
||||
*/
|
||||
HFONT EditFieldFont() const;
|
||||
|
||||
/**
|
||||
* @brief 计算「筛选信号」输入框的内容矩形(编辑与显示共用,保证不跳动)。
|
||||
* @return 内容矩形。
|
||||
|
||||
@@ -1186,6 +1186,17 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
HFONT SignalPanel::EditFieldFont() const
|
||||
{
|
||||
if (m_fonts == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
// 分组名的编辑框在行内是用 `uiSmall` 绘制的(分组行字号更小),其余输入框用 `ui`。
|
||||
// 绘制、命中、插入符换算三处必须一致,否则鼠标落点与插入符位置会错开。
|
||||
return (m_editField == StyleEditField::GROUP_TITLE) ? m_fonts->uiSmall : m_fonts->ui;
|
||||
}
|
||||
|
||||
const SignalStyle* SignalPanel::StyleOf(const SignalRef& ref) const
|
||||
{
|
||||
return (m_dataSet != nullptr) ? m_dataSet->Style(ref) : nullptr;
|
||||
|
||||
@@ -821,7 +821,7 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseDown(m_fonts->ui, EditFieldRect(), m_edit, m_scale, x, y);
|
||||
return UiEditFieldMouseDown(EditFieldFont(), EditFieldRect(), m_edit, m_scale, x, y);
|
||||
}
|
||||
|
||||
bool SignalPanel::OnEditMouseMove(int x)
|
||||
@@ -834,7 +834,7 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseMove(m_fonts->ui, EditFieldRect(), m_edit, m_scale, x);
|
||||
return UiEditFieldMouseMove(EditFieldFont(), EditFieldRect(), m_edit, m_scale, x);
|
||||
}
|
||||
|
||||
bool SignalPanel::OnEditMouseUp(int x, int y)
|
||||
|
||||
@@ -326,6 +326,13 @@ namespace jdi
|
||||
if (editing)
|
||||
{
|
||||
mixText(m_edit.Text());
|
||||
// ⚠️ 插入符与选区必须进键:行位图缓存只按“行的可见内容”生效,
|
||||
// 而编辑态下插入符位置 / 选区范围也是可见内容。漏掉它们时,
|
||||
// 方向键与鼠标框选虽然改变了 `TextEdit` 状态,但键不变 ⇒ 贴回的仍是旧位图,
|
||||
// 屏幕上看就是“无法部分选择、光标不动”(用户 2026-10-19 报告)。
|
||||
mix(static_cast<std::uint64_t>(m_edit.Caret()) + 1ull);
|
||||
mix(static_cast<std::uint64_t>(m_edit.SelectionBegin()) + 1ull);
|
||||
mix(static_cast<std::uint64_t>(m_edit.SelectionEnd()) + 1ull);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
@@ -350,6 +357,10 @@ namespace jdi
|
||||
if (editing)
|
||||
{
|
||||
mixText(m_edit.Text());
|
||||
// 同分组行:插入符 / 选区也是行的可见内容(见 `GROUP_TITLE` 分支的注释)。
|
||||
mix(static_cast<std::uint64_t>(m_edit.Caret()) + 1ull);
|
||||
mix(static_cast<std::uint64_t>(m_edit.SelectionBegin()) + 1ull);
|
||||
mix(static_cast<std::uint64_t>(m_edit.SelectionEnd()) + 1ull);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user