输入框鼠标编辑、图例省略、信号跨视图共享选择与网格菜单布局修复
- 输入框:支持鼠标左键点击定位插入符、拖拽框选区域,并且不再限制字符长度 - 输入框:长文本使用 ETO_CLIPPED 裁剪绘制,避免字符溢出到编辑区域之外 - 图例:信号名称超过 200px 时省略号截断,不再挤压标尺区域 - 选中状态从 TabConfig 迁移到 DataSet,切换视图后信号选中状态保持一致 - 网格右键菜单:Y 轴最大值/最小值直接显示当前值,点击后原位置弹出内联编辑框 - 标尺数值改为绘制在图例下层,不再因避让图例而隐藏 - 移除已无用的 LegendBandRect,新增共享的编辑框布局与命中检测工具 - .gitignore 忽略 TestData 测试数据目录
This commit is contained in:
1 parent
e3744f3eff
commit
12b3531485
19 files changed
+790
-186
No files matched your search
@@ -124,6 +124,27 @@ namespace jdi
|
||||
*/
|
||||
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 宿主窗口句柄,允许为空。
|
||||
|
||||
+25
-13
@@ -249,8 +249,33 @@ namespace jdi
|
||||
*/
|
||||
double MaxTime() const;
|
||||
|
||||
/**
|
||||
* @brief 获取当前选中的信号(全标签页共享)。
|
||||
* @return 信号引用。
|
||||
*/
|
||||
const SignalRef& Selected() const { return m_selected; }
|
||||
|
||||
/**
|
||||
* @brief 设置当前选中的信号(全标签页共享)。
|
||||
* @param [IN] ref 信号引用;传入无效值表示取消选择。
|
||||
*/
|
||||
void SetSelected(const SignalRef& ref) { m_selected = ref; }
|
||||
|
||||
/**
|
||||
* @brief 在数据层移除某文件之前,按旧下标修正选中引用。
|
||||
* @param [IN] fileIndex 即将被移除的文件下标。
|
||||
*/
|
||||
void RemapSelectionAfterFileRemoval(int fileIndex);
|
||||
|
||||
/**
|
||||
* @brief 校验选中引用是否仍指向存在的信号,失效则取消选中。
|
||||
* @return 选中状态发生变化返回 true。
|
||||
*/
|
||||
bool ValidateSelection();
|
||||
|
||||
private:
|
||||
std::vector<DataFile> m_files; /**< 已加载文件集合。 */
|
||||
SignalRef m_selected; /**< 当前选中信号(跨标签页共享)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -452,18 +477,6 @@ namespace jdi
|
||||
*/
|
||||
void ToggleCollapsed(int fileIndex);
|
||||
|
||||
/**
|
||||
* @brief 获取当前选中的信号引用。
|
||||
* @return 信号引用。
|
||||
*/
|
||||
SignalRef Selected() const { return m_selected; }
|
||||
|
||||
/**
|
||||
* @brief 设置当前选中的信号。
|
||||
* @param [IN] ref 信号引用,无效值表示取消选择。
|
||||
*/
|
||||
void SetSelected(const SignalRef& ref) { m_selected = ref; }
|
||||
|
||||
/**
|
||||
* @brief 计算某网格的 Y 轴显示范围。
|
||||
* @param [IN] dataSet 数据集合。
|
||||
@@ -513,7 +526,6 @@ namespace jdi
|
||||
std::vector<bool> m_collapsed; /**< 文件分组折叠状态,按文件索引。 */
|
||||
std::vector<Lane> m_lanes; /**< 网格划分。 */
|
||||
LaneMode m_laneMode = LaneMode::PER_SIGNAL; /**< 网格划分策略。 */
|
||||
SignalRef m_selected; /**< 当前选中信号。 */
|
||||
double m_viewMin = 0.0; /**< 视图左边界时间。 */
|
||||
double m_viewMax = 1.0; /**< 视图右边界时间。 */
|
||||
bool m_viewInitialized = false; /**< 视图范围是否已初始化。 */
|
||||
|
||||
@@ -154,6 +154,44 @@ namespace jdi
|
||||
*/
|
||||
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] 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,OUT] g 绘图上下文。
|
||||
@@ -164,6 +202,29 @@ namespace jdi
|
||||
*/
|
||||
std::wstring UiEllipsize(gd::Graphics& g, 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 绘图上下文。
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace jdi
|
||||
MenuScope scope = MenuScope::NONE; /**< 来源组件。 */
|
||||
int id = 0; /**< 被选中项的命令标识。 */
|
||||
int swatchIndex = -1; /**< 被选中的色块下标(非色块行为 -1)。 */
|
||||
gd::RectF itemRect; /**< 被点击条目的矩形(客户区坐标),供就近弹出输入框。 */
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -144,6 +144,37 @@ namespace jdi
|
||||
*/
|
||||
bool DeselectToEnd();
|
||||
|
||||
/**
|
||||
* @brief 把插入符定位到指定偏移(单击输入框内时使用),并清除选区。
|
||||
* @param [IN] offset 目标偏移(内部会夹到合法范围)。
|
||||
* @return 光标或选区发生变化返回 true。
|
||||
*/
|
||||
bool PlaceCaret(std::size_t offset);
|
||||
|
||||
/**
|
||||
* @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 剪贴板所有者窗口句柄,允许为空。
|
||||
@@ -204,6 +235,7 @@ namespace jdi
|
||||
|
||||
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; /**< 选区锚点。 */
|
||||
|
||||
@@ -214,7 +214,7 @@ namespace jdi
|
||||
{
|
||||
const int index = static_cast<int>(field) - 1;
|
||||
m_edit.Begin(std::to_wstring(ToByte(m_channels[static_cast<std::size_t>(index)])), true,
|
||||
EditFilter::BYTE, 3);
|
||||
EditFilter::BYTE, 0u);
|
||||
break;
|
||||
}
|
||||
case EditField::HEX:
|
||||
@@ -223,7 +223,7 @@ namespace jdi
|
||||
wchar_t buffer[16];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%02X%02X%02X", (color >> 16) & 0xFFu,
|
||||
(color >> 8) & 0xFFu, color & 0xFFu);
|
||||
m_edit.Begin(std::wstring(buffer), true, EditFilter::HEX, 7);
|
||||
m_edit.Begin(std::wstring(buffer), true, EditFilter::HEX, 0u);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -373,6 +373,34 @@ namespace jdi
|
||||
return m_edit.DeselectToEnd();
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::OnEditMouseDown(int x, int y)
|
||||
{
|
||||
if (m_editField == EditField::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseDown(m_fonts->monoSmall, EditFieldRect(), m_edit, m_scale, x, y);
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::OnEditMouseMove(int x)
|
||||
{
|
||||
if (m_editField == EditField::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return UiEditFieldMouseMove(m_fonts->monoSmall, EditFieldRect(), m_edit, m_scale, x);
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::OnEditMouseUp()
|
||||
{
|
||||
if (!m_edit.Dragging())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_edit.EndDrag();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::ContainsPoint(int x, int y) const
|
||||
{
|
||||
if (!m_open)
|
||||
|
||||
+31
-15
@@ -550,6 +550,36 @@ namespace jdi
|
||||
return std::isfinite(value) ? value : 1.0;
|
||||
}
|
||||
|
||||
void DataSet::RemapSelectionAfterFileRemoval(int fileIndex)
|
||||
{
|
||||
if (fileIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (m_selected.fileIndex == fileIndex)
|
||||
{
|
||||
m_selected = SignalRef();
|
||||
}
|
||||
else if (m_selected.fileIndex > fileIndex)
|
||||
{
|
||||
--m_selected.fileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
bool DataSet::ValidateSelection()
|
||||
{
|
||||
if (!m_selected.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Resolve(m_selected) != nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_selected = SignalRef();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================ 采样辅助 ============================
|
||||
|
||||
int FindNearestSample(const DataFile& file, double time)
|
||||
@@ -686,12 +716,7 @@ namespace jdi
|
||||
m_visible[fileIndex].resize(file->signals.size(), false);
|
||||
}
|
||||
|
||||
// 清理已失效的选中状态与网格引用。
|
||||
if (m_selected.IsValid() && dataSet.Resolve(m_selected) == nullptr)
|
||||
{
|
||||
m_selected = SignalRef();
|
||||
}
|
||||
|
||||
// 清理已失效的网格引用(选中状态由 DataSet 统一维护)。
|
||||
for (Lane& lane : m_lanes)
|
||||
{
|
||||
std::vector<SignalRef>& refs = lane.signals;
|
||||
@@ -741,15 +766,6 @@ namespace jdi
|
||||
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
|
||||
[](const Lane& lane) { return lane.signals.empty(); }),
|
||||
m_lanes.end());
|
||||
|
||||
if (m_selected.fileIndex == fileIndex)
|
||||
{
|
||||
m_selected = SignalRef();
|
||||
}
|
||||
else if (m_selected.fileIndex > fileIndex)
|
||||
{
|
||||
--m_selected.fileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
bool TabConfig::IsVisible(const SignalRef& ref) const
|
||||
|
||||
+146
-39
@@ -428,6 +428,101 @@ namespace jdi
|
||||
knobRadius * 2.0f);
|
||||
}
|
||||
|
||||
float UiEditTextLeft(const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale,
|
||||
float& innerWidth)
|
||||
{
|
||||
innerWidth = std::max(4.0f, rect.Width - 12.0f * scale);
|
||||
const float textLeft = rect.X + 6.0f * scale;
|
||||
|
||||
const std::wstring& text = edit.Text();
|
||||
if (text.empty())
|
||||
{
|
||||
return textLeft;
|
||||
}
|
||||
|
||||
const float textWidth = UiMeasureTextWidth(font, text);
|
||||
if (textWidth <= innerWidth)
|
||||
{
|
||||
return textLeft;
|
||||
}
|
||||
|
||||
// 保证插入符可见:插入符超出右边界时向左滚动,并夹到总可滚动范围内。
|
||||
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
|
||||
float scroll = 0.0f;
|
||||
if (caretX > innerWidth)
|
||||
{
|
||||
scroll = caretX - innerWidth;
|
||||
}
|
||||
const float maximum = textWidth - innerWidth;
|
||||
scroll = std::max(0.0f, std::min(scroll, maximum));
|
||||
return textLeft - scroll;
|
||||
}
|
||||
|
||||
std::size_t UiEditOffsetAt(HFONT font, const std::wstring& text, float localX)
|
||||
{
|
||||
if (text.empty() || font == nullptr)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (localX <= 0.0f)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 逐字符累加宽度,找到距离最近的字符边界(文本通常很短,线性扫描足够)。
|
||||
float previous = 0.0f;
|
||||
for (std::size_t index = 0; index < text.size(); ++index)
|
||||
{
|
||||
const float current = UiMeasureTextWidth(font, text.substr(0, index + 1));
|
||||
if (localX < current)
|
||||
{
|
||||
// 落在本字符的左半 → 取左边界,右半 → 取右边界。
|
||||
return (localX - previous <= current - localX) ? index : (index + 1);
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
return text.size();
|
||||
}
|
||||
|
||||
void UiTextAtClipped(gd::Graphics& g, const std::wstring& text, HFONT font, float x, const gd::RectF& rect,
|
||||
std::uint32_t argb, const gd::RectF& clipRect)
|
||||
{
|
||||
if (text.empty() || font == nullptr || clipRect.Width <= 0.0f || clipRect.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HDC hdc = g.GetHDC();
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||||
const int oldMode = SetBkMode(hdc, TRANSPARENT);
|
||||
const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb));
|
||||
|
||||
TEXTMETRICW metrics = {};
|
||||
GetTextMetricsW(hdc, &metrics);
|
||||
const int baselineY = static_cast<int>(
|
||||
std::floor(rect.Y + (rect.Height - static_cast<float>(metrics.tmHeight)) * 0.5f));
|
||||
|
||||
RECT clip = {};
|
||||
clip.left = static_cast<LONG>(std::floor(clipRect.X));
|
||||
clip.top = static_cast<LONG>(std::floor(clipRect.Y));
|
||||
clip.right = static_cast<LONG>(std::ceil(clipRect.X + clipRect.Width));
|
||||
clip.bottom = static_cast<LONG>(std::ceil(clipRect.Y + clipRect.Height));
|
||||
|
||||
// ETO_CLIPPED 让 GDI 在指定矩形内裁剪,长文本不会再画到输入框之外。
|
||||
ExtTextOutW(hdc, static_cast<int>(std::floor(x)), baselineY, ETO_CLIPPED, &clip, text.c_str(),
|
||||
static_cast<UINT>(text.size()), nullptr);
|
||||
|
||||
SetTextColor(hdc, oldColor);
|
||||
SetBkMode(hdc, oldMode);
|
||||
SelectObject(hdc, oldFont);
|
||||
g.ReleaseHDC(hdc);
|
||||
}
|
||||
|
||||
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale)
|
||||
{
|
||||
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
|
||||
@@ -442,70 +537,52 @@ namespace jdi
|
||||
UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f);
|
||||
|
||||
const std::wstring& text = edit.Text();
|
||||
const float innerLeft = rect.X + 6.0f * scale;
|
||||
|
||||
float innerWidth = 0.0f;
|
||||
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
|
||||
const gd::RectF clipRect(innerLeft, rect.Y + 1.0f * scale, innerWidth, rect.Height - 2.0f * scale);
|
||||
|
||||
if (text.empty())
|
||||
{
|
||||
// 空内容仍绘制插入符,保持焦点可见。
|
||||
const float caretX = rect.X + 6.0f * scale;
|
||||
UiDrawLine(g, caretX, rect.Y + 4.0f * scale, caretX, rect.Y + rect.Height - 4.0f * scale,
|
||||
UiDrawLine(g, textLeft, rect.Y + 4.0f * scale, textLeft, rect.Y + rect.Height - 4.0f * scale,
|
||||
theme.accent, 1.4f * scale);
|
||||
return;
|
||||
}
|
||||
|
||||
const float innerLeft = rect.X + 6.0f * scale;
|
||||
const float innerWidth = std::max(4.0f, rect.Width - 12.0f * scale);
|
||||
|
||||
// 光标与选区端点对应的像素位置(以文本左端为原点)。
|
||||
const float caretX = UiTextWidth(g, text.substr(0, edit.Caret()), font);
|
||||
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
|
||||
const float selBeginX =
|
||||
edit.HasSelection() ? UiTextWidth(g, text.substr(0, edit.SelectionBegin()), font) : 0.0f;
|
||||
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionBegin())) : 0.0f;
|
||||
const float selEndX =
|
||||
edit.HasSelection() ? UiTextWidth(g, text.substr(0, edit.SelectionEnd()), font) : 0.0f;
|
||||
const float textWidth = UiTextWidth(g, text, font);
|
||||
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionEnd())) : 0.0f;
|
||||
|
||||
// 内容超出可视宽度时横向滚动,保证光标始终可见(向常规输入框靠齐)。
|
||||
float scroll = 0.0f;
|
||||
if (textWidth > innerWidth)
|
||||
{
|
||||
if (caretX > innerWidth)
|
||||
{
|
||||
scroll = caretX - innerWidth;
|
||||
}
|
||||
const float maximum = textWidth - innerWidth;
|
||||
scroll = std::max(0.0f, std::min(scroll, maximum));
|
||||
}
|
||||
const bool fullSelection =
|
||||
edit.HasSelection() && edit.SelectionBegin() == 0 && edit.SelectionEnd() == text.size();
|
||||
|
||||
const gd::RectF clipRect(innerLeft, rect.Y + 1.0f * scale, innerWidth, rect.Height - 2.0f * scale);
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(clipRect, gd::CombineModeIntersect);
|
||||
|
||||
const float textLeft = innerLeft - scroll;
|
||||
|
||||
// 选区高亮绘制在文本之下,整段选中时呈现“已全选”观感。
|
||||
const bool fullSelection = edit.HasSelection() && edit.SelectionBegin() == 0 &&
|
||||
edit.SelectionEnd() == text.size();
|
||||
// 选区高亮绘制在文本之下,整段选中时呈现“已全选”观感;
|
||||
// 叠加上 GDI+ 裁剪,高亮同样不会越过输入框边界。
|
||||
if (edit.HasSelection() && selEndX > selBeginX)
|
||||
{
|
||||
const gd::RectF highlight(textLeft + selBeginX, rect.Y + 3.0f * scale,
|
||||
selEndX - selBeginX, rect.Height - 6.0f * scale);
|
||||
const gd::RectF highlight(textLeft + selBeginX, rect.Y + 3.0f * scale, selEndX - selBeginX,
|
||||
rect.Height - 6.0f * scale);
|
||||
UiFillRect(g, highlight, theme.accent);
|
||||
}
|
||||
|
||||
UiText(g, text, font, gd::RectF(textLeft, rect.Y, std::max(textWidth, innerWidth), rect.Height),
|
||||
theme.textStrong, TextAlign::LEFT);
|
||||
// 长文本用 ETO_CLIPPING 交给 GDI 裁剪,避免字符画到输入框之外。
|
||||
UiTextAtClipped(g, text, font, textLeft, rect,
|
||||
fullSelection ? theme.textOnAccent : theme.textStrong, clipRect);
|
||||
|
||||
if (edit.HasSelection() && !fullSelection && selEndX > selBeginX)
|
||||
if (!fullSelection && edit.HasSelection() && selEndX > selBeginX)
|
||||
{
|
||||
// 局部选区:在反白底衬上重绘被选中的片段,其余文字保持常规颜色。
|
||||
const std::wstring selected =
|
||||
text.substr(edit.SelectionBegin(), edit.SelectionEnd() - edit.SelectionBegin());
|
||||
UiText(g, selected, font,
|
||||
gd::RectF(textLeft + selBeginX, rect.Y, selEndX - selBeginX, rect.Height),
|
||||
theme.textOnAccent, TextAlign::LEFT);
|
||||
}
|
||||
else if (fullSelection)
|
||||
{
|
||||
UiText(g, text, font, gd::RectF(textLeft, rect.Y, std::max(textWidth, innerWidth), rect.Height),
|
||||
theme.textOnAccent, TextAlign::LEFT);
|
||||
UiTextAtClipped(g, selected, font, textLeft + selBeginX, rect, theme.textOnAccent, clipRect);
|
||||
}
|
||||
|
||||
// 插入符。
|
||||
@@ -516,6 +593,36 @@ namespace jdi
|
||||
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);
|
||||
|
||||
@@ -277,6 +277,7 @@ namespace jdi
|
||||
result.scope = m_scope;
|
||||
result.id = item.id;
|
||||
result.swatchIndex = -1;
|
||||
result.itemRect = m_itemRects[static_cast<std::size_t>(m_hoverIndex)];
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -353,6 +354,7 @@ namespace jdi
|
||||
result.scope = m_scope;
|
||||
result.id = item.id;
|
||||
result.swatchIndex = -1;
|
||||
result.itemRect = m_itemRects[static_cast<std::size_t>(index)];
|
||||
|
||||
if (item.kind == MenuItemKind::SWATCHES)
|
||||
{
|
||||
|
||||
@@ -71,6 +71,7 @@ namespace jdi
|
||||
{
|
||||
m_active = false;
|
||||
m_fresh = false;
|
||||
m_dragging = false;
|
||||
m_text.clear();
|
||||
m_caret = 0;
|
||||
m_anchor = 0;
|
||||
@@ -321,6 +322,50 @@ namespace jdi
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool TextEdit::PlaceCaret(std::size_t offset)
|
||||
{
|
||||
if (!m_active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t target = ClampOffset(offset);
|
||||
const bool changed = HasSelection() || (m_caret != target);
|
||||
m_caret = target;
|
||||
m_anchor = target;
|
||||
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())
|
||||
|
||||
Reference in new issue
Block a user