feat: refine editor color picker UI

This commit is contained in:
2026-03-28 17:24:48 +08:00
parent 2b255751b6
commit 3c45a051a2
2 changed files with 920 additions and 38 deletions

843
editor/src/UI/ColorPicker.h Normal file
View File

@@ -0,0 +1,843 @@
#pragma once
#include "Core.h"
#include "PropertyLayout.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_map>
#include <imgui.h>
namespace XCEngine {
namespace Editor {
namespace UI {
namespace detail {
constexpr float kColorPickerPi = 3.14159265358979323846f;
struct ColorPickerState {
float color[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
float hue = 0.0f;
float saturation = 0.0f;
float value = 1.0f;
char hexBuffer[16] = "FFFFFF";
bool hexEditing = false;
bool initialized = false;
bool hueDragging = false;
};
inline std::unordered_map<ImGuiID, ColorPickerState>& ColorPickerStates() {
static std::unordered_map<ImGuiID, ColorPickerState> states;
return states;
}
inline float ColorPickerHeaderHeight() {
return 30.0f;
}
inline float ColorPickerBodyPadding() {
return 10.0f;
}
inline float ColorPickerTopRowHeight() {
return 34.0f;
}
inline float ColorPickerPreviewWidth() {
return 96.0f;
}
inline float ColorPickerPreviewHeight() {
return 28.0f;
}
inline float ColorPickerContentSpacing() {
return 6.0f;
}
inline float ColorPickerFieldInset() {
return 6.0f;
}
inline float ColorPickerHueOuterRadius() {
return 110.0f;
}
inline float ColorPickerHueRingThickness() {
return 24.0f;
}
inline float ColorPickerValueSquareSize() {
return 114.0f;
}
inline float ColorPickerWheelRegionHeight() {
return 220.0f;
}
inline float ColorPickerChannelRowHeight() {
return 20.0f;
}
inline float ColorPickerInputWidth() {
return 62.0f;
}
inline float ColorPickerCloseButtonSize() {
return 18.0f;
}
inline float ColorPickerControlRowLayoutHeight() {
return (std::max)(ColorPickerChannelRowHeight(), ImGui::GetFrameHeight());
}
inline float ColorPickerHexRowLayoutHeight() {
return ImGui::GetFrameHeight();
}
inline ImVec2 ColorPickerPopupSize(bool includeAlpha) {
const float spacing = ColorPickerContentSpacing();
const float channelRows = includeAlpha ? 4.0f : 3.0f;
const float contentHeight =
ColorPickerTopRowHeight() +
spacing +
ColorPickerWheelRegionHeight() +
spacing +
channelRows * ColorPickerControlRowLayoutHeight() +
channelRows * spacing +
ColorPickerHexRowLayoutHeight();
return ImVec2(
292.0f,
ColorPickerHeaderHeight() + ColorPickerBodyPadding() * 2.0f + contentHeight);
}
inline ImVec4 ColorPickerHeaderColor() {
return ImVec4(0.43f, 0.24f, 0.05f, 1.0f);
}
inline ImVec4 ColorPickerCloseButtonColor() {
return ImVec4(0.76f, 0.35f, 0.34f, 1.0f);
}
inline ImVec4 ColorPickerCloseButtonHoveredColor() {
return ImVec4(0.82f, 0.40f, 0.39f, 1.0f);
}
inline ImVec4 ColorPickerBodyColor() {
return ImVec4(0.24f, 0.24f, 0.24f, 1.0f);
}
inline ImVec4 ColorPickerBorderColor() {
return ImVec4(0.15f, 0.15f, 0.15f, 1.0f);
}
inline ImVec4 ColorPickerPreviewBorderColor() {
return ImVec4(0.12f, 0.12f, 0.12f, 1.0f);
}
inline ImVec4 ColorPickerPreviewBaseColor() {
return ImVec4(0.19f, 0.19f, 0.19f, 1.0f);
}
inline ImVec4 CheckerDarkColor() {
return ImVec4(0.55f, 0.55f, 0.55f, 1.0f);
}
inline ImVec4 CheckerLightColor() {
return ImVec4(0.84f, 0.84f, 0.84f, 1.0f);
}
inline ImU32 HueColorU32(float hue, float alpha = 1.0f) {
float red = 1.0f;
float green = 0.0f;
float blue = 0.0f;
ImGui::ColorConvertHSVtoRGB(hue, 1.0f, 1.0f, red, green, blue);
return ImGui::ColorConvertFloat4ToU32(ImVec4(red, green, blue, alpha));
}
inline ImVec4 ToImVec4(const float color[4]) {
return ImVec4(color[0], color[1], color[2], color[3]);
}
inline ImVec2 ClampPopupPositionToViewport(const ImVec2& desiredPosition, const ImVec2& popupSize) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
if (!viewport) {
return desiredPosition;
}
const ImVec2 min = viewport->WorkPos;
const ImVec2 max(viewport->WorkPos.x + viewport->WorkSize.x, viewport->WorkPos.y + viewport->WorkSize.y);
const float maxX = (std::max)(min.x, max.x - popupSize.x);
const float maxY = (std::max)(min.y, max.y - popupSize.y);
return ImVec2(
std::clamp(desiredPosition.x, min.x, maxX),
std::clamp(desiredPosition.y, min.y, maxY));
}
inline bool NearlyEqual(float lhs, float rhs, float epsilon = 0.0005f) {
return std::fabs(lhs - rhs) <= epsilon;
}
inline bool ColorsEqual(const float lhs[4], const float rhs[4], bool includeAlpha) {
const int componentCount = includeAlpha ? 4 : 3;
for (int i = 0; i < componentCount; ++i) {
if (!NearlyEqual(lhs[i], rhs[i])) {
return false;
}
}
return true;
}
inline void FormatHexBuffer(ColorPickerState& state) {
const int red = static_cast<int>(std::round(std::clamp(state.color[0], 0.0f, 1.0f) * 255.0f));
const int green = static_cast<int>(std::round(std::clamp(state.color[1], 0.0f, 1.0f) * 255.0f));
const int blue = static_cast<int>(std::round(std::clamp(state.color[2], 0.0f, 1.0f) * 255.0f));
std::snprintf(state.hexBuffer, sizeof(state.hexBuffer), "%02X%02X%02X", red, green, blue);
}
inline void SyncStateFromColor(ColorPickerState& state, const float color[4], bool includeAlpha) {
state.color[0] = color[0];
state.color[1] = color[1];
state.color[2] = color[2];
state.color[3] = includeAlpha ? color[3] : 1.0f;
float hue = state.hue;
float saturation = state.saturation;
float value = state.value;
ImGui::ColorConvertRGBtoHSV(state.color[0], state.color[1], state.color[2], hue, saturation, value);
if (saturation > 0.0001f) {
state.hue = hue;
}
state.saturation = saturation;
state.value = value;
if (!state.hexEditing) {
FormatHexBuffer(state);
}
state.initialized = true;
}
inline void SyncStateFromColorIfNeeded(ColorPickerState& state, const float color[4], bool includeAlpha) {
if (!state.initialized || !ColorsEqual(state.color, color, includeAlpha)) {
SyncStateFromColor(state, color, includeAlpha);
}
}
inline void RebuildRgbFromHsv(ColorPickerState& state) {
ImGui::ColorConvertHSVtoRGB(
std::clamp(state.hue, 0.0f, 1.0f),
std::clamp(state.saturation, 0.0f, 1.0f),
std::clamp(state.value, 0.0f, 1.0f),
state.color[0],
state.color[1],
state.color[2]);
}
inline void RebuildHsvFromRgb(ColorPickerState& state) {
float hue = state.hue;
float saturation = state.saturation;
float value = state.value;
ImGui::ColorConvertRGBtoHSV(state.color[0], state.color[1], state.color[2], hue, saturation, value);
if (saturation > 0.0001f) {
state.hue = hue;
}
state.saturation = saturation;
state.value = value;
}
inline bool TryParseHexColor(ColorPickerState& state) {
std::string text(state.hexBuffer);
text.erase(std::remove_if(text.begin(), text.end(), [](unsigned char ch) {
return ch == '#' || std::isspace(ch) != 0;
}), text.end());
if (text.size() != 6 && text.size() != 8) {
return false;
}
unsigned int value = 0;
if (std::sscanf(text.c_str(), "%x", &value) != 1) {
return false;
}
if (text.size() == 6) {
state.color[0] = static_cast<float>((value >> 16) & 0xFF) / 255.0f;
state.color[1] = static_cast<float>((value >> 8) & 0xFF) / 255.0f;
state.color[2] = static_cast<float>(value & 0xFF) / 255.0f;
} else {
state.color[0] = static_cast<float>((value >> 24) & 0xFF) / 255.0f;
state.color[1] = static_cast<float>((value >> 16) & 0xFF) / 255.0f;
state.color[2] = static_cast<float>((value >> 8) & 0xFF) / 255.0f;
state.color[3] = static_cast<float>(value & 0xFF) / 255.0f;
}
RebuildHsvFromRgb(state);
FormatHexBuffer(state);
return true;
}
inline void DrawCheckerboard(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, float cellSize = 6.0f) {
if (!drawList) {
return;
}
const ImU32 darkColor = ImGui::GetColorU32(CheckerDarkColor());
const ImU32 lightColor = ImGui::GetColorU32(CheckerLightColor());
const int columns = static_cast<int>(std::ceil((max.x - min.x) / cellSize));
const int rows = static_cast<int>(std::ceil((max.y - min.y) / cellSize));
for (int row = 0; row < rows; ++row) {
for (int column = 0; column < columns; ++column) {
const bool light = ((row + column) & 1) == 0;
const ImVec2 cellMin(min.x + cellSize * static_cast<float>(column), min.y + cellSize * static_cast<float>(row));
const ImVec2 cellMax(
std::min(cellMin.x + cellSize, max.x),
std::min(cellMin.y + cellSize, max.y));
drawList->AddRectFilled(cellMin, cellMax, light ? lightColor : darkColor);
}
}
}
inline bool ShouldDrawAlphaCheckerboard(const ImVec4& color) {
return color.w < 0.999f;
}
inline void DrawPreviewSwatch(
ImDrawList* drawList,
const ImRect& rect,
const ImVec4& color,
float rounding = 2.0f,
bool showAlphaBackground = false) {
if (!drawList) {
return;
}
drawList->AddRectFilled(rect.Min, rect.Max, ImGui::GetColorU32(ColorPickerPreviewBaseColor()), rounding);
if (showAlphaBackground) {
DrawCheckerboard(drawList, rect.Min, rect.Max);
}
drawList->AddRectFilled(rect.Min, rect.Max, ImGui::ColorConvertFloat4ToU32(color), rounding);
drawList->AddRect(rect.Min, rect.Max, ImGui::GetColorU32(ColorPickerPreviewBorderColor()), rounding);
}
inline void SetColorComponent(ImVec4& color, int componentIndex, float value) {
switch (componentIndex) {
case 0: color.x = value; break;
case 1: color.y = value; break;
case 2: color.z = value; break;
default: color.w = value; break;
}
}
inline bool DrawColorPreviewButton(const char* id, const float color[4], float size, bool popupOpen, ImRect& outRect) {
const float previewSize = (std::max)(size, 1.0f);
ImGui::InvisibleButton(id, ImVec2(previewSize, previewSize));
const bool hovered = ImGui::IsItemHovered();
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
outRect = ImRect(min, max);
const float rounding = ImGui::GetStyle().FrameRounding;
const ImU32 borderColor = ImGui::GetColorU32(ImGuiCol_Border);
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec4 previewColor = ToImVec4(color);
DrawPreviewSwatch(drawList, ImRect(min, max), previewColor, rounding, false);
if (popupOpen || hovered) {
const ImU32 overlayColor = popupOpen ? IM_COL32(255, 255, 255, 24) : IM_COL32(255, 255, 255, 12);
drawList->AddRectFilled(min, max, overlayColor, rounding);
}
drawList->AddRect(min, max, borderColor, rounding);
return pressed;
}
inline bool DrawHueWheel(const char* id, ColorPickerState& state, const ImVec2& center) {
const float outerRadius = ColorPickerHueOuterRadius();
const float innerRadius = outerRadius - ColorPickerHueRingThickness();
const float feather = 1.5f;
const float outerFadeRadius = outerRadius + feather;
const float innerFadeRadius = (std::max)(innerRadius - feather, 1.0f);
const ImVec2 wheelMin(center.x - outerRadius, center.y - outerRadius);
const ImVec2 wheelSize(outerRadius * 2.0f, outerRadius * 2.0f);
const ImVec2 mouse = ImGui::GetIO().MousePos;
const bool windowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
const bool insideBounds =
mouse.x >= wheelMin.x &&
mouse.x <= wheelMin.x + wheelSize.x &&
mouse.y >= wheelMin.y &&
mouse.y <= wheelMin.y + wheelSize.y;
const float dx = mouse.x - center.x;
const float dy = mouse.y - center.y;
const float distance = std::sqrt(dx * dx + dy * dy);
const bool hoveringRing = windowHovered &&
insideBounds &&
distance >= innerRadius - 5.0f &&
distance <= outerRadius + 5.0f;
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
state.hueDragging = false;
} else if (!state.hueDragging && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && hoveringRing) {
state.hueDragging = true;
}
bool changed = false;
if (state.hueDragging) {
float angle = std::atan2(dy, dx);
if (angle < 0.0f) {
angle += 2.0f * kColorPickerPi;
}
state.hue = std::clamp(angle / (2.0f * kColorPickerPi), 0.0f, 1.0f);
changed = true;
}
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec2 uvWhite = ImGui::GetFontTexUvWhitePixel();
const int segmentCount = (std::max)(192, static_cast<int>(outerRadius * 2.4f));
for (int i = 0; i < segmentCount; ++i) {
const float hue0 = static_cast<float>(i) / static_cast<float>(segmentCount);
const float hue1 = static_cast<float>(i + 1) / static_cast<float>(segmentCount);
const float angle0 = hue0 * 2.0f * kColorPickerPi;
const float angle1 = hue1 * 2.0f * kColorPickerPi;
ImVec2 outerFade0(center.x + std::cos(angle0) * outerFadeRadius, center.y + std::sin(angle0) * outerFadeRadius);
ImVec2 outerFade1(center.x + std::cos(angle1) * outerFadeRadius, center.y + std::sin(angle1) * outerFadeRadius);
ImVec2 outer0(center.x + std::cos(angle0) * outerRadius, center.y + std::sin(angle0) * outerRadius);
ImVec2 outer1(center.x + std::cos(angle1) * outerRadius, center.y + std::sin(angle1) * outerRadius);
ImVec2 inner0(center.x + std::cos(angle0) * innerRadius, center.y + std::sin(angle0) * innerRadius);
ImVec2 inner1(center.x + std::cos(angle1) * innerRadius, center.y + std::sin(angle1) * innerRadius);
ImVec2 innerFade0(center.x + std::cos(angle0) * innerFadeRadius, center.y + std::sin(angle0) * innerFadeRadius);
ImVec2 innerFade1(center.x + std::cos(angle1) * innerFadeRadius, center.y + std::sin(angle1) * innerFadeRadius);
const ImU32 col0 = HueColorU32(hue0, 1.0f);
const ImU32 col1 = HueColorU32(hue1, 1.0f);
const ImU32 col0Transparent = HueColorU32(hue0, 0.0f);
const ImU32 col1Transparent = HueColorU32(hue1, 0.0f);
drawList->PrimReserve(18, 12);
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 0));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 1));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 2));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 0));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 2));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 3));
drawList->PrimWriteVtx(outerFade0, uvWhite, col0Transparent);
drawList->PrimWriteVtx(outerFade1, uvWhite, col1Transparent);
drawList->PrimWriteVtx(outer1, uvWhite, col1);
drawList->PrimWriteVtx(outer0, uvWhite, col0);
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 0));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 1));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 2));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 0));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 2));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 3));
drawList->PrimWriteVtx(outer0, uvWhite, col0);
drawList->PrimWriteVtx(outer1, uvWhite, col1);
drawList->PrimWriteVtx(inner1, uvWhite, col1);
drawList->PrimWriteVtx(inner0, uvWhite, col0);
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 0));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 1));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 2));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 0));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 2));
drawList->PrimWriteIdx(static_cast<ImDrawIdx>(drawList->_VtxCurrentIdx + 3));
drawList->PrimWriteVtx(inner0, uvWhite, col0);
drawList->PrimWriteVtx(inner1, uvWhite, col1);
drawList->PrimWriteVtx(innerFade1, uvWhite, col1Transparent);
drawList->PrimWriteVtx(innerFade0, uvWhite, col0Transparent);
}
const float selectionAngle = state.hue * 2.0f * kColorPickerPi;
const float selectionRadius = (outerRadius + innerRadius) * 0.5f;
const ImVec2 selectionCenter(
center.x + std::cos(selectionAngle) * selectionRadius,
center.y + std::sin(selectionAngle) * selectionRadius);
drawList->AddCircle(selectionCenter, 13.0f, IM_COL32(255, 255, 255, 255), 28, 2.2f);
drawList->AddCircle(selectionCenter, 12.0f, IM_COL32(0, 0, 0, 92), 28, 1.0f);
return changed;
}
inline bool DrawValueSquare(const char* id, ColorPickerState& state, const ImVec2& min) {
const float size = ColorPickerValueSquareSize();
const ImVec2 max(min.x + size, min.y + size);
const ImVec2 restoreCursorPos = ImGui::GetCursorPos();
ImGui::SetCursorScreenPos(min);
ImGui::InvisibleButton(id, ImVec2(size, size));
const bool active = ImGui::IsItemActive();
bool changed = false;
if (active) {
const ImVec2 mouse = ImGui::GetIO().MousePos;
state.saturation = std::clamp((mouse.x - min.x) / size, 0.0f, 1.0f);
state.value = std::clamp(1.0f - (mouse.y - min.y) / size, 0.0f, 1.0f);
changed = true;
}
float red = 1.0f, green = 0.0f, blue = 0.0f;
ImGui::ColorConvertHSVtoRGB(state.hue, 1.0f, 1.0f, red, green, blue);
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilledMultiColor(
min,
max,
IM_COL32(255, 255, 255, 255),
ImGui::ColorConvertFloat4ToU32(ImVec4(red, green, blue, 1.0f)),
ImGui::ColorConvertFloat4ToU32(ImVec4(red, green, blue, 1.0f)),
IM_COL32(255, 255, 255, 255));
drawList->AddRectFilledMultiColor(
min,
max,
IM_COL32(0, 0, 0, 0),
IM_COL32(0, 0, 0, 0),
IM_COL32(0, 0, 0, 255),
IM_COL32(0, 0, 0, 255));
drawList->AddRect(min, max, ImGui::GetColorU32(ColorPickerPreviewBorderColor()), 0.0f);
const ImVec2 selection(
min.x + state.saturation * size,
min.y + (1.0f - state.value) * size);
drawList->AddCircle(selection, 8.0f, IM_COL32(255, 255, 255, 255), 24, 2.0f);
drawList->AddCircle(selection, 7.0f, IM_COL32(0, 0, 0, 96), 24, 1.0f);
ImGui::SetCursorPos(restoreCursorPos);
return changed;
}
inline bool DrawChannelGradientSlider(
const char* id,
float& value,
const ImVec2& size,
const ImVec4& colorA,
const ImVec4& colorB,
bool drawAlphaBackground = false) {
ImGui::InvisibleButton(id, size);
bool changed = false;
if (ImGui::IsItemActive()) {
const float t = std::clamp((ImGui::GetIO().MousePos.x - ImGui::GetItemRectMin().x) / size.x, 0.0f, 1.0f);
value = t;
changed = true;
}
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (drawAlphaBackground) {
DrawCheckerboard(drawList, min, max, 5.0f);
}
drawList->AddRectFilledMultiColor(
min,
max,
ImGui::ColorConvertFloat4ToU32(colorA),
ImGui::ColorConvertFloat4ToU32(colorB),
ImGui::ColorConvertFloat4ToU32(colorB),
ImGui::ColorConvertFloat4ToU32(colorA));
drawList->AddRect(min, max, ImGui::GetColorU32(ColorPickerPreviewBorderColor()), 0.0f);
const float x = min.x + std::clamp(value, 0.0f, 1.0f) * size.x;
drawList->AddLine(ImVec2(x, min.y), ImVec2(x, max.y), IM_COL32(255, 255, 255, 255), 2.0f);
drawList->AddLine(ImVec2(x + 1.5f, min.y), ImVec2(x + 1.5f, max.y), IM_COL32(0, 0, 0, 100), 1.0f);
return changed;
}
inline bool DrawChannelRow(const char* label, int componentIndex, ColorPickerState& state, float rowWidth) {
constexpr float labelWidth = 14.0f;
constexpr float spacing = 8.0f;
const float inputWidth = ColorPickerInputWidth();
const float sliderWidth = ImMax(rowWidth - labelWidth - spacing - inputWidth - spacing, 60.0f);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(label);
ImGui::SameLine(0.0f, spacing);
ImGui::PushID(label);
float& value = state.color[componentIndex];
ImVec4 startColor(state.color[0], state.color[1], state.color[2], 1.0f);
ImVec4 endColor(startColor);
SetColorComponent(startColor, componentIndex, 0.0f);
SetColorComponent(endColor, componentIndex, 1.0f);
const bool gradientChanged = DrawChannelGradientSlider("##slider", value, ImVec2(sliderWidth, ColorPickerChannelRowHeight()), startColor, endColor);
ImGui::SameLine(0.0f, spacing);
float inputValue = value;
ImGui::SetNextItemWidth(inputWidth);
const bool inputChanged = ImGui::InputFloat("##input", &inputValue, 0.0f, 0.0f, "%.4f");
value = std::clamp(inputValue, 0.0f, 1.0f);
ImGui::PopID();
return gradientChanged || inputChanged;
}
inline bool DrawAlphaRow(ColorPickerState& state, float rowWidth) {
constexpr float labelWidth = 14.0f;
constexpr float spacing = 8.0f;
const float inputWidth = ColorPickerInputWidth();
const float sliderWidth = ImMax(rowWidth - labelWidth - spacing - inputWidth - spacing, 60.0f);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("A");
ImGui::SameLine(0.0f, spacing);
ImGui::PushID("Alpha");
const ImVec4 startColor(state.color[0], state.color[1], state.color[2], 0.0f);
const ImVec4 endColor(state.color[0], state.color[1], state.color[2], 1.0f);
bool changed = DrawChannelGradientSlider(
"##slider",
state.color[3],
ImVec2(sliderWidth, ColorPickerChannelRowHeight()),
startColor,
endColor,
true);
ImGui::SameLine(0.0f, spacing);
float inputValue = state.color[3];
ImGui::SetNextItemWidth(inputWidth);
changed = ImGui::InputFloat("##input", &inputValue, 0.0f, 0.0f, "%.4f") || changed;
state.color[3] = std::clamp(inputValue, 0.0f, 1.0f);
ImGui::PopID();
return changed;
}
inline bool DrawHexRow(ColorPickerState& state, float rowWidth) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Hexadecimal");
ImGui::SameLine(0.0f, 8.0f);
if (!state.hexEditing) {
FormatHexBuffer(state);
}
ImGui::SetNextItemWidth(ImMax(rowWidth - ImGui::CalcTextSize("Hexadecimal").x - 8.0f, 40.0f));
const bool edited = ImGui::InputText(
"##hex",
state.hexBuffer,
IM_ARRAYSIZE(state.hexBuffer),
ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_EnterReturnsTrue);
bool changed = false;
if (ImGui::IsItemActive()) {
state.hexEditing = true;
} else if (state.hexEditing) {
changed = TryParseHexColor(state) || edited;
state.hexEditing = false;
} else if (edited) {
changed = TryParseHexColor(state);
}
return changed;
}
inline bool DrawCloseButton(const char* id, const ImVec2& size) {
ImGui::InvisibleButton(id, size);
const bool hovered = ImGui::IsItemHovered();
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
const ImRect rect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImU32 fillColor = ImGui::GetColorU32(hovered ? ColorPickerCloseButtonHoveredColor() : ColorPickerCloseButtonColor());
drawList->AddRectFilled(rect.Min, rect.Max, fillColor, 0.0f);
const float pad = 5.0f;
drawList->AddLine(
ImVec2(rect.Min.x + pad, rect.Min.y + pad),
ImVec2(rect.Max.x - pad, rect.Max.y - pad),
IM_COL32(255, 255, 255, 255),
1.2f);
drawList->AddLine(
ImVec2(rect.Max.x - pad, rect.Min.y + pad),
ImVec2(rect.Min.x + pad, rect.Max.y - pad),
IM_COL32(255, 255, 255, 255),
1.2f);
return pressed;
}
inline bool DrawUnityColorPickerPopup(const char* popupId, ColorPickerState& state, bool includeAlpha) {
const float headerHeight = ColorPickerHeaderHeight();
const float bodyPadding = ColorPickerBodyPadding();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_PopupBg, ColorPickerBodyColor());
ImGui::PushStyleColor(ImGuiCol_Border, ColorPickerBorderColor());
bool popupOpen = ImGui::BeginPopup(
popupId,
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
if (!popupOpen) {
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(3);
return false;
}
bool changed = false;
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec2 windowPos = ImGui::GetWindowPos();
const ImVec2 windowSize = ImGui::GetWindowSize();
const ImVec2 windowMax(windowPos.x + windowSize.x, windowPos.y + windowSize.y);
drawList->AddRectFilled(windowPos, windowMax, ImGui::GetColorU32(ColorPickerBodyColor()), 3.0f);
drawList->AddRectFilled(
windowPos,
ImVec2(windowMax.x, windowPos.y + headerHeight),
ImGui::GetColorU32(ColorPickerHeaderColor()),
3.0f,
ImDrawFlags_RoundCornersTop);
drawList->AddRect(windowPos, windowMax, ImGui::GetColorU32(ColorPickerBorderColor()), 3.0f);
drawList->AddText(ImVec2(windowPos.x + 10.0f, windowPos.y + 8.0f), IM_COL32(255, 255, 255, 255), "Color");
const float closeButtonSize = ColorPickerCloseButtonSize();
ImGui::SetCursorPos(ImVec2(windowSize.x - closeButtonSize - 4.0f, 5.0f));
if (DrawCloseButton("##close", ImVec2(closeButtonSize, closeButtonSize))) {
ImGui::CloseCurrentPopup();
}
const float contentWidth = windowSize.x - bodyPadding * 2.0f;
const float fieldInset = ColorPickerFieldInset();
const float fieldWidth = contentWidth - fieldInset * 2.0f;
ImGui::SetCursorPos(ImVec2(bodyPadding, headerHeight + bodyPadding));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, ColorPickerContentSpacing()));
const ImVec2 previewRowMin = ImGui::GetCursorScreenPos();
ImGui::Dummy(ImVec2(contentWidth, ColorPickerTopRowHeight()));
const float previewOffsetY = (ColorPickerTopRowHeight() - ColorPickerPreviewHeight()) * 0.5f;
const ImVec4 previewColor = ToImVec4(state.color);
DrawPreviewSwatch(
drawList,
ImRect(
ImVec2(previewRowMin.x + contentWidth - ColorPickerPreviewWidth(), previewRowMin.y + previewOffsetY),
ImVec2(previewRowMin.x + contentWidth, previewRowMin.y + previewOffsetY + ColorPickerPreviewHeight())),
previewColor,
2.0f,
includeAlpha && ShouldDrawAlphaCheckerboard(previewColor));
const ImVec2 wheelRegionMin = ImGui::GetCursorScreenPos();
ImGui::Dummy(ImVec2(contentWidth, ColorPickerWheelRegionHeight()));
const ImVec2 wheelRegionCursor = ImGui::GetCursorPos();
const ImVec2 ringCenter(
wheelRegionMin.x + contentWidth * 0.5f,
wheelRegionMin.y + ColorPickerHueOuterRadius());
const bool hueChanged = DrawHueWheel("##HueWheel", state, ringCenter);
const float squareSize = ColorPickerValueSquareSize();
const ImVec2 squareMin(ringCenter.x - squareSize * 0.5f, ringCenter.y - squareSize * 0.5f);
const bool squareChanged = DrawValueSquare("##ValueSquare", state, squareMin);
ImGui::SetCursorPos(wheelRegionCursor);
if (hueChanged || squareChanged) {
RebuildRgbFromHsv(state);
changed = true;
}
ImGui::SetCursorPosX(bodyPadding + fieldInset);
if (DrawChannelRow("R", 0, state, fieldWidth)) {
RebuildHsvFromRgb(state);
changed = true;
}
ImGui::SetCursorPosX(bodyPadding + fieldInset);
if (DrawChannelRow("G", 1, state, fieldWidth)) {
RebuildHsvFromRgb(state);
changed = true;
}
ImGui::SetCursorPosX(bodyPadding + fieldInset);
if (DrawChannelRow("B", 2, state, fieldWidth)) {
RebuildHsvFromRgb(state);
changed = true;
}
ImGui::SetCursorPosX(bodyPadding + fieldInset);
if (includeAlpha && DrawAlphaRow(state, fieldWidth)) {
changed = true;
}
ImGui::SetCursorPosX(bodyPadding + fieldInset);
if (DrawHexRow(state, fieldWidth)) {
changed = true;
}
if (changed && !state.hexEditing) {
FormatHexBuffer(state);
}
ImGui::PopStyleVar();
ImGui::EndPopup();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(3);
return changed;
}
inline bool DrawUnityColorControl(
const char* label,
float color[4],
bool includeAlpha,
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout()) {
bool changed = false;
DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) {
const float controlSize = CompactIndicatorSize();
AlignPropertyControlVertically(layout, controlSize);
ImRect previewRect;
const bool popupOpen = ImGui::IsPopupOpen("##picker_popup");
if (DrawColorPreviewButton("##value", color, controlSize, popupOpen, previewRect)) {
auto& state = ColorPickerStates()[ImGui::GetID("##picker_state")];
SyncStateFromColor(state, color, includeAlpha);
ImGui::OpenPopup("##picker_popup");
}
auto& state = ColorPickerStates()[ImGui::GetID("##picker_state")];
SyncStateFromColorIfNeeded(state, color, includeAlpha);
const ImVec2 popupSize = ColorPickerPopupSize(includeAlpha);
const ImVec2 popupPos = ClampPopupPositionToViewport(
ImVec2(previewRect.Min.x, previewRect.Max.y + 2.0f),
popupSize);
ImGui::SetNextWindowPos(popupPos, ImGuiCond_Always);
ImGui::SetNextWindowSize(popupSize, ImGuiCond_Always);
if (DrawUnityColorPickerPopup("##picker_popup", state, includeAlpha)) {
color[0] = state.color[0];
color[1] = state.color[1];
color[2] = state.color[2];
if (includeAlpha) {
color[3] = state.color[3];
}
changed = true;
}
return false;
});
return changed;
}
} // namespace detail
inline bool DrawUnityColor3(
const char* label,
float color[3],
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout()) {
float rgba[4] = { color[0], color[1], color[2], 1.0f };
const bool changed = detail::DrawUnityColorControl(label, rgba, false, layoutSpec);
if (changed) {
color[0] = rgba[0];
color[1] = rgba[1];
color[2] = rgba[2];
}
return changed;
}
inline bool DrawUnityColor4(
const char* label,
float color[4],
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout()) {
return detail::DrawUnityColorControl(label, color, true, layoutSpec);
}
} // namespace UI
} // namespace Editor
} // namespace XCEngine

View File

@@ -1,5 +1,6 @@
#pragma once
#include "ColorPicker.h"
#include "Core.h"
#include "PropertyLayout.h"
@@ -26,8 +27,9 @@ inline void DrawComboPreviewFrame(
const char* id,
const char* previewValue,
float width,
float height,
bool popupOpen) {
const float frameHeight = ImGui::GetFrameHeight();
const float frameHeight = (std::max)(height, 1.0f);
ImGui::InvisibleButton(id, ImVec2(width, frameHeight));
const bool hovered = ImGui::IsItemHovered();
@@ -86,28 +88,42 @@ inline void DrawComboPreviewFrame(
textColor);
}
inline void DrawControlFrameChrome(
ImDrawList* drawList,
const ImVec2& min,
const ImVec2& max,
bool hovered,
bool active = false) {
if (!drawList) {
return;
inline bool DrawCompactCheckboxControl(const char* id, bool& value) {
const float size = CompactIndicatorSize();
ImGui::InvisibleButton(id, ImVec2(size, size));
const bool hovered = ImGui::IsItemHovered();
const bool held = ImGui::IsItemActive();
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
if (pressed) {
value = !value;
}
const ImGuiStyle& style = ImGui::GetStyle();
const ImU32 frameColor = ImGui::GetColorU32(
active ? ImGuiCol_FrameBgActive : (hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg));
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
const float rounding = ImGui::GetStyle().FrameRounding;
const ImU32 fillColor = ImGui::GetColorU32(
(held && hovered) ? ImGuiCol_FrameBgActive :
hovered ? ImGuiCol_FrameBgHovered :
ImGuiCol_FrameBg);
const ImU32 borderColor = ImGui::GetColorU32(ImGuiCol_Border);
drawList->AddRectFilled(min, max, frameColor, style.FrameRounding);
drawList->AddRect(
ImVec2(min.x + 0.5f, min.y + 0.5f),
ImVec2(max.x - 0.5f, max.y - 0.5f),
borderColor,
style.FrameRounding,
0,
style.FrameBorderSize);
const ImU32 checkColor = ImGui::GetColorU32(ImGuiCol_CheckMark);
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(min, max, fillColor, rounding);
drawList->AddRect(min, max, borderColor, rounding);
if (value) {
const float pad = (std::max)(size * 0.22f, 2.0f);
const ImVec2 points[3] = {
ImVec2(min.x + pad, min.y + size * 0.56f),
ImVec2(min.x + size * 0.44f, max.y - pad),
ImVec2(max.x - pad, min.y + pad)
};
drawList->AddPolyline(points, 3, checkColor, ImDrawFlags_None, 2.0f);
}
return pressed;
}
inline bool DrawFloat(
@@ -119,9 +135,16 @@ inline bool DrawFloat(
float max = 0.0f,
const char* format = "%.2f"
) {
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) {
const ImVec2 framePadding = ScalarControlFramePadding();
const PropertyLayoutSpec adjustedLayout =
WithMinimumRowHeight(layoutSpec, CalcPropertyRowHeightForFramePadding(framePadding));
return DrawPropertyRow(label, adjustedLayout, [&](const PropertyLayoutMetrics& layout) {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, framePadding);
AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
SetNextPropertyControlWidth(layout);
return ImGui::DragFloat("##value", &value, dragSpeed, min, max, format);
const bool changed = ImGui::DragFloat("##value", &value, dragSpeed, min, max, format);
ImGui::PopStyleVar();
return changed;
});
}
@@ -137,8 +160,7 @@ inline bool DrawLinearSlider(
const bool active = ImGui::IsItemActive();
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
const ImGuiStyle& style = ImGui::GetStyle();
const float trackPadding = (std::max)(LinearSliderHorizontalPadding(), style.FramePadding.x);
const float trackPadding = LinearSliderHorizontalPadding();
const float trackMinX = min.x + trackPadding;
const float trackMaxX = max.x - trackPadding;
const float centerY = (min.y + max.y) * 0.5f;
@@ -146,7 +168,6 @@ inline bool DrawLinearSlider(
const float trackHalfThickness = LinearSliderTrackThickness() * 0.5f;
ImDrawList* drawList = ImGui::GetWindowDrawList();
DrawControlFrameChrome(drawList, min, max, hovered, active);
drawList->AddRectFilled(
ImVec2(trackMinX, centerY - trackHalfThickness),
ImVec2(trackMaxX, centerY + trackHalfThickness),
@@ -184,9 +205,16 @@ inline bool DrawInt(
int min = 0,
int max = 0
) {
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) {
const ImVec2 framePadding = ScalarControlFramePadding();
const PropertyLayoutSpec adjustedLayout =
WithMinimumRowHeight(layoutSpec, CalcPropertyRowHeightForFramePadding(framePadding));
return DrawPropertyRow(label, adjustedLayout, [&](const PropertyLayoutMetrics& layout) {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, framePadding);
AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
SetNextPropertyControlWidth(layout);
return ImGui::DragInt("##value", &value, static_cast<float>(step), min, max);
const bool changed = ImGui::DragInt("##value", &value, static_cast<float>(step), min, max);
ImGui::PopStyleVar();
return changed;
});
}
@@ -195,8 +223,9 @@ inline bool DrawBool(
bool& value,
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout()
) {
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics&) {
return ImGui::Checkbox("##value", &value);
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) {
AlignPropertyControlVertically(layout, CompactIndicatorSize());
return DrawCompactCheckboxControl("##value", value);
});
}
@@ -205,9 +234,7 @@ inline bool DrawColor3(
float color[3],
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout()
) {
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics&) {
return ImGui::ColorEdit3("##value", color, ImGuiColorEditFlags_NoInputs);
});
return DrawUnityColor3(label, color, layoutSpec);
}
inline bool DrawColor4(
@@ -215,9 +242,7 @@ inline bool DrawColor4(
float color[4],
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout()
) {
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics&) {
return ImGui::ColorEdit4("##value", color, ImGuiColorEditFlags_NoInputs);
});
return DrawUnityColor4(label, color, layoutSpec);
}
inline bool DrawSliderFloat(
@@ -228,13 +253,18 @@ inline bool DrawSliderFloat(
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout(),
const char* format = "%.2f"
) {
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) {
const ImVec2 framePadding = ScalarControlFramePadding();
const PropertyLayoutSpec adjustedLayout =
WithMinimumRowHeight(layoutSpec, CalcPropertyRowHeightForFramePadding(framePadding));
return DrawPropertyRow(label, adjustedLayout, [&](const PropertyLayoutMetrics& layout) {
const float totalWidth = layout.controlWidth;
const float inputWidth = SliderValueFieldWidth();
const float spacing = CompoundControlSpacing();
const float sliderWidth = ImMax(totalWidth - inputWidth - spacing, 1.0f);
const float range = max - min;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, framePadding);
AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, 0.0f));
float normalizedValue = range > 0.0f ? (value - min) / range : 0.0f;
bool changed = DrawLinearSlider("##slider", sliderWidth, normalizedValue);
@@ -251,6 +281,7 @@ inline bool DrawSliderFloat(
changed = ImGui::InputFloat("##value", &value, 0.0f, 0.0f, format) || changed;
value = std::clamp(value, min, max);
ImGui::PopStyleVar();
ImGui::PopStyleVar();
return changed;
});
}
@@ -262,13 +293,18 @@ inline bool DrawSliderInt(
int max,
const PropertyLayoutSpec& layoutSpec = MakePropertyLayout()
) {
return DrawPropertyRow(label, layoutSpec, [&](const PropertyLayoutMetrics& layout) {
const ImVec2 framePadding = ScalarControlFramePadding();
const PropertyLayoutSpec adjustedLayout =
WithMinimumRowHeight(layoutSpec, CalcPropertyRowHeightForFramePadding(framePadding));
return DrawPropertyRow(label, adjustedLayout, [&](const PropertyLayoutMetrics& layout) {
const float totalWidth = layout.controlWidth;
const float inputWidth = SliderValueFieldWidth();
const float spacing = CompoundControlSpacing();
const float sliderWidth = ImMax(totalWidth - inputWidth - spacing, 1.0f);
const int range = max - min;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, framePadding);
AlignPropertyControlVertically(layout, ImGui::GetFrameHeight());
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, 0.0f));
const float normalizedValue = range > 0 ? static_cast<float>(value - min) / static_cast<float>(range) : 0.0f;
bool changed = DrawLinearSlider("##slider", sliderWidth, normalizedValue);
@@ -285,6 +321,7 @@ inline bool DrawSliderInt(
changed = ImGui::InputInt("##value", &value, 0, 0) || changed;
value = std::clamp(value, min, max);
ImGui::PopStyleVar();
ImGui::PopStyleVar();
return changed;
});
}
@@ -306,7 +343,9 @@ inline int DrawCombo(
: "";
const float comboWidth = layout.controlWidth;
const float popupWidth = comboWidth;
DrawComboPreviewFrame("##value", previewValue, comboWidth, ImGui::IsPopupOpen(popupId));
const float comboHeight = (std::max)(ImGui::GetFrameHeight() + ComboPreviewHeightOffset(), 1.0f);
AlignPropertyControlVertically(layout, comboHeight);
DrawComboPreviewFrame("##value", previewValue, comboWidth, comboHeight, ImGui::IsPopupOpen(popupId));
const ImVec2 comboMin = ImGui::GetItemRectMin();
const ImVec2 comboMax = ImGui::GetItemRectMax();