508 lines
18 KiB
C++
508 lines
18 KiB
C++
#pragma once
|
|
|
|
#include <XCEditor/Fields/UIEditorNumberField.h>
|
|
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
|
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <string>
|
|
#include <string_view>
|
|
|
|
namespace XCEngine::UI::Editor::Internal::VectorFieldWidgetShared {
|
|
|
|
using ::XCEngine::UI::UIDrawList;
|
|
using ::XCEngine::UI::UIPoint;
|
|
using ::XCEngine::UI::UIRect;
|
|
|
|
inline float ClampNonNegative(float value) {
|
|
return (std::max)(0.0f, value);
|
|
}
|
|
|
|
inline ::XCEngine::UI::UIColor DefaultAxisPlaceholderColor() {
|
|
return ::XCEngine::UI::UIColor(0.67f, 0.67f, 0.67f, 1.0f);
|
|
}
|
|
|
|
inline void OverrideMetricIfDefault(
|
|
float& value,
|
|
float placeholder,
|
|
float replacement) {
|
|
if (Widgets::AreUIEditorFieldMetricsEqual(value, placeholder)) {
|
|
value = replacement;
|
|
}
|
|
}
|
|
|
|
inline void OverrideColorIfDefault(
|
|
::XCEngine::UI::UIColor& value,
|
|
const ::XCEngine::UI::UIColor& placeholder,
|
|
const ::XCEngine::UI::UIColor& replacement) {
|
|
if (Widgets::AreUIEditorFieldColorsEqual(value, placeholder)) {
|
|
value = replacement;
|
|
}
|
|
}
|
|
|
|
inline const ::XCEngine::UI::UIColor& ResolveAxisTokenColor(
|
|
const Widgets::UIEditorInspectorFieldStyleTokens& tokens,
|
|
std::size_t componentIndex) {
|
|
switch (componentIndex) {
|
|
case 0u:
|
|
return tokens.axisXColor;
|
|
case 1u:
|
|
return tokens.axisYColor;
|
|
case 2u:
|
|
return tokens.axisZColor;
|
|
default:
|
|
return tokens.axisWColor;
|
|
}
|
|
}
|
|
|
|
template <typename Traits>
|
|
typename Traits::Metrics ResolveMetrics(const typename Traits::Metrics& metrics) {
|
|
const auto& tokens = Widgets::GetUIEditorInspectorFieldStyleTokens();
|
|
|
|
typename Traits::Metrics resolved = metrics;
|
|
OverrideMetricIfDefault(
|
|
resolved.controlTrailingInset,
|
|
8.0f,
|
|
tokens.controlTrailingInset);
|
|
OverrideMetricIfDefault(
|
|
resolved.componentMinWidth,
|
|
72.0f,
|
|
tokens.vectorComponentMinWidth);
|
|
OverrideMetricIfDefault(
|
|
resolved.componentPrefixWidth,
|
|
9.0f,
|
|
tokens.vectorPrefixWidth);
|
|
OverrideMetricIfDefault(
|
|
resolved.componentLabelGap,
|
|
4.0f,
|
|
tokens.vectorPrefixGap);
|
|
|
|
return resolved;
|
|
}
|
|
|
|
template <typename Traits>
|
|
typename Traits::Palette ResolvePalette(const typename Traits::Palette& palette) {
|
|
const auto& tokens = Widgets::GetUIEditorInspectorFieldStyleTokens();
|
|
|
|
typename Traits::Palette resolved = palette;
|
|
OverrideColorIfDefault(
|
|
resolved.componentColor,
|
|
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f),
|
|
tokens.controlColor);
|
|
OverrideColorIfDefault(
|
|
resolved.componentHoverColor,
|
|
::XCEngine::UI::UIColor(0.21f, 0.21f, 0.21f, 1.0f),
|
|
tokens.controlHoverColor);
|
|
OverrideColorIfDefault(
|
|
resolved.componentEditingColor,
|
|
::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f),
|
|
tokens.controlEditingColor);
|
|
OverrideColorIfDefault(
|
|
resolved.readOnlyColor,
|
|
::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f),
|
|
tokens.controlReadOnlyColor);
|
|
OverrideColorIfDefault(
|
|
resolved.componentBorderColor,
|
|
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f),
|
|
tokens.controlBorderColor);
|
|
OverrideColorIfDefault(
|
|
resolved.componentFocusedBorderColor,
|
|
::XCEngine::UI::UIColor(0.20f, 0.20f, 0.20f, 1.0f),
|
|
tokens.controlFocusedBorderColor);
|
|
OverrideColorIfDefault(
|
|
resolved.prefixColor,
|
|
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f),
|
|
tokens.prefixColor);
|
|
OverrideColorIfDefault(
|
|
resolved.prefixBorderColor,
|
|
::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f),
|
|
tokens.prefixBorderColor);
|
|
OverrideColorIfDefault(
|
|
resolved.labelColor,
|
|
::XCEngine::UI::UIColor(0.80f, 0.80f, 0.80f, 1.0f),
|
|
tokens.labelColor);
|
|
OverrideColorIfDefault(
|
|
resolved.valueColor,
|
|
::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f),
|
|
tokens.valueColor);
|
|
OverrideColorIfDefault(
|
|
resolved.readOnlyValueColor,
|
|
::XCEngine::UI::UIColor(0.62f, 0.62f, 0.62f, 1.0f),
|
|
tokens.readOnlyValueColor);
|
|
|
|
for (std::size_t componentIndex = 0u;
|
|
componentIndex < Traits::kComponentCount;
|
|
++componentIndex) {
|
|
if (Widgets::AreUIEditorFieldColorsEqual(
|
|
Traits::AxisColor(palette, componentIndex),
|
|
DefaultAxisPlaceholderColor())) {
|
|
Traits::AxisColor(resolved, componentIndex) =
|
|
ResolveAxisTokenColor(tokens, componentIndex);
|
|
}
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
inline float ApproximateTextWidth(float fontSize, std::size_t characterCount) {
|
|
return fontSize * 0.55f * static_cast<float>(characterCount);
|
|
}
|
|
|
|
template <typename Traits>
|
|
Widgets::UIEditorNumberFieldSpec BuildComponentNumberSpec(
|
|
const typename Traits::Spec& spec,
|
|
std::size_t componentIndex) {
|
|
Widgets::UIEditorNumberFieldSpec numberSpec = {};
|
|
numberSpec.fieldId = spec.fieldId + "." + std::to_string(componentIndex);
|
|
numberSpec.label.clear();
|
|
numberSpec.value = componentIndex < spec.values.size()
|
|
? spec.values[componentIndex]
|
|
: 0.0;
|
|
numberSpec.step = spec.step;
|
|
numberSpec.minValue = spec.minValue;
|
|
numberSpec.maxValue = spec.maxValue;
|
|
numberSpec.integerMode = spec.integerMode;
|
|
numberSpec.readOnly = spec.readOnly;
|
|
return numberSpec;
|
|
}
|
|
|
|
template <typename Traits>
|
|
::XCEngine::UI::UIColor ResolveRowFillColor(
|
|
const typename Traits::State& state,
|
|
const typename Traits::Palette& palette) {
|
|
if (state.activeTarget != Traits::kNoneHitTargetKind) {
|
|
return palette.rowActiveColor;
|
|
}
|
|
if (state.hoveredTarget != Traits::kNoneHitTargetKind) {
|
|
return palette.rowHoverColor;
|
|
}
|
|
return palette.surfaceColor;
|
|
}
|
|
|
|
template <typename Traits>
|
|
::XCEngine::UI::UIColor ResolveComponentFillColor(
|
|
const typename Traits::Spec& spec,
|
|
const typename Traits::State& state,
|
|
const typename Traits::Palette& palette,
|
|
std::size_t componentIndex) {
|
|
if (spec.readOnly) {
|
|
return palette.readOnlyColor;
|
|
}
|
|
if (state.editing && state.selectedComponentIndex == componentIndex) {
|
|
return palette.componentEditingColor;
|
|
}
|
|
if (state.hoveredComponentIndex == componentIndex) {
|
|
return palette.componentHoverColor;
|
|
}
|
|
return palette.componentColor;
|
|
}
|
|
|
|
template <typename Traits>
|
|
::XCEngine::UI::UIColor ResolveComponentBorderColor(
|
|
const typename Traits::State& state,
|
|
const typename Traits::Palette& palette,
|
|
std::size_t componentIndex) {
|
|
if (state.editing && state.selectedComponentIndex == componentIndex) {
|
|
return palette.componentFocusedBorderColor;
|
|
}
|
|
return palette.componentBorderColor;
|
|
}
|
|
|
|
inline bool IsPointInside(const UIRect& rect, const UIPoint& point) {
|
|
return point.x >= rect.x &&
|
|
point.x <= rect.x + rect.width &&
|
|
point.y >= rect.y &&
|
|
point.y <= rect.y + rect.height;
|
|
}
|
|
|
|
template <typename Traits>
|
|
double NormalizeComponentValue(
|
|
const typename Traits::Spec& spec,
|
|
double value) {
|
|
return Widgets::NormalizeUIEditorNumberFieldValue(
|
|
BuildComponentNumberSpec<Traits>(spec, 0u),
|
|
value);
|
|
}
|
|
|
|
template <typename Traits>
|
|
bool TryParseComponentValue(
|
|
const typename Traits::Spec& spec,
|
|
std::string_view text,
|
|
double& outValue) {
|
|
return Widgets::TryParseUIEditorNumberFieldValue(
|
|
BuildComponentNumberSpec<Traits>(spec, 0u),
|
|
text,
|
|
outValue);
|
|
}
|
|
|
|
template <typename Traits>
|
|
std::string FormatComponentValue(
|
|
const typename Traits::Spec& spec,
|
|
std::size_t componentIndex) {
|
|
return Widgets::FormatUIEditorNumberFieldValue(
|
|
BuildComponentNumberSpec<Traits>(spec, componentIndex));
|
|
}
|
|
|
|
template <typename Traits>
|
|
typename Traits::Layout BuildLayout(
|
|
const UIRect& bounds,
|
|
const typename Traits::Spec&,
|
|
const typename Traits::Metrics& metrics) {
|
|
static_assert(Traits::kComponentCount > 0u);
|
|
|
|
const typename Traits::Metrics resolvedMetrics = ResolveMetrics<Traits>(metrics);
|
|
const float gapCount = static_cast<float>(Traits::kComponentCount - 1u);
|
|
const float requiredControlWidth =
|
|
resolvedMetrics.componentMinWidth * static_cast<float>(Traits::kComponentCount) +
|
|
resolvedMetrics.componentGap * gapCount;
|
|
const Widgets::UIEditorFieldRowLayout hostLayout =
|
|
Widgets::BuildUIEditorFieldRowLayout(
|
|
bounds,
|
|
requiredControlWidth,
|
|
Widgets::UIEditorFieldRowLayoutMetrics{
|
|
resolvedMetrics.rowHeight,
|
|
resolvedMetrics.horizontalPadding,
|
|
resolvedMetrics.labelControlGap,
|
|
resolvedMetrics.controlColumnStart,
|
|
resolvedMetrics.sharedControlColumnMinWidth,
|
|
resolvedMetrics.controlTrailingInset,
|
|
resolvedMetrics.controlInsetY,
|
|
});
|
|
const float componentWidth = ClampNonNegative(
|
|
(hostLayout.controlRect.width - resolvedMetrics.componentGap * gapCount) /
|
|
static_cast<float>(Traits::kComponentCount));
|
|
|
|
typename Traits::Layout layout = {};
|
|
layout.bounds = hostLayout.bounds;
|
|
layout.labelRect = hostLayout.labelRect;
|
|
layout.controlRect = hostLayout.controlRect;
|
|
|
|
for (std::size_t componentIndex = 0u;
|
|
componentIndex < layout.componentRects.size();
|
|
++componentIndex) {
|
|
const float componentX =
|
|
layout.controlRect.x +
|
|
(componentWidth + resolvedMetrics.componentGap) *
|
|
static_cast<float>(componentIndex);
|
|
const UIRect componentRect(
|
|
componentX,
|
|
layout.controlRect.y,
|
|
componentWidth,
|
|
layout.controlRect.height);
|
|
const float prefixWidth =
|
|
(std::min)(resolvedMetrics.componentPrefixWidth, componentRect.width);
|
|
const float labelGap = ClampNonNegative(resolvedMetrics.componentLabelGap);
|
|
const float valueX = componentRect.x + prefixWidth + labelGap;
|
|
layout.componentRects[componentIndex] = componentRect;
|
|
layout.componentPrefixRects[componentIndex] =
|
|
UIRect(componentRect.x, componentRect.y, prefixWidth, componentRect.height);
|
|
layout.componentValueRects[componentIndex] =
|
|
UIRect(
|
|
valueX,
|
|
componentRect.y,
|
|
ClampNonNegative(componentRect.width - prefixWidth - labelGap),
|
|
componentRect.height);
|
|
}
|
|
|
|
return layout;
|
|
}
|
|
|
|
template <typename Traits>
|
|
typename Traits::HitTarget HitTest(
|
|
const typename Traits::Layout& layout,
|
|
const UIPoint& point) {
|
|
for (std::size_t componentIndex = 0u;
|
|
componentIndex < layout.componentRects.size();
|
|
++componentIndex) {
|
|
if (IsPointInside(layout.componentRects[componentIndex], point)) {
|
|
return { Traits::kComponentHitTargetKind, componentIndex };
|
|
}
|
|
}
|
|
|
|
if (IsPointInside(layout.bounds, point)) {
|
|
return { Traits::kRowHitTargetKind, Traits::kInvalidComponentIndex };
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
template <typename Traits>
|
|
void AppendBackground(
|
|
UIDrawList& drawList,
|
|
const typename Traits::Layout& layout,
|
|
const typename Traits::Spec& spec,
|
|
const typename Traits::State& state,
|
|
const typename Traits::Palette& palette,
|
|
const typename Traits::Metrics& metrics) {
|
|
const typename Traits::Palette resolvedPalette = ResolvePalette<Traits>(palette);
|
|
const typename Traits::Metrics resolvedMetrics = ResolveMetrics<Traits>(metrics);
|
|
|
|
if (ResolveRowFillColor<Traits>(state, resolvedPalette).a > 0.0f) {
|
|
drawList.AddFilledRect(
|
|
layout.bounds,
|
|
ResolveRowFillColor<Traits>(state, resolvedPalette),
|
|
resolvedMetrics.cornerRounding);
|
|
}
|
|
if ((state.focused
|
|
? resolvedPalette.focusedBorderColor.a
|
|
: resolvedPalette.borderColor.a) > 0.0f) {
|
|
drawList.AddRectOutline(
|
|
layout.bounds,
|
|
state.focused
|
|
? resolvedPalette.focusedBorderColor
|
|
: resolvedPalette.borderColor,
|
|
state.focused
|
|
? resolvedMetrics.focusedBorderThickness
|
|
: resolvedMetrics.borderThickness,
|
|
resolvedMetrics.cornerRounding);
|
|
}
|
|
|
|
for (std::size_t componentIndex = 0u;
|
|
componentIndex < layout.componentRects.size();
|
|
++componentIndex) {
|
|
drawList.AddFilledRect(
|
|
layout.componentValueRects[componentIndex],
|
|
ResolveComponentFillColor<Traits>(
|
|
spec,
|
|
state,
|
|
resolvedPalette,
|
|
componentIndex),
|
|
resolvedMetrics.componentRounding);
|
|
drawList.AddRectOutline(
|
|
layout.componentValueRects[componentIndex],
|
|
ResolveComponentBorderColor<Traits>(
|
|
state,
|
|
resolvedPalette,
|
|
componentIndex),
|
|
resolvedMetrics.borderThickness,
|
|
resolvedMetrics.componentRounding);
|
|
}
|
|
}
|
|
|
|
template <typename Traits>
|
|
void AppendForeground(
|
|
UIDrawList& drawList,
|
|
const typename Traits::Layout& layout,
|
|
const typename Traits::Spec& spec,
|
|
const typename Traits::State& state,
|
|
const typename Traits::Palette& palette,
|
|
const typename Traits::Metrics& metrics,
|
|
const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer) {
|
|
const typename Traits::Palette resolvedPalette = ResolvePalette<Traits>(palette);
|
|
const typename Traits::Metrics resolvedMetrics = ResolveMetrics<Traits>(metrics);
|
|
|
|
drawList.PushClipRect(
|
|
Widgets::ResolveUIEditorTextClipRect(
|
|
layout.labelRect,
|
|
resolvedMetrics.labelFontSize));
|
|
drawList.AddText(
|
|
UIPoint(
|
|
layout.labelRect.x,
|
|
Widgets::ResolveUIEditorTextTop(
|
|
layout.labelRect,
|
|
resolvedMetrics.labelFontSize,
|
|
resolvedMetrics.labelTextInsetY)),
|
|
spec.label,
|
|
resolvedPalette.labelColor,
|
|
resolvedMetrics.labelFontSize);
|
|
drawList.PopClipRect();
|
|
|
|
for (std::size_t componentIndex = 0u;
|
|
componentIndex < layout.componentRects.size();
|
|
++componentIndex) {
|
|
drawList.PushClipRect(
|
|
Widgets::ResolveUIEditorTextClipRect(
|
|
layout.componentPrefixRects[componentIndex],
|
|
resolvedMetrics.prefixFontSize));
|
|
const std::string& componentLabel = spec.componentLabels[componentIndex];
|
|
const float prefixTextX =
|
|
layout.componentPrefixRects[componentIndex].x +
|
|
ClampNonNegative(
|
|
(layout.componentPrefixRects[componentIndex].width -
|
|
ApproximateTextWidth(
|
|
resolvedMetrics.prefixFontSize,
|
|
componentLabel.size())) *
|
|
0.5f) +
|
|
resolvedMetrics.prefixTextInsetX;
|
|
drawList.AddText(
|
|
UIPoint(
|
|
prefixTextX,
|
|
Widgets::ResolveUIEditorTextTop(
|
|
layout.componentPrefixRects[componentIndex],
|
|
resolvedMetrics.prefixFontSize,
|
|
resolvedMetrics.prefixTextInsetY)),
|
|
componentLabel,
|
|
resolvedPalette.labelColor,
|
|
resolvedMetrics.prefixFontSize);
|
|
drawList.PopClipRect();
|
|
|
|
drawList.PushClipRect(
|
|
Widgets::ResolveUIEditorTextClipRect(
|
|
layout.componentValueRects[componentIndex],
|
|
resolvedMetrics.valueFontSize));
|
|
drawList.AddText(
|
|
UIPoint(
|
|
layout.componentValueRects[componentIndex].x +
|
|
resolvedMetrics.valueTextInsetX,
|
|
Widgets::ResolveUIEditorTextTop(
|
|
layout.componentValueRects[componentIndex],
|
|
resolvedMetrics.valueFontSize,
|
|
resolvedMetrics.valueTextInsetY)),
|
|
state.editing && state.selectedComponentIndex == componentIndex
|
|
? state.displayTexts[componentIndex]
|
|
: FormatComponentValue<Traits>(spec, componentIndex),
|
|
spec.readOnly
|
|
? resolvedPalette.readOnlyValueColor
|
|
: resolvedPalette.valueColor,
|
|
resolvedMetrics.valueFontSize);
|
|
if (state.editing && state.selectedComponentIndex == componentIndex) {
|
|
Widgets::AppendUIEditorTextCaret(
|
|
drawList,
|
|
layout.componentValueRects[componentIndex],
|
|
state.displayTexts[componentIndex],
|
|
state.caretOffset,
|
|
state.caretBlinkStartNanoseconds,
|
|
resolvedPalette.valueColor,
|
|
resolvedMetrics.valueFontSize,
|
|
resolvedMetrics.valueTextInsetX,
|
|
resolvedMetrics.valueTextInsetY,
|
|
1.0f,
|
|
textMeasurer);
|
|
}
|
|
drawList.PopClipRect();
|
|
}
|
|
}
|
|
|
|
template <typename Traits>
|
|
void AppendField(
|
|
UIDrawList& drawList,
|
|
const UIRect& bounds,
|
|
const typename Traits::Spec& spec,
|
|
const typename Traits::State& state,
|
|
const typename Traits::Palette& palette,
|
|
const typename Traits::Metrics& metrics,
|
|
const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer) {
|
|
const typename Traits::Palette resolvedPalette = ResolvePalette<Traits>(palette);
|
|
const typename Traits::Metrics resolvedMetrics = ResolveMetrics<Traits>(metrics);
|
|
const typename Traits::Layout layout =
|
|
BuildLayout<Traits>(bounds, spec, resolvedMetrics);
|
|
AppendBackground<Traits>(
|
|
drawList,
|
|
layout,
|
|
spec,
|
|
state,
|
|
resolvedPalette,
|
|
resolvedMetrics);
|
|
AppendForeground<Traits>(
|
|
drawList,
|
|
layout,
|
|
spec,
|
|
state,
|
|
resolvedPalette,
|
|
resolvedMetrics,
|
|
textMeasurer);
|
|
}
|
|
|
|
} // namespace XCEngine::UI::Editor::Internal::VectorFieldWidgetShared
|