#include "ColorPickerPopup.hpp" #include #include #include #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& 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 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(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(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(index) * (kRowHeight + kRowGap) * m_scale; m_tracks[static_cast(index)] = gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f, trackWidth, kTrackHeight * m_scale); m_values[static_cast(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((color >> 16) & 0xFFu) / 255.0; m_channels[1] = static_cast((color >> 8) & 0xFFu) / 255.0; m_channels[2] = static_cast(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(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(data.Scan0); for (int y = 0; y < size; ++y) { // 纵轴明度(上 1 → 下 0),横轴饱和度(左 0 → 右 1)——与常见取色器一致。 const double value = 1.0 - (static_cast(y) + 0.5) / static_cast(size); unsigned char* row = (data.Stride >= 0) ? (base + static_cast(y) * data.Stride) : (base + static_cast(size - 1 - y) * (-data.Stride)); for (int x = 0; x < size; ++x) { const double saturation = (static_cast(x) + 0.5) / static_cast(size); const std::array rgb = FromHsv(hue, saturation, value); row[x * 4 + 0] = static_cast(ToByte(rgb[2])); // B row[x * 4 + 1] = static_cast(ToByte(rgb[1])); // G row[x * 4 + 2] = static_cast(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(field) - 1; m_edit.Begin(std::to_wstring(ToByte(m_channels[static_cast(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(c - L'0'); } else if (c >= L'a' && c <= L'f') { digit = static_cast(c - L'a') + 10u; } else if (c >= L'A' && c <= L'F') { digit = static_cast(c - L'A') + 10u; } else { valid = false; break; } value = (value << 4) | digit; } if (valid) { m_channels[0] = static_cast((value >> 16) & 0xFFu) / 255.0; m_channels[1] = static_cast((value >> 8) & 0xFFu) / 255.0; m_channels[2] = static_cast(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(field) - 1; if (index >= 0 && index < 3) { m_channels[static_cast(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(x), static_cast(y)); } HCURSOR ColorPickerPopup::QueryCursor(int x, int y) const { if (!m_open) { return nullptr; } // 拖拽过程中始终保持对应光标,避免指针移出控件后光标跳变。 if (m_drag == DragTarget::GAMUT || UiPointInRect(m_gamutRect, static_cast(x), static_cast(y))) { return LoadCursorW(nullptr, IDC_CROSS); } if (m_drag == DragTarget::HUE || UiPointInRect(m_hueRect, static_cast(x), static_cast(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(x); const float fy = static_cast(y); for (int index = 0; index < 3; ++index) { const gd::RectF& track = m_tracks[static_cast(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(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(x), static_cast(y)); return true; case DragTarget::HUE: ApplyHue(static_cast(y)); return true; case DragTarget::SLIDER: ApplySlider(m_draggingSlider, static_cast(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(x), static_cast(y))) { EndEdit(true); } // 数值与色号文本优先于滑块:两者几何重叠时要能进入输入状态。 for (int index = 0; index < 3; ++index) { if (UiPointInRect(m_values[static_cast(index)], static_cast(x), static_cast(y))) { BeginEdit(static_cast(index + 1)); return true; } } if (UiPointInRect(m_hexRect, static_cast(x), static_cast(y))) { BeginEdit(EditField::HEX); return true; } // 色域方块与色相条:按下即取值,之后拖动持续更新。 if (UiPointInRect(m_gamutRect, static_cast(x), static_cast(y))) { m_drag = DragTarget::GAMUT; ApplyGamut(static_cast(x), static_cast(y)); return true; } if (UiPointInRect(m_hueRect, static_cast(x), static_cast(y))) { m_drag = DragTarget::HUE; ApplyHue(static_cast(y)); return true; } m_draggingSlider = SliderAtPoint(x, y); if (m_draggingSlider >= 0) { m_drag = DragTarget::SLIDER; ApplySlider(m_draggingSlider, static_cast(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(segment) / 6.0; const double hue1 = static_cast(segment + 1) / 6.0; const std::array rgb0 = FromHsv(hue0, 1.0, 1.0); const std::array 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(hue0); const float bottom = m_hueRect.Y + m_hueRect.Height * static_cast(hue1); const gd::Point from(static_cast(std::lround(m_hueRect.X)), static_cast(std::lround(top))); const gd::Point to(static_cast(std::lround(m_hueRect.X)), static_cast(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(m_sat) * m_gamutRect.Width; const float markerY = m_gamutRect.Y + static_cast(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 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(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(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(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