Files
Just-DataInspector/Utilities/Src/ColorPickerPopup.cpp
T
DeepSeek V4.1 Flash bc8db72f18 修复色域拖拽回弹、网格 GX 拖拽重排、均值滤波多阶与逐信号条目
1. 自定义颜色面板:拖动时鼠标移出控件不再"弹回去"
   - 根因是选点位置每帧都从 RGB 反推:色相 1 与 0 是同一个颜色(红),拖到色相条底部
     下一帧反推回 0,指示条弹到顶部;拖到色域方块右下角之外时明度为 0(黑色),
     黑色的饱和度无定义,反推得 0,选点弹到最左边。
   - 现在把"选点位置(饱和度/明度)与色相"作为独立状态保存,只由对应控件与外部改色维护;
     越界一律夹取到最近边界,因此鼠标离开控件后选点停在边界上。
   - 统一色域方块、色相条、预览色块与色号框的圆角;RGB 滑块适当加粗;
     色域选点去掉外圈深色描边(只保留白色描边),色相指示器改为白色描边的圆角长方条、
     底色取该位置的颜色;色相条改用分段线性渐变绘制。

2. 网格 GX 编号支持拖拽重排
   - 按住网格图例左侧的 GX 徽标即可上下拖动,整块网格(连同波形与标尺)跟随鼠标移动;
     途经的网格保持自身大小、整体让出一格,目标槽位有吸附高亮提示。
   - 松开后真正重排并从下到上/从上到下重新编号,信号列表徽标、图例徽标、菜单与浮动面板
     中所有 GX 编号同步更新;手动排布后切换为自定义划分,后续重建网格不会打乱次序。
   - 拖动期间复用静态场景缓存,按网格条带平移贴图,因此大文件下依然流畅。

3. 均值滤波升级为"均值点数 X + 阶数 N"
   - 原"均值点数 N"更名"均值点数 X",新增"阶数 N":预览曲线以 X 点均值滤波的结果为蓝本,
     再重复同样的滤波共 N 阶(1 阶即与原来的单次滤波逐位一致)。
   - 浮动面板改为逐信号条目:第一排是信号名与复选框,第二排是均值点数与阶数输入;
     只有勾选的信号才绘制预览曲线,未勾选时不显示第二排;启用功能时默认只勾选第一个信号。
   - 参数按信号保存(同一网格内不同信号可用不同参数),「构建为新信号」按各自参数构建。
2026-10-10 15:33:45 +08:00

1030 lines
30 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "ColorPickerPopup.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include "Utf8Util.hpp"
namespace jdi
{
namespace
{
const float kPanelWidth = 324.0f; /**< 面板宽度(逻辑像素)。 */
const float kPadH = 12.0f; /**< 面板左右内边距。 */
const float kPadV = 10.0f; /**< 面板上下内边距。 */
const float kTitleHeight = 20.0f; /**< 标题行高度。 */
const float kTitleGap = 8.0f; /**< 标题与内容区间距。 */
const float kRowHeight = 26.0f; /**< 单个滑块行高度。 */
const float kRowGap = 2.0f; /**< 滑块行间距。 */
const float kPreviewGap = 10.0f; /**< 滑块区与预览区间距。 */
const float kPreviewHeight = 24.0f; /**< 预览行高度。 */
const float kLabelWidth = 16.0f; /**< 通道标签宽度。 */
const float kLabelGap = 6.0f; /**< 标签与滑块间距。 */
const float kValueWidth = 40.0f; /**< 数值文本宽度。 */
const float kTrackHeight = 8.0f; /**< 滑块轨道高度。 */
const float kCorner = 6.0f; /**< 面板圆角半径。 */
/** 色域方块 / 色相条 / 预览色块 / 色号框共用同一圆角(视觉上统一)。 */
const float kSwatchCorner = 3.0f;
const float kGamutSize = 112.0f; /**< 色域方块边长。 */
const float kHueWidth = 14.0f; /**< 色相条宽度。 */
const float kGamutGap = 10.0f; /**< 色域方块与色相条间距。 */
const float kColumnGap = 12.0f; /**< 色相条与滑块列间距。 */
const float kSwatchWidth = 52.0f; /**< 预览色块宽度。 */
const float kSwatchHexGap = 8.0f; /**< 预览色块与色号间距。 */
const float kGamutBorder = 1.0f; /**< 色域方块/色相条描边宽度。 */
/** @brief 通道显示名称。 */
const wchar_t* kChannelNames[3] = {L"R", L"G", L"B"};
/** @brief 通道滑块填充色。 */
const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
/**
* @brief 把数值限制到 0-1。
* @param [IN] value 输入值。
* @return 限制后的值。
*/
double Clamp01(double value)
{
if (value < 0.0)
{
return 0.0;
}
if (value > 1.0)
{
return 1.0;
}
return value;
}
/**
* @brief HSV 颜色(色相/饱和度/明度,均为 0-1)。
*/
struct Hsv
{
double h = 0.0; /**< 色相。 */
double s = 0.0; /**< 饱和度。 */
double v = 0.0; /**< 明度。 */
};
/**
* @brief RGB(0-1 分量)转 HSV。
* @param [IN] rgb RGB 分量。
* @return HSV 值(灰色时色相为 0)。
*/
Hsv ToHsv(const std::array<double, 3>& rgb)
{
const double maximum = std::max(rgb[0], std::max(rgb[1], rgb[2]));
const double minimum = std::min(rgb[0], std::min(rgb[1], rgb[2]));
const double delta = maximum - minimum;
Hsv hsv;
hsv.v = maximum;
hsv.s = (maximum > 1.0e-9) ? (delta / maximum) : 0.0;
if (delta <= 1.0e-9)
{
hsv.h = 0.0;
return hsv;
}
double hue = 0.0;
if (maximum == rgb[0])
{
hue = (rgb[1] - rgb[2]) / delta;
}
else if (maximum == rgb[1])
{
hue = 2.0 + (rgb[2] - rgb[0]) / delta;
}
else
{
hue = 4.0 + (rgb[0] - rgb[1]) / delta;
}
hue /= 6.0;
if (hue < 0.0)
{
hue += 1.0;
}
hsv.h = hue;
return hsv;
}
/**
* @brief HSV 转 RGB(0-1 分量)。
* @param [IN] h 色相(0-1,自动取模)。
* @param [IN] s 饱和度(0-1)。
* @param [IN] v 明度(0-1)。
* @return RGB 分量。
*/
std::array<double, 3> FromHsv(double h, double s, double v)
{
h = h - std::floor(h);
s = Clamp01(s);
v = Clamp01(v);
if (s <= 1.0e-9)
{
return {v, v, v};
}
const double sector = h * 6.0;
const int index = static_cast<int>(std::floor(sector)) % 6;
const double fraction = sector - std::floor(sector);
const double p = v * (1.0 - s);
const double q = v * (1.0 - s * fraction);
const double t = v * (1.0 - s * (1.0 - fraction));
switch (index)
{
case 0:
return {v, t, p};
case 1:
return {q, v, p};
case 2:
return {p, v, t};
case 3:
return {p, q, v};
case 4:
return {t, p, v};
default:
return {v, p, q};
}
}
/**
* @brief 绘制水平滑块(与样式区共用同一实现,保证行为一致)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] track 轨道矩形。
* @param [IN] ratio 当前值比例(0-1)。
* @param [IN] fillColor 已填充部分颜色。
* @param [IN] hovered 是否处于悬停或拖拽状态。
* @param [IN] scale 缩放比例。
*/
void DrawChannelSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
bool hovered, float scale)
{
UiDrawSlider(g, track, ratio, fillColor, hovered, scale);
}
/**
* @brief 将 0-1 分量换算为 0-255 整数。
* @param [IN] value 分量值。
* @return 整数分量。
*/
unsigned int ToByte(double value)
{
const double scaled = value * 255.0 + 0.5;
if (scaled < 0.0)
{
return 0u;
}
if (scaled > 255.0)
{
return 255u;
}
return static_cast<unsigned int>(scaled);
}
}
ColorPickerPopup::ColorPickerPopup() = default;
void ColorPickerPopup::Setup(const UiFonts* fonts, const Theme* theme)
{
m_fonts = fonts;
m_theme = theme;
}
void ColorPickerPopup::SetScale(float scale)
{
m_scale = (scale > 0.1f) ? scale : 1.0f;
}
std::uint32_t ColorPickerPopup::CurrentColor() const
{
const unsigned int r = ToByte(m_channels[0]);
const unsigned int g = ToByte(m_channels[1]);
const unsigned int b = ToByte(m_channels[2]);
return 0xFF000000u | (r << 16) | (g << 8) | b;
}
bool ColorPickerPopup::TakeChanged(std::uint32_t& color)
{
if (!m_changed)
{
return false;
}
m_changed = false;
color = CurrentColor();
return true;
}
void ColorPickerPopup::BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor)
{
// 布局:左侧一列放"色域方块 + 色相条",右侧一列放 R/G/B 三行
//(每行 = 通道标签 + 滑块 + 数值),两列下方再放"预览色块 + 色号"一行。
// 所有控件都从面板内边距起算,因此不会出现越界。
const float width = kPanelWidth * m_scale;
const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + kGamutSize + kPreviewGap +
kPreviewHeight) *
m_scale;
const float gap = 4.0f * m_scale;
// 纵向:优先紧贴触发控件下方展开;空间不足时改到其上方,避免遮挡触发控件本身。
float top = anchor.Y + anchor.Height + gap;
if (top + height > bounds.Y + bounds.Height)
{
top = anchor.Y - height - gap;
}
top = std::max(bounds.Y + 2.0f, std::min(top, bounds.Y + bounds.Height - height - 2.0f));
// 横向:与触发控件左对齐,越界时再收敛。
float left = anchor.X;
if (left + width > bounds.X + bounds.Width)
{
left = anchor.X + anchor.Width - width;
}
left = std::max(bounds.X + 2.0f, std::min(left, bounds.X + bounds.Width - width - 2.0f));
m_rect = gd::RectF(left, top, width, height);
m_titleRect = gd::RectF(left + kPadH * m_scale, top + kPadV * m_scale,
width - kPadH * 2.0f * m_scale, kTitleHeight * m_scale);
const float contentTop = m_titleRect.Y + m_titleRect.Height + kTitleGap * m_scale;
const float contentLeft = left + kPadH * m_scale;
const float contentRight = left + width - kPadH * m_scale;
// 左列:色域方块(横轴饱和度、纵轴明度)+ 色相条。
m_gamutRect = gd::RectF(contentLeft, contentTop, kGamutSize * m_scale, kGamutSize * m_scale);
m_hueRect = gd::RectF(m_gamutRect.X + m_gamutRect.Width + kGamutGap * m_scale, contentTop,
kHueWidth * m_scale, kGamutSize * m_scale);
// 右列:R/G/B 三行。整列在方块高度内纵向居中,与左侧方块视觉平衡。
const float leftColumnWidth =
(kGamutSize + kGamutGap + kHueWidth) * m_scale;
const float columnLeft = m_gamutRect.X + leftColumnWidth + kColumnGap * m_scale;
const float columnWidth = std::max(60.0f * m_scale, contentRight - columnLeft);
const float rowsHeight = (kRowHeight * 3.0f + kRowGap * 2.0f) * m_scale;
const float firstRowTop =
contentTop + std::max(0.0f, (kGamutSize * m_scale - rowsHeight) * 0.5f);
const float trackLeft = columnLeft + (kLabelWidth + kLabelGap) * m_scale;
const float trackWidth =
std::max(24.0f, columnWidth - (kLabelWidth + kLabelGap + kValueWidth + 6.0f) * m_scale);
for (int index = 0; index < 3; ++index)
{
const float rowTop = firstRowTop + static_cast<float>(index) * (kRowHeight + kRowGap) * m_scale;
m_tracks[static_cast<std::size_t>(index)] =
gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f,
trackWidth, kTrackHeight * m_scale);
m_values[static_cast<std::size_t>(index)] =
gd::RectF(trackLeft + trackWidth + 6.0f * m_scale, rowTop, kValueWidth * m_scale,
kRowHeight * m_scale);
}
// 下行:预览色块 + 色号,与上方的"方块 + 色相条"同宽。
const float previewTop = contentTop + (kGamutSize + kPreviewGap) * m_scale;
m_previewRect =
gd::RectF(contentLeft, previewTop, kSwatchWidth * m_scale, kPreviewHeight * m_scale);
m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + kSwatchHexGap * m_scale,
previewTop,
std::max(40.0f * m_scale, leftColumnWidth - (kSwatchWidth + kSwatchHexGap) * m_scale),
kPreviewHeight * m_scale);
}
void ColorPickerPopup::Open(const gd::RectF& bounds, const gd::RectF& anchor, std::uint32_t color)
{
m_channels[0] = static_cast<double>((color >> 16) & 0xFFu) / 255.0;
m_channels[1] = static_cast<double>((color >> 8) & 0xFFu) / 255.0;
m_channels[2] = static_cast<double>(color & 0xFFu) / 255.0;
m_open = true;
m_changed = false;
m_draggingSlider = -1;
m_hoverSlider = -1;
m_drag = DragTarget::NONE;
// 记住初始色相与选点位置:之后即使把明度拖到 0(黑色)色相/饱和度也不会丢失。
RefreshSelectionFromChannels();
InvalidateGamutCache();
BuildLayout(bounds, anchor);
}
void ColorPickerPopup::Close()
{
// 收起前先提交正在编辑的取值,避免用户输入被静默丢弃。
if (m_editField != EditField::NONE)
{
EndEdit(true);
}
m_open = false;
m_draggingSlider = -1;
m_hoverSlider = -1;
m_drag = DragTarget::NONE;
// 释放色域方块缓存(它是 GDI+ 对象,必须在 GdiplusShutdown 之前销毁)。
// 下次打开时按需重建,代价只有一张小位图的填充。
m_gamutCache.reset();
m_gamutCacheSize = 0;
m_gamutCacheHue = -1.0;
}
double ColorPickerPopup::CurrentHue() const
{
// 直接返回保存的色相选择值(不再从 RGB 反推):
// 色相 1 与 0 是同一个颜色(红),反推会把它们归一到 0,
// 表现为"把滑块拖到最下面却弹回最上面"。
// `RefreshSelectionFromChannels` 会在改色(RGB 滑块 / 色号)后同步这个值。
return m_hue;
}
void ColorPickerPopup::RefreshSelectionFromChannels()
{
const Hsv hsv = ToHsv(m_channels);
// 饱和度足够大时才认色相(灰度时色相无定义,沿用上次取值)。
if (hsv.s > 1.0e-4)
{
m_hue = hsv.h;
}
// 饱和度/明度总是可以从 RGB 反推(明度=最大值,饱和度=max>0 ? delta/max : 0)。
// 注意这是"由 RGB 派生"的路径,只用于外部改色(滑块/色号)后的同步;
// 直接用色域方块时不会走这里,以免黑色把饱和度重置为 0。
m_sat = hsv.s;
m_val = hsv.v;
}
void ColorPickerPopup::InvalidateGamutCache()
{
m_gamutCacheHue = -1.0;
}
bool ColorPickerPopup::EnsureGamutCache()
{
const int size = std::max(8, static_cast<int>(std::lround(kGamutSize * m_scale)));
if (m_gamutCache == nullptr || m_gamutCacheSize != size)
{
m_gamutCache.reset(new gd::Bitmap(size, size, PixelFormat32bppRGB));
m_gamutCacheSize = size;
m_gamutCacheHue = -1.0;
}
const double hue = CurrentHue();
if (std::fabs(m_gamutCacheHue - hue) < 1.0e-4)
{
return true;
}
gd::BitmapData data = {};
const gd::Rect lockRect(0, 0, size, size);
if (m_gamutCache->LockBits(&lockRect, gd::ImageLockModeWrite, PixelFormat32bppRGB, &data) != gd::Ok)
{
return false;
}
unsigned char* base = static_cast<unsigned char*>(data.Scan0);
for (int y = 0; y < size; ++y)
{
// 纵轴明度(上 1 → 下 0),横轴饱和度(左 0 → 右 1)——与常见取色器一致。
const double value = 1.0 - (static_cast<double>(y) + 0.5) / static_cast<double>(size);
unsigned char* row = (data.Stride >= 0)
? (base + static_cast<std::ptrdiff_t>(y) * data.Stride)
: (base + static_cast<std::ptrdiff_t>(size - 1 - y) * (-data.Stride));
for (int x = 0; x < size; ++x)
{
const double saturation =
(static_cast<double>(x) + 0.5) / static_cast<double>(size);
const std::array<double, 3> rgb = FromHsv(hue, saturation, value);
row[x * 4 + 0] = static_cast<unsigned char>(ToByte(rgb[2])); // B
row[x * 4 + 1] = static_cast<unsigned char>(ToByte(rgb[1])); // G
row[x * 4 + 2] = static_cast<unsigned char>(ToByte(rgb[0])); // R
row[x * 4 + 3] = 0xFF;
}
}
m_gamutCache->UnlockBits(&data);
m_gamutCacheHue = hue;
return true;
}
void ColorPickerPopup::ApplyGamut(float x, float y)
{
if (m_gamutRect.Width <= 0.0f || m_gamutRect.Height <= 0.0f)
{
return;
}
// 越出方块时夹取到最近边界(而不是丢失位置):鼠标拖到方块之外时选点
// 会停在对应角落,与鼠标相对方块的方位一致。
const double saturation = Clamp01((x - m_gamutRect.X) / m_gamutRect.Width);
const double value = 1.0 - Clamp01((y - m_gamutRect.Y) / m_gamutRect.Height);
if (std::fabs(saturation - m_sat) < 1.0e-9 && std::fabs(value - m_val) < 1.0e-9)
{
return;
}
// 先记录选点位置,再推颜色:两者分开保存,明度为 0 时饱和度不会因此丢失。
m_sat = saturation;
m_val = value;
m_channels = FromHsv(CurrentHue(), m_sat, m_val);
m_changed = true;
}
void ColorPickerPopup::ApplyHue(float y)
{
if (m_hueRect.Height <= 0.0f)
{
return;
}
// 越出色相条时同样夹取:拖到下方停在最下面(色相 1),不会因 0/1 等价而弹回顶部。
const double hue = Clamp01((y - m_hueRect.Y) / m_hueRect.Height);
if (std::fabs(hue - m_hue) < 1.0e-9)
{
return;
}
m_hue = hue;
m_channels = FromHsv(m_hue, m_sat, m_val);
m_changed = true;
}
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_edit.Begin(std::to_wstring(ToByte(m_channels[static_cast<std::size_t>(index)])), true,
EditFilter::BYTE, 0u);
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_edit.Begin(std::wstring(buffer), true, EditFilter::HEX, 0u);
break;
}
default:
return false;
}
m_draggingSlider = -1;
m_editField = field;
return true;
}
bool ColorPickerPopup::EndEdit(bool commit)
{
if (m_editField == EditField::NONE)
{
return false;
}
const EditField field = m_editField;
const std::wstring text = m_edit.Text();
m_editField = EditField::NONE;
m_edit.End();
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;
RefreshSelectionFromChannels();
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;
RefreshSelectionFromChannels();
m_changed = true;
}
return true;
}
bool ColorPickerPopup::OnChar(wchar_t character)
{
if (m_editField == EditField::NONE)
{
return false;
}
// 剪贴板与全选快捷键经 WM_CHAR 送达(Ctrl+C/X/V/A 分别对应 3/24/22/1)。
switch (character)
{
case 0x03: // Ctrl+C
m_edit.CopyToClipboard(m_owner);
return false;
case 0x18: // Ctrl+X
return m_edit.CutToClipboard(m_owner);
case 0x16: // Ctrl+V
return m_edit.PasteFromClipboard(m_owner);
case 0x01: // Ctrl+A
m_edit.SelectAll();
return true;
default:
break;
}
if (character == L'\r' || character == L'\n')
{
return EndEdit(true);
}
// 退格/删除已由 WM_KEYDOWN 的 OnKeyDown 处理,此处忽略控制字符避免重复删除。
if (character < 0x20 || character == 0x7F)
{
return false;
}
return m_edit.Insert(character);
}
bool ColorPickerPopup::CollapseEditSelectionToEnd()
{
if (m_editField == EditField::NONE)
{
return false;
}
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)
{
return false;
}
return UiPointInRect(m_rect, static_cast<float>(x), static_cast<float>(y));
}
HCURSOR ColorPickerPopup::QueryCursor(int x, int y) const
{
if (!m_open)
{
return nullptr;
}
// 拖拽过程中始终保持对应光标,避免指针移出控件后光标跳变。
if (m_drag == DragTarget::GAMUT || UiPointInRect(m_gamutRect, static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_CROSS);
}
if (m_drag == DragTarget::HUE ||
UiPointInRect(m_hueRect, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZENS);
}
if (m_drag == DragTarget::SLIDER || m_draggingSlider >= 0 || SliderAtPoint(x, y) >= 0)
{
return LoadCursorW(nullptr, IDC_SIZEWE);
}
return LoadCursorW(nullptr, IDC_ARROW);
}
int ColorPickerPopup::SliderAtPoint(int x, int y) const
{
if (!m_open)
{
return -1;
}
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
for (int index = 0; index < 3; ++index)
{
const gd::RectF& track = m_tracks[static_cast<std::size_t>(index)];
// 命中范围在纵向适当放宽,便于拖拽。
const gd::RectF hit(track.X - 4.0f * m_scale, track.Y - 9.0f * m_scale,
track.Width + 8.0f * m_scale, track.Height + 18.0f * m_scale);
if (UiPointInRect(hit, fx, fy))
{
return index;
}
}
return -1;
}
void ColorPickerPopup::ApplySlider(int index, float x)
{
if (index < 0 || index >= 3)
{
return;
}
const std::size_t slot = static_cast<std::size_t>(index);
const double ratio = UiSliderRatioFromX(m_tracks[slot], x);
if (std::fabs(ratio - m_channels[slot]) < 1.0e-6)
{
return;
}
m_channels[slot] = ratio;
RefreshSelectionFromChannels();
m_changed = true;
}
bool ColorPickerPopup::OnMouseMove(int x, int y)
{
if (!m_open)
{
return false;
}
switch (m_drag)
{
case DragTarget::GAMUT:
ApplyGamut(static_cast<float>(x), static_cast<float>(y));
return true;
case DragTarget::HUE:
ApplyHue(static_cast<float>(y));
return true;
case DragTarget::SLIDER:
ApplySlider(m_draggingSlider, static_cast<float>(x));
return true;
case DragTarget::NONE:
default:
break;
}
const int hover = SliderAtPoint(x, y);
if (hover == m_hoverSlider)
{
return false;
}
m_hoverSlider = hover;
return true;
}
bool ColorPickerPopup::OnLButtonDown(int x, int y)
{
if (!m_open)
{
return false;
}
if (!ContainsPoint(x, y))
{
// 点击面板外部:收起面板(外部点击不再下传,避免误触其他控件)。
Close();
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;
}
// 色域方块与色相条:按下即取值,之后拖动持续更新。
if (UiPointInRect(m_gamutRect, static_cast<float>(x), static_cast<float>(y)))
{
m_drag = DragTarget::GAMUT;
ApplyGamut(static_cast<float>(x), static_cast<float>(y));
return true;
}
if (UiPointInRect(m_hueRect, static_cast<float>(x), static_cast<float>(y)))
{
m_drag = DragTarget::HUE;
ApplyHue(static_cast<float>(y));
return true;
}
m_draggingSlider = SliderAtPoint(x, y);
if (m_draggingSlider >= 0)
{
m_drag = DragTarget::SLIDER;
ApplySlider(m_draggingSlider, static_cast<float>(x));
return true;
}
return true;
}
bool ColorPickerPopup::OnLButtonUp()
{
if (!m_open || (m_draggingSlider < 0 && m_drag == DragTarget::NONE))
{
return false;
}
m_draggingSlider = -1;
m_drag = DragTarget::NONE;
return true;
}
bool ColorPickerPopup::OnKeyDown(WPARAM key)
{
if (!m_open)
{
return false;
}
if (m_editField != EditField::NONE)
{
const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
const bool control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
switch (key)
{
case VK_RETURN:
EndEdit(true);
return true;
case VK_ESCAPE:
EndEdit(false);
return true;
case VK_BACK:
m_edit.Backspace();
return true;
case VK_DELETE:
m_edit.DeleteForward();
return true;
case VK_LEFT:
m_edit.MoveCaret(-1, shift);
return true;
case VK_RIGHT:
m_edit.MoveCaret(1, shift);
return true;
case VK_HOME:
m_edit.MoveHome(shift);
return true;
case VK_END:
m_edit.MoveEnd(shift);
return true;
case 'A':
if (control)
{
m_edit.SelectAll();
}
return true;
default:
break;
}
return true;
}
if (key == VK_ESCAPE)
{
Close();
}
return true;
}
void ColorPickerPopup::Paint(gd::Graphics& g)
{
if (!m_open || m_theme == nullptr || m_fonts == nullptr || m_rect.Width <= 0.0f)
{
return;
}
// 投影。
UiFillRoundRect(g, gd::RectF(m_rect.X + 1.0f * m_scale, m_rect.Y + 2.0f * m_scale, m_rect.Width,
m_rect.Height),
kCorner * m_scale, 0x14000000u);
UiFillRoundRect(g, gd::RectF(m_rect.X + 2.0f * m_scale, m_rect.Y + 4.0f * m_scale, m_rect.Width,
m_rect.Height),
kCorner * m_scale, 0x10000000u);
UiFillRoundRect(g, m_rect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_rect, kCorner * m_scale, m_theme->borderStrong, 1.0f);
UiText(g, L"自定义颜色", m_fonts->uiSmallBold, m_titleRect, m_theme->textMuted, TextAlign::LEFT);
// ---- 色域方块(横轴饱和度、纵轴明度)----
// 方块按当前色相预渲染到缓存位图里,避免每帧重画上百条渐变;
// 色相变化时才重建一次。
if (EnsureGamutCache())
{
g.DrawImage(m_gamutCache.get(), m_gamutRect);
}
UiDrawRoundRect(g, m_gamutRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
// ---- 色相条(纵向 0-1):6 段线性渐变拼出完整色相环 ----
{
for (int segment = 0; segment < 6; ++segment)
{
const double hue0 = static_cast<double>(segment) / 6.0;
const double hue1 = static_cast<double>(segment + 1) / 6.0;
const std::array<double, 3> rgb0 = FromHsv(hue0, 1.0, 1.0);
const std::array<double, 3> rgb1 = FromHsv(hue1, 1.0, 1.0);
const std::uint32_t color0 =
0xFF000000u | (ToByte(rgb0[0]) << 16) | (ToByte(rgb0[1]) << 8) | ToByte(rgb0[2]);
const std::uint32_t color1 =
0xFF000000u | (ToByte(rgb1[0]) << 16) | (ToByte(rgb1[1]) << 8) | ToByte(rgb1[2]);
const float top = m_hueRect.Y + m_hueRect.Height * static_cast<float>(hue0);
const float bottom = m_hueRect.Y + m_hueRect.Height * static_cast<float>(hue1);
const gd::Point from(static_cast<INT>(std::lround(m_hueRect.X)),
static_cast<INT>(std::lround(top)));
const gd::Point to(static_cast<INT>(std::lround(m_hueRect.X)),
static_cast<INT>(std::lround(bottom)));
gd::LinearGradientBrush brush(from, to, ToGdiColor(color0), ToGdiColor(color1));
// 渐变端点会各自向两侧延伸半段,逐段微重叠地填充即可无缝拼接。
g.FillRectangle(&brush, m_hueRect.X, top, m_hueRect.Width, bottom - top);
}
UiDrawRoundRect(g, m_hueRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
// ---- 色域方块上的选点:中心就是当前选中的颜色,外面只加一圈白色描边 ----
// 位置直接用保存的选点状态(不再从 RGB 反推,否则黑色会让选点弹回左侧)。
const float markerX = m_gamutRect.X + static_cast<float>(m_sat) * m_gamutRect.Width;
const float markerY = m_gamutRect.Y + static_cast<float>(1.0 - m_val) * m_gamutRect.Height;
const float markerRadius = 5.5f * m_scale;
gd::SolidBrush markerBrush(ToGdiColor(CurrentColor()));
g.FillEllipse(&markerBrush, markerX - markerRadius, markerY - markerRadius,
markerRadius * 2.0f, markerRadius * 2.0f);
gd::Pen markerPen(ToGdiColor(0xFFFFFFFFu), 2.0f * m_scale);
g.DrawEllipse(&markerPen, markerX - markerRadius, markerY - markerRadius,
markerRadius * 2.0f, markerRadius * 2.0f);
// ---- 色相条上的指示器:白色描边的圆角长方条,底色就是该位置的颜色 ----
// 色相直接用保存值:色相 1 与 0 是同一个颜色(红),从 RGB 反推会归一到 0,
// 表现为"把滑块拖到最下面却弹回最上面"。
const double hue = CurrentHue();
const std::array<double, 3> hueRgb = FromHsv(hue, 1.0, 1.0);
const std::uint32_t hueColor =
0xFF000000u | (ToByte(hueRgb[0]) << 16) | (ToByte(hueRgb[1]) << 8) | ToByte(hueRgb[2]);
const float hueMarkerY = m_hueRect.Y + static_cast<float>(hue) * m_hueRect.Height;
const float pillHeight = 10.0f * m_scale;
const float pillWidth = m_hueRect.Width + 9.0f * m_scale;
const gd::RectF pill(m_hueRect.X + (m_hueRect.Width - pillWidth) * 0.5f,
hueMarkerY - pillHeight * 0.5f, pillWidth, pillHeight);
UiFillRoundRect(g, pill, pillHeight * 0.5f, hueColor);
UiDrawRoundRect(g, pill, pillHeight * 0.5f, 0xFFFFFFFFu, 1.8f * m_scale);
}
for (int index = 0; index < 3; ++index)
{
const std::size_t slot = static_cast<std::size_t>(index);
const gd::RectF& track = m_tracks[slot];
// 通道标签放在滑块同一行的左侧,与轨道/数值纵向对齐。
const gd::RectF labelRect(track.X - (kLabelWidth + kLabelGap) * m_scale, m_values[slot].Y,
kLabelWidth * m_scale, kRowHeight * m_scale);
UiText(g, kChannelNames[index], m_fonts->uiSmall, labelRect, m_theme->textMuted, TextAlign::LEFT);
const bool active = (m_draggingSlider == index) || (m_hoverSlider == index);
DrawChannelSlider(g, track, m_channels[slot], kChannelColors[index], active, m_scale);
if (m_editField == static_cast<EditField>(index + 1))
{
UiDrawEditField(g, m_values[slot], m_edit, 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);
}
}
// 预览与十六进制值。
const std::uint32_t color = CurrentColor();
UiFillRoundRect(g, m_previewRect, kSwatchCorner * m_scale, color);
UiDrawRoundRect(g, m_previewRect, kSwatchCorner * m_scale, m_theme->borderStrong, 1.0f);
if (m_editField == EditField::HEX)
{
UiDrawEditField(g, m_hexRect, m_edit, 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