#pragma once #include #include #include #include #include #include #include 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::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::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(characterCount); } template 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 ::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 ::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 ::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 double NormalizeComponentValue( const typename Traits::Spec& spec, double value) { return Widgets::NormalizeUIEditorNumberFieldValue( BuildComponentNumberSpec(spec, 0u), value); } template bool TryParseComponentValue( const typename Traits::Spec& spec, std::string_view text, double& outValue) { return Widgets::TryParseUIEditorNumberFieldValue( BuildComponentNumberSpec(spec, 0u), text, outValue); } template std::string FormatComponentValue( const typename Traits::Spec& spec, std::size_t componentIndex) { return Widgets::FormatUIEditorNumberFieldValue( BuildComponentNumberSpec(spec, componentIndex)); } template 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(metrics); const float gapCount = static_cast(Traits::kComponentCount - 1u); const float requiredControlWidth = resolvedMetrics.componentMinWidth * static_cast(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(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(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::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 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(palette); const typename Traits::Metrics resolvedMetrics = ResolveMetrics(metrics); if (ResolveRowFillColor(state, resolvedPalette).a > 0.0f) { drawList.AddFilledRect( layout.bounds, ResolveRowFillColor(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( spec, state, resolvedPalette, componentIndex), resolvedMetrics.componentRounding); drawList.AddRectOutline( layout.componentValueRects[componentIndex], ResolveComponentBorderColor( state, resolvedPalette, componentIndex), resolvedMetrics.borderThickness, resolvedMetrics.componentRounding); } } template 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 typename Traits::Palette resolvedPalette = ResolvePalette(palette); const typename Traits::Metrics resolvedMetrics = ResolveMetrics(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(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); } drawList.PopClipRect(); } } template 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 typename Traits::Palette resolvedPalette = ResolvePalette(palette); const typename Traits::Metrics resolvedMetrics = ResolveMetrics(metrics); const typename Traits::Layout layout = BuildLayout(bounds, spec, resolvedMetrics); AppendBackground( drawList, layout, spec, state, resolvedPalette, resolvedMetrics); AppendForeground( drawList, layout, spec, state, resolvedPalette, resolvedMetrics); } } // namespace XCEngine::UI::Editor::Internal::VectorFieldWidgetShared