Files
Just-DataInspector/Utilities/Src/ColorPickerPopup.cpp
T
DeepSeek V4.1 Flash 40b12d6e8b 调色性能优化 + 新增色域方块拾色 + 取值胶囊高对比配色 + 数据点标记修复
1. 修复自定义颜色滑动卡顿(VERY_BIG_DATA 全信号可见时 36ms/步 → 约 12ms/步)
   - 根因:场景缓存的内容指纹把逐信号的颜色/不透明度/线宽/模式都算了进去,
     改一次颜色就整幅失效,于是所有网格的波形都要重新光栅化。
   - 新增"不含外观样式"的几何指纹与样式快照:只有在网格结构、视图几何、信号集合与
     文字都不变、仅外观样式变化时,走"局部重画"快路径 —— 只重画颜色变了的那个网格,
     再把结果写回场景缓存(下一帧回到纯贴图)。
   - 补丁矩形向内对齐到整数像素,避免把属于相邻网格的一行擦掉而留下 1px 接缝;
     底色填充按补丁矩形本身进行(大面积不透明填充会走 GDI 快路径,不受 GDI+ 裁剪约束)。
   - 该优化对调色板点击、线宽/不透明度滑块、绘制模式切换同样生效。

2. 自定义颜色新增"方块色域图"拾色
   - 新增饱和度-明度方块(横轴饱和度、纵轴明度)与纵向色相条,可直接在色域图上取色。
   - 与面板内其它控件完全联动:RGB 滑块、十六进制输入、预览色块同源(RGB 为唯一数据源),
     并记忆色相,消色差时仍可用色相条换回颜色。
   - 色域方块按当前色相预渲染缓存,色相不变时不重复绘制;色域内用十字光标、色相条用纵向光标。

3. 取值胶囊的对比度描边与文本色改为对信号配色线性响应
   - 原实现只有一个亮度阈值,文字/描边只有黑、白两套固定取值。现按底色亮度选定亮/暗一侧,
     再与底色的互补色按固定比例线性混合(互补色亮度恰为 1−底色亮度),
     因此文字与描边的亮度随信号配色线性变化,且任意底色下都保持高对比。

4. 修复光标数据点越界三角的判定与绘制
   - 是否改用三角,以"数据点本身是否超出绘制边界"为准,不再因为数值标注被贴边而改变形状。
   - 三角方向修正为尖端朝外(此前上越界画下三角、左越界画右三角,方向完全相反),
     内三角改按质心收缩,描边不再是歪的;上下越界时三角与显示的数值纵向居中对齐。
2026-10-10 14:02:37 +08:00

995 lines
28 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 = 6.0f; /**< 滑块轨道高度。 */
const float kCorner = 6.0f; /**< 面板圆角半径。 */
const float kGamutSize = 112.0f; /**< 色域方块边长。 */
const float kHueWidth = 14.0f; /**< 色相条宽度。 */
const float kGamutGap = 10.0f; /**< 色域方块与色相条间距。 */
const float kColumnGap = 12.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)
{
const float width = kPanelWidth * m_scale;
// 高度:标题 + 内容区。内容区里"色域方块"是最高的控件,
// 预览行/色号行落在它与滑块列的下方,仍在其纵向范围内,不再额外叠加高度。
const float contentHeight = std::max(kGamutSize, kRowHeight * 3.0f + kRowGap * 2.0f +
kPreviewGap + kPreviewHeight);
const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + contentHeight) * 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;
// 左侧:色域方块(横轴饱和度、纵轴明度)+ 色相条。
m_gamutRect = gd::RectF(left + kPadH * m_scale, 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 columnLeft = m_hueRect.X + m_hueRect.Width + kColumnGap * m_scale;
const float columnWidth =
std::max(80.0f * m_scale, m_rect.X + m_rect.Width - kPadH * m_scale - columnLeft);
const float trackLeft = columnLeft + (kLabelWidth + kLabelGap) * m_scale;
const float trackWidth =
std::max(24.0f, columnWidth - (kLabelWidth + kLabelGap + kValueWidth) * m_scale);
for (int index = 0; index < 3; ++index)
{
const float rowTop =
contentTop + 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 - 6.0f * m_scale, kRowHeight * m_scale);
}
const float previewTop =
contentTop + (3.0f * (kRowHeight + kRowGap) + kPreviewGap) * m_scale;
m_previewRect = gd::RectF(trackLeft, previewTop + 2.0f * m_scale, 52.0f * m_scale, 20.0f * m_scale);
m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + 8.0f * m_scale,
previewTop + 2.0f * m_scale, 96.0f * m_scale, 20.0f * 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(灰色)色相也不会丢失。
m_hue = ToHsv(m_channels).h;
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
{
const Hsv hsv = ToHsv(m_channels);
// 灰度/黑色时 HSV 的色相没有定义:沿用记忆色相,避免拖动 RGB 滑块时色相跳变、
// 也保证"饱和度归零后仍能靠色相条换回颜色"。
return (hsv.s > 1.0e-4) ? hsv.h : m_hue;
}
void ColorPickerPopup::RefreshHueFromChannels()
{
const Hsv hsv = ToHsv(m_channels);
if (hsv.s > 1.0e-4)
{
m_hue = hsv.h;
}
}
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);
const std::array<double, 3> rgb = FromHsv(CurrentHue(), saturation, value);
if (std::fabs(rgb[0] - m_channels[0]) < 1.0e-6 && std::fabs(rgb[1] - m_channels[1]) < 1.0e-6 &&
std::fabs(rgb[2] - m_channels[2]) < 1.0e-6)
{
return;
}
m_channels = rgb;
// 色相不变(明度归零时 FromHsv 会给出黑色,但记忆色相仍要保留)。
m_changed = true;
}
void ColorPickerPopup::ApplyHue(float y)
{
if (m_hueRect.Height <= 0.0f)
{
return;
}
const double hue = Clamp01((y - m_hueRect.Y) / m_hueRect.Height);
const Hsv hsv = ToHsv(m_channels);
m_hue = hue;
const std::array<double, 3> rgb = FromHsv(m_hue, hsv.s, hsv.v);
if (std::fabs(rgb[0] - m_channels[0]) < 1.0e-6 && std::fabs(rgb[1] - m_channels[1]) < 1.0e-6 &&
std::fabs(rgb[2] - m_channels[2]) < 1.0e-6)
{
return;
}
m_channels = rgb;
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;
RefreshHueFromChannels();
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;
RefreshHueFromChannels();
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;
RefreshHueFromChannels();
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, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder);
// ---- 色相条(纵向 0-1,用 6 段线性渐变近似色相环)----
{
const Hsv hsv = ToHsv(m_channels);
const double saturation = (hsv.s > 1.0e-4) ? hsv.s : 1.0;
const double value = (hsv.v > 1.0e-4) ? hsv.v : 1.0;
const int bands = std::max(12, static_cast<int>(std::lround(m_hueRect.Height)));
for (int index = 0; index < bands; ++index)
{
const double hue = (static_cast<double>(index) + 0.5) / static_cast<double>(bands);
const std::array<double, 3> rgb = FromHsv(hue, 1.0, 1.0);
const std::uint32_t band = 0xFF000000u | (ToByte(rgb[0]) << 16) | (ToByte(rgb[1]) << 8) |
ToByte(rgb[2]);
const float top = m_hueRect.Y + m_hueRect.Height * index / static_cast<float>(bands);
const float bottom =
m_hueRect.Y + m_hueRect.Height * (index + 1) / static_cast<float>(bands);
UiFillRect(g, gd::RectF(m_hueRect.X, top, m_hueRect.Width, bottom - top), band);
}
UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder);
// 控件指示器:色域方块上标出 (S, V),色相条上标出当前色相。
// 两者都由 m_channels 反推,因此与 R/G/B 滑块、十六进制输入天然联动。
const float markerX = m_gamutRect.X + static_cast<float>(saturation) * m_gamutRect.Width;
const float markerY = m_gamutRect.Y + static_cast<float>(1.0 - value) * m_gamutRect.Height;
gd::Pen outerPen(ToGdiColor(0xFF1B1B1Bu), 2.0f * m_scale);
gd::Pen innerPen(ToGdiColor(0xFFFFFFFFu), 1.4f * m_scale);
const float outerRadius = 5.0f * m_scale;
g.DrawEllipse(&outerPen, markerX - outerRadius, markerY - outerRadius, outerRadius * 2.0f,
outerRadius * 2.0f);
const float innerRadius = 3.4f * m_scale;
g.DrawEllipse(&innerPen, markerX - innerRadius, markerY - innerRadius, innerRadius * 2.0f,
innerRadius * 2.0f);
const float hueMarkerY = m_hueRect.Y + static_cast<float>(CurrentHue()) * m_hueRect.Height;
UiFillRect(g, gd::RectF(m_hueRect.X - 2.0f * m_scale, hueMarkerY - 1.5f * m_scale,
m_hueRect.Width + 4.0f * m_scale, 3.0f * m_scale),
0xFF1B1B1Bu);
UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder);
}
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(m_rect.X + kPadH * m_scale, track.Y - 9.0f * m_scale,
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, 3.0f * m_scale, color);
UiDrawRoundRect(g, m_previewRect, 3.0f * 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