#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 kPreviewHeight = 24.0f; /**< 预览行(预览色块 + 色号)高度。 */ const float kLabelWidth = 16.0f; /**< 通道标签宽度。 */ /** * 滑块**两侧**的间隙(左:通道字母 ↔ 轨道;右:轨道 ↔ 数值区)。 * * ⚠️ 必须共用同一个常量:用户明确要求轨道左右两侧的留白相等。 */ const float kSliderGap = 8.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) * 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); // 右列:预览行在滑块之上,整列高度与左侧方块一致。 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 previewRowGap = std::max(0.0f, kGamutSize - kPreviewHeight - (kRowHeight * 3.0f + kRowGap * 2.0f)) * m_scale; // 通道数值区固定占 **3 个字符宽**(0-255 最多三位):与数字本身的宽度无关, // 这样三行数值的右边界严格一致(右对齐后三列数字纵向对齐)。 const float valueWidth = std::max(1.0f, UiMeasureTextWidth(m_fonts->monoSmall, L"000")); // 轨道两侧用**同一个**间隙(用户要求两侧留白相等)。 const float sliderGap = kSliderGap * m_scale; const float labelWidth = kLabelWidth * m_scale; const float trackLeft = columnLeft + labelWidth + sliderGap; const float trackWidth = std::max( 24.0f, columnWidth - (labelWidth + sliderGap + sliderGap + valueWidth)); const float valueLeft = trackLeft + trackWidth + sliderGap; const float valueRight = valueLeft + valueWidth; // 预览色块的**左边界与轨道左边界对齐**(长宽不变)。 m_previewRect = gd::RectF(trackLeft, contentTop, kSwatchWidth * m_scale, kPreviewHeight * m_scale); // 色号 "#RRGGBB" 固定 **6+1 个字符宽**,并且**右边界与三行数值的右边界对齐**。 const float hexWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"#000000"); const float hexLeft = std::max(m_previewRect.X + m_previewRect.Width + kSwatchHexGap * m_scale, valueRight - hexWidth); m_hexRect = gd::RectF(hexLeft, contentTop, valueRight - hexLeft, kPreviewHeight * m_scale); const float sliderTop = m_previewRect.Y + m_previewRect.Height + previewRowGap; for (int index = 0; index < 3; ++index) { const float rowTop = sliderTop + 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(valueLeft, rowTop, valueWidth, kRowHeight * 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 PaddedEditRect(m_values[0]); case EditField::G: return PaddedEditRect(m_values[1]); case EditField::B: return PaddedEditRect(m_values[2]); case EditField::HEX: return PaddedEditRect(m_hexRect); case EditField::NONE: default: return gd::RectF(); } } gd::RectF ColorPickerPopup::PaddedEditRect(const gd::RectF& textRect) const { // 与 `UiDrawEditField` 的内边距(6*scale)一致:文本区向外扩这么多, // 编辑态才能完整显示 3 位数值 / 7 位色号(否则最后一个字符被裁掉)。 const float padding = 6.0f * m_scale; return gd::RectF(textRect.X - padding, textRect.Y, textRect.Width + padding * 2.0f, textRect.Height); } gd::RectF ColorPickerPopup::ChannelLabelRect(int index) const { if (index < 0 || index >= 3) { return gd::RectF(); } const std::size_t slot = static_cast(index); const gd::RectF& track = m_tracks[slot]; // 间隙直接由实际几何推出(轨道右端 → 数值区左端),因此标签侧与数值侧**必然**相等。 const float gap = std::max(0.0f, m_values[slot].X - (track.X + track.Width)); return gd::RectF(track.X - gap - kLabelWidth * m_scale, m_values[slot].Y, kLabelWidth * m_scale, kRowHeight * m_scale); } 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); } // 数值与色号文本优先于滑块:两者几何重叠时要能进入输入状态。 // 命中区用**编辑框**矩形(比 3 / 7 个字符宽的文本区宽出输入框内边距): // 文本区只有 3 个字符宽,命中区太小不便点击。 for (int index = 0; index < 3; ++index) { if (UiPointInRect(PaddedEditRect(m_values[static_cast(index)]), static_cast(x), static_cast(y))) { BeginEdit(static_cast(index + 1)); return true; } } if (UiPointInRect(PaddedEditRect(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); // ---- 色域方块(横轴饱和度、纵轴明度)---- // 方块按当前色相预渲染到缓存位图里,避免每帧重画上百条渐变; // 色相变化时才重建一次。 // // ⚠️ 填充必须**裁剪进圆角路径**里: // * 缓存的位图是直角矩形,直接贴上去会在四个角的多出来像素(用户报告的"角落里出现 // 莫名其妙的黑色像素点"——方块左下/右下本身就是接近黑色的饱和/明度极值)。 // * `WrapModeTileFlipXY` 让采样到边界之外的插值镜像取边,不再混入透明黑, // 边界也就不会再被"压暗"出一圈深色。 if (EnsureGamutCache()) { gd::GraphicsPath gamutPath; UiBuildRoundRectPath(gamutPath, m_gamutRect, kSwatchCorner * m_scale); gd::ImageAttributes attributes; attributes.SetWrapMode(gd::WrapModeTileFlipXY, gd::Color(), FALSE); const int saveState = g.Save(); g.SetClip(&gamutPath); g.DrawImage(m_gamutCache.get(), m_gamutRect, 0.0f, 0.0f, static_cast(m_gamutCacheSize), static_cast(m_gamutCacheSize), gd::UnitPixel, &attributes); g.Restore(saveState); } UiDrawRoundRect(g, m_gamutRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder); // ---- 色相条(纵向 0-1)---- { // 一条带 7 个色标的线性渐变画满整条(0、1/6 … 1,每段内 RGB 线性): // 与"6 段拼色"的颜色完全一致,但不会有段与段之间的接缝横线。 gd::Color stops[7]; gd::REAL positions[7]; for (int index = 0; index < 7; ++index) { const double hue = static_cast(index) / 6.0; const std::array rgb = FromHsv((hue > 1.0) ? 1.0 : hue, 1.0, 1.0); stops[index] = ToGdiColor(0xFF000000u | (ToByte(rgb[0]) << 16) | (ToByte(rgb[1]) << 8) | ToByte(rgb[2])); positions[index] = static_cast(static_cast(index) / 6.0); } gd::LinearGradientBrush brush( gd::Point(static_cast(std::lround(m_hueRect.X)), static_cast(std::lround(m_hueRect.Y))), gd::Point(static_cast(std::lround(m_hueRect.X)), static_cast(std::lround(m_hueRect.Y + m_hueRect.Height))), stops[0], stops[6]); brush.SetInterpolationColors(stops, positions, 7); // 与色域方块**共用同一个圆角**(`kSwatchCorner`),并且同样裁剪进圆角路径: // 直角填充会在圆角描边之外露出四个尖角(用户报告的"四个角的杂色像素点")。 gd::GraphicsPath huePath; UiBuildRoundRectPath(huePath, m_hueRect, kSwatchCorner * m_scale); const int saveState = g.Save(); g.SetClip(&huePath); g.FillRectangle(&brush, m_hueRect); g.Restore(saveState); 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]; // 通道标签放在滑块同一行的左侧,**右对齐**到"轨道左端 − 间隙": // 这样 R/G/B 三个字母的右边界与轨道的距离严格等于轨道到数值区的距离 // (用户要求两侧留白相同,且三个字母纵向对齐)。 const gd::RectF labelRect = ChannelLabelRect(index); UiText(g, kChannelNames[index], m_fonts->uiSmall, labelRect, m_theme->textMuted, TextAlign::RIGHT); 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, PaddedEditRect(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, PaddedEditRect(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::RIGHT); } } } // namespace jdi