diff --git a/editor/src/UI/ColorPicker.h b/editor/src/UI/ColorPicker.h new file mode 100644 index 00000000..5ba2b2a5 --- /dev/null +++ b/editor/src/UI/ColorPicker.h @@ -0,0 +1,843 @@ +#pragma once + +#include "Core.h" +#include "PropertyLayout.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +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& ColorPickerStates() { + static std::unordered_map 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(std::round(std::clamp(state.color[0], 0.0f, 1.0f) * 255.0f)); + const int green = static_cast(std::round(std::clamp(state.color[1], 0.0f, 1.0f) * 255.0f)); + const int blue = static_cast(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((value >> 16) & 0xFF) / 255.0f; + state.color[1] = static_cast((value >> 8) & 0xFF) / 255.0f; + state.color[2] = static_cast(value & 0xFF) / 255.0f; + } else { + state.color[0] = static_cast((value >> 24) & 0xFF) / 255.0f; + state.color[1] = static_cast((value >> 16) & 0xFF) / 255.0f; + state.color[2] = static_cast((value >> 8) & 0xFF) / 255.0f; + state.color[3] = static_cast(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(std::ceil((max.x - min.x) / cellSize)); + const int rows = static_cast(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(column), min.y + cellSize * static_cast(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(outerRadius * 2.4f)); + for (int i = 0; i < segmentCount; ++i) { + const float hue0 = static_cast(i) / static_cast(segmentCount); + const float hue1 = static_cast(i + 1) / static_cast(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(drawList->_VtxCurrentIdx + 0)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 1)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 2)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 0)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 2)); + drawList->PrimWriteIdx(static_cast(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(drawList->_VtxCurrentIdx + 0)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 1)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 2)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 0)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 2)); + drawList->PrimWriteIdx(static_cast(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(drawList->_VtxCurrentIdx + 0)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 1)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 2)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 0)); + drawList->PrimWriteIdx(static_cast(drawList->_VtxCurrentIdx + 2)); + drawList->PrimWriteIdx(static_cast(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 diff --git a/editor/src/UI/ScalarControls.h b/editor/src/UI/ScalarControls.h index 8fe4715a..4a042008 100644 --- a/editor/src/UI/ScalarControls.h +++ b/editor/src/UI/ScalarControls.h @@ -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(step), min, max); + const bool changed = ImGui::DragInt("##value", &value, static_cast(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(value - min) / static_cast(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();