#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 = 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& 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) { 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(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 - 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((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(灰色)色相也不会丢失。 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(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); const std::array 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 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(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; 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(field) - 1; if (index >= 0 && index < 3) { m_channels[static_cast(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(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; 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(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, 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(std::lround(m_hueRect.Height))); for (int index = 0; index < bands; ++index) { const double hue = (static_cast(index) + 0.5) / static_cast(bands); const std::array 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(bands); const float bottom = m_hueRect.Y + m_hueRect.Height * (index + 1) / static_cast(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(saturation) * m_gamutRect.Width; const float markerY = m_gamutRect.Y + static_cast(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(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(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(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