fix: 修正滑块填充残留、数值内联输入与信号/分组重命名,拖拽虚影跟随鼠标

- 滑块最小值时不再有超出圆钮的填充残留,三处滑块统一为共享实现
- 透明度/RGB/色号支持点击或双击直接输入,整段高亮、首次键入替换、回车提交、Ctrl+C 复制
- 双击信号行或分组名进入重命名编辑;分组竖线改为绘制而非字符
- 拖拽信号时曲线虚影跟随鼠标纵向位移
- 数据层新增展示名覆盖,面板改为持有可写数据集合
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 09:38:22 +08:00
1 parent 542aa51e40
commit 261b8ac07d
13 files changed
+1074 -274

No files matched your search

+297 -23
View File
@@ -2,6 +2,9 @@
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include "Utf8Util.hpp"
namespace jdi
{
@@ -30,7 +33,7 @@ namespace jdi
const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
/**
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
* @brief 绘制水平滑块(与样式区共用同一实现,保证行为一致)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] track 轨道矩形。
* @param [IN] ratio 当前值比例(0-1)。
@@ -41,22 +44,7 @@ namespace jdi
void DrawChannelSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
bool hovered, float scale)
{
const float radius = track.Height * 0.5f;
UiFillRoundRect(g, track, radius, 0x2A657B83u);
ratio = std::max(0.0, std::min(1.0, ratio));
const float fillWidth = std::max(track.Height, static_cast<float>(ratio) * track.Width);
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
const float knobRadius = radius + (hovered ? 2.5f * scale : 0.8f * scale);
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
knobRadius * 2.0f);
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
knobRadius * 2.0f);
UiDrawSlider(g, track, ratio, fillColor, hovered, scale);
}
/**
@@ -179,11 +167,238 @@ namespace jdi
void ColorPickerPopup::Close()
{
// 收起前先提交正在编辑的取值,避免用户输入被静默丢弃。
if (m_editField != EditField::NONE)
{
EndEdit(true);
}
m_open = false;
m_draggingSlider = -1;
m_hoverSlider = -1;
}
gd::RectF ColorPickerPopup::EditFieldRect() const
{
switch (m_editField)
{
case EditField::R:
return m_values[0];
case EditField::G:
return m_values[1];
case EditField::B:
return m_values[2];
case EditField::HEX:
return m_hexRect;
case EditField::NONE:
default:
return gd::RectF();
}
}
bool ColorPickerPopup::BeginEdit(EditField field)
{
if (!m_open || field == EditField::NONE)
{
return false;
}
if (m_editField == field)
{
return false;
}
switch (field)
{
case EditField::R:
case EditField::G:
case EditField::B:
{
const int index = static_cast<int>(field) - 1;
m_editText = std::to_wstring(ToByte(m_channels[static_cast<std::size_t>(index)]));
break;
}
case EditField::HEX:
{
const std::uint32_t color = CurrentColor();
wchar_t buffer[16];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%02X%02X%02X", (color >> 16) & 0xFFu,
(color >> 8) & 0xFFu, color & 0xFFu);
m_editText = std::wstring(buffer);
break;
}
default:
return false;
}
m_draggingSlider = -1;
m_editField = field;
m_editFresh = true;
return true;
}
bool ColorPickerPopup::EndEdit(bool commit)
{
if (m_editField == EditField::NONE)
{
return false;
}
const EditField field = m_editField;
const std::wstring text = m_editText;
m_editField = EditField::NONE;
m_editFresh = false;
m_editText.clear();
if (!commit)
{
return true;
}
const std::wstring trimmed = Trim(text);
if (field == EditField::HEX)
{
// 接受带或不带 '#' 的 6 位十六进制色号。
std::wstring digits = trimmed;
if (!digits.empty() && digits.front() == L'#')
{
digits.erase(digits.begin());
}
if (digits.size() == 6)
{
unsigned int value = 0;
bool valid = true;
for (const wchar_t c : digits)
{
unsigned int digit = 0;
if (c >= L'0' && c <= L'9')
{
digit = static_cast<unsigned int>(c - L'0');
}
else if (c >= L'a' && c <= L'f')
{
digit = static_cast<unsigned int>(c - L'a') + 10u;
}
else if (c >= L'A' && c <= L'F')
{
digit = static_cast<unsigned int>(c - L'A') + 10u;
}
else
{
valid = false;
break;
}
value = (value << 4) | digit;
}
if (valid)
{
m_channels[0] = static_cast<double>((value >> 16) & 0xFFu) / 255.0;
m_channels[1] = static_cast<double>((value >> 8) & 0xFFu) / 255.0;
m_channels[2] = static_cast<double>(value & 0xFFu) / 255.0;
m_changed = true;
}
}
return true;
}
if (trimmed.empty())
{
return true;
}
wchar_t* stop = nullptr;
const double parsed = std::wcstod(trimmed.c_str(), &stop);
if (stop == trimmed.c_str() || parsed != parsed)
{
return true;
}
double value = parsed;
if (value < 0.0)
{
value = 0.0;
}
if (value > 255.0)
{
value = 255.0;
}
const int index = static_cast<int>(field) - 1;
if (index >= 0 && index < 3)
{
m_channels[static_cast<std::size_t>(index)] = value / 255.0;
m_changed = true;
}
return true;
}
bool ColorPickerPopup::OnChar(wchar_t character)
{
if (m_editField == EditField::NONE)
{
return false;
}
// Ctrl+C:把当前文本复制到剪贴板。
if (character == 0x03)
{
CopyTextToClipboard(nullptr, m_editText);
return false;
}
if (character == L'\r' || character == L'\n')
{
return EndEdit(true);
}
if (character == L'\b')
{
m_editFresh = false;
if (!m_editText.empty())
{
m_editText.pop_back();
}
return true;
}
if (character < 0x20 || character == 0x7F)
{
return true;
}
if (m_editField == EditField::HEX)
{
const bool hexDigit = (character >= L'0' && character <= L'9') ||
(character >= L'a' && character <= L'f') ||
(character >= L'A' && character <= L'F') || (character == L'#');
if (!hexDigit)
{
return true;
}
if (m_editFresh)
{
m_editText.clear();
m_editFresh = false;
}
if (m_editText.size() < 7)
{
m_editText.push_back(character);
}
return true;
}
const bool digit = (character >= L'0' && character <= L'9');
if (!digit)
{
return true;
}
if (m_editFresh)
{
m_editText.clear();
m_editFresh = false;
}
if (m_editText.size() < 3)
{
m_editText.push_back(character);
}
return true;
}
bool ColorPickerPopup::ContainsPoint(int x, int y) const
{
if (!m_open)
@@ -283,6 +498,29 @@ namespace jdi
return true;
}
// 点击当前输入框以外的位置时提交输入。
if (m_editField != EditField::NONE && !UiPointInRect(EditFieldRect(), static_cast<float>(x),
static_cast<float>(y)))
{
EndEdit(true);
}
// 数值与色号文本优先于滑块:两者几何重叠时要能进入输入状态。
for (int index = 0; index < 3; ++index)
{
if (UiPointInRect(m_values[static_cast<std::size_t>(index)], static_cast<float>(x),
static_cast<float>(y)))
{
BeginEdit(static_cast<EditField>(index + 1));
return true;
}
}
if (UiPointInRect(m_hexRect, static_cast<float>(x), static_cast<float>(y)))
{
BeginEdit(EditField::HEX);
return true;
}
m_draggingSlider = SliderAtPoint(x, y);
if (m_draggingSlider >= 0)
{
@@ -308,6 +546,28 @@ namespace jdi
{
return false;
}
if (m_editField != EditField::NONE)
{
switch (key)
{
case VK_RETURN:
EndEdit(true);
return true;
case VK_ESCAPE:
EndEdit(false);
return true;
case VK_BACK:
m_editFresh = false;
if (!m_editText.empty())
{
m_editText.pop_back();
}
return true;
default:
break;
}
return true;
}
if (key == VK_ESCAPE)
{
Close();
@@ -347,8 +607,15 @@ namespace jdi
const bool active = (m_draggingSlider == index) || (m_hoverSlider == index);
DrawChannelSlider(g, track, m_channels[slot], kChannelColors[index], active, m_scale);
UiText(g, std::to_wstring(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
m_theme->text, TextAlign::RIGHT);
if (m_editField == static_cast<EditField>(index + 1))
{
UiDrawEditField(g, m_values[slot], m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
}
else
{
UiText(g, std::to_wstring(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
m_theme->text, TextAlign::RIGHT);
}
}
// 预览与十六进制值。
@@ -356,10 +623,17 @@ namespace jdi
UiFillRoundRect(g, m_previewRect, 3.0f * m_scale, color);
UiDrawRoundRect(g, m_previewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
wchar_t buffer[16];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
(color >> 8) & 0xFFu, color & 0xFFu);
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text, TextAlign::LEFT);
if (m_editField == EditField::HEX)
{
UiDrawEditField(g, m_hexRect, m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
}
else
{
wchar_t buffer[16];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
(color >> 8) & 0xFFu, color & 0xFFu);
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text, TextAlign::LEFT);
}
}
} // namespace jdi
+41
View File
@@ -446,6 +446,47 @@ namespace jdi
return &m_files[static_cast<std::size_t>(fileIndex)];
}
bool DataSet::SetSignalDisplayName(int fileIndex, int signalIndex, const std::wstring& name)
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
if (signalIndex < 0 || signalIndex >= static_cast<int>(file.signals.size()))
{
return false;
}
// 统一去除首尾空白;结果为空则视为恢复原始列名。
std::wstring trimmed = Trim(name);
SignalData& signal = file.signals[static_cast<std::size_t>(signalIndex)];
if (trimmed == signal.displayName)
{
return false;
}
signal.displayName = std::move(trimmed);
return true;
}
bool DataSet::SetFileDisplayName(int fileIndex, const std::wstring& name)
{
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
std::wstring trimmed = Trim(name);
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
if (trimmed == file.displayName)
{
return false;
}
file.displayName = std::move(trimmed);
return true;
}
const SignalData* DataSet::Resolve(const SignalRef& ref) const
{
const DataFile* file = FileAt(ref.fileIndex);
+77
View File
@@ -419,6 +419,83 @@ namespace jdi
}
}
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
bool active, float scale)
{
if (track.Width <= 0.0f || track.Height <= 0.0f)
{
return;
}
if (ratio < 0.0)
{
ratio = 0.0;
}
if (ratio > 1.0)
{
ratio = 1.0;
}
const float radius = track.Height * 0.5f;
UiFillRoundRect(g, track, radius, 0x2A657B83u);
// 已填充部分严格止于圆钮中心,最小值时不会留下超出圆钮的填充残留。
const float fillWidth = static_cast<float>(ratio) * track.Width;
if (fillWidth > 0.5f)
{
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
}
const float knobRadius = radius + (active ? 2.5f * scale : 0.8f * scale);
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
knobRadius * 2.0f);
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
knobRadius * 2.0f);
}
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const std::wstring& text, bool fresh,
HFONT font, float scale)
{
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
{
return;
}
const Theme& theme = SolarizedLight();
const float corner = 3.0f * scale;
UiFillRoundRect(g, rect, corner, theme.cardBg);
UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f);
const std::wstring shown = text.empty() ? std::wstring(L" ") : text;
if (fresh)
{
// 整段内容处于“将被覆盖”状态:以强调色底衬表示已全选。
const float textWidth = UiTextWidth(g, shown, font) + 8.0f * scale;
const gd::RectF highlight(rect.X + (rect.Width - textWidth) * 0.5f, rect.Y + 2.0f * scale,
textWidth, rect.Height - 4.0f * scale);
UiFillRoundRect(g, highlight, 2.0f * scale, theme.accent);
UiText(g, shown, font, highlight, theme.textOnAccent, TextAlign::CENTER);
return;
}
UiText(g, shown, font, rect, theme.textStrong, TextAlign::CENTER);
// 已开始键入:在文本末尾显示插入符。
const float textWidth = UiTextWidth(g, shown, font);
const float caretX = rect.X + (rect.Width + textWidth) * 0.5f + 1.0f * scale;
if (caretX < rect.X + rect.Width - 2.0f * scale)
{
UiDrawLine(g, caretX, rect.Y + 4.0f * scale, caretX, rect.Y + rect.Height - 4.0f * scale,
theme.accent, 1.4f * scale);
}
}
bool UiSwatchNeedsOutline(float opacity)
{
return !(opacity >= kSwatchOutlineOpacity);
+44
View File
@@ -367,4 +367,48 @@ namespace jdi
}
}
bool CopyTextToClipboard(HWND owner, const std::wstring& text)
{
if (text.empty())
{
return false;
}
if (OpenClipboard(owner) == FALSE)
{
return false;
}
bool success = false;
if (EmptyClipboard() != FALSE)
{
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes);
if (handle != nullptr)
{
void* target = GlobalLock(handle);
if (target != nullptr)
{
std::memcpy(target, text.c_str(), bytes);
GlobalUnlock(handle);
if (SetClipboardData(CF_UNICODETEXT, handle) != nullptr)
{
success = true;
}
else
{
GlobalFree(handle);
}
}
else
{
GlobalFree(handle);
}
}
}
CloseClipboard();
return success;
}
} // namespace jdi