395 lines
14 KiB
C++
395 lines
14 KiB
C++
|
|
#include <XCEditor/Fields/UIEditorAssetField.h>
|
||
|
|
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||
|
|
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <cctype>
|
||
|
|
#include <string_view>
|
||
|
|
|
||
|
|
namespace XCEngine::UI::Editor::Widgets {
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
using ::XCEngine::UI::UIDrawList;
|
||
|
|
using ::XCEngine::UI::UIPoint;
|
||
|
|
using ::XCEngine::UI::UIRect;
|
||
|
|
|
||
|
|
float ClampNonNegative(float value) {
|
||
|
|
return (std::max)(0.0f, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
float EstimateTextWidth(std::string_view text, float fontSize) {
|
||
|
|
return static_cast<float>(text.size()) * fontSize * 0.58f;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ShowsPickerButton(const UIEditorAssetFieldSpec& spec) {
|
||
|
|
return spec.showPickerButton;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ShowsClearButton(const UIEditorAssetFieldSpec& spec) {
|
||
|
|
return spec.allowClear && HasUIEditorAssetFieldValue(spec);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ShowsStatusBadge(const UIEditorAssetFieldSpec& spec) {
|
||
|
|
return spec.showStatusBadge && !spec.statusText.empty();
|
||
|
|
}
|
||
|
|
|
||
|
|
::XCEngine::UI::UIColor ResolveRowFillColor(
|
||
|
|
const UIEditorAssetFieldState& state,
|
||
|
|
const UIEditorAssetFieldPalette& palette) {
|
||
|
|
if (state.activeTarget != UIEditorAssetFieldHitTargetKind::None) {
|
||
|
|
return palette.rowActiveColor;
|
||
|
|
}
|
||
|
|
if (state.hoveredTarget != UIEditorAssetFieldHitTargetKind::None) {
|
||
|
|
return palette.rowHoverColor;
|
||
|
|
}
|
||
|
|
return palette.surfaceColor;
|
||
|
|
}
|
||
|
|
|
||
|
|
::XCEngine::UI::UIColor ResolveValueFillColor(
|
||
|
|
const UIEditorAssetFieldSpec& spec,
|
||
|
|
const UIEditorAssetFieldState& state,
|
||
|
|
const UIEditorAssetFieldPalette& palette) {
|
||
|
|
if (spec.readOnly) {
|
||
|
|
return palette.readOnlyColor;
|
||
|
|
}
|
||
|
|
if (state.activeTarget == UIEditorAssetFieldHitTargetKind::ValueBox ||
|
||
|
|
state.activeTarget == UIEditorAssetFieldHitTargetKind::PickerButton ||
|
||
|
|
state.activeTarget == UIEditorAssetFieldHitTargetKind::ClearButton) {
|
||
|
|
return palette.valueBoxActiveColor;
|
||
|
|
}
|
||
|
|
if (state.hoveredTarget == UIEditorAssetFieldHitTargetKind::ValueBox ||
|
||
|
|
state.hoveredTarget == UIEditorAssetFieldHitTargetKind::PickerButton ||
|
||
|
|
state.hoveredTarget == UIEditorAssetFieldHitTargetKind::ClearButton) {
|
||
|
|
return palette.valueBoxHoverColor;
|
||
|
|
}
|
||
|
|
return palette.valueBoxColor;
|
||
|
|
}
|
||
|
|
|
||
|
|
::XCEngine::UI::UIColor ResolveActionFillColor(
|
||
|
|
UIEditorAssetFieldHitTargetKind targetKind,
|
||
|
|
const UIEditorAssetFieldState& state,
|
||
|
|
const UIEditorAssetFieldPalette& palette) {
|
||
|
|
if (state.activeTarget == targetKind) {
|
||
|
|
return palette.actionButtonActiveColor;
|
||
|
|
}
|
||
|
|
if (state.hoveredTarget == targetKind) {
|
||
|
|
return palette.actionButtonHoverColor;
|
||
|
|
}
|
||
|
|
return palette.actionButtonColor;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
bool HasUIEditorAssetFieldValue(const UIEditorAssetFieldSpec& spec) {
|
||
|
|
return !spec.assetId.empty() || !spec.displayName.empty();
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string ResolveUIEditorAssetFieldValueText(const UIEditorAssetFieldSpec& spec) {
|
||
|
|
if (!spec.displayName.empty()) {
|
||
|
|
return spec.displayName;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!spec.assetId.empty()) {
|
||
|
|
return spec.assetId;
|
||
|
|
}
|
||
|
|
|
||
|
|
return spec.emptyText.empty() ? std::string("None") : spec.emptyText;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string ResolveUIEditorAssetFieldPreviewGlyph(const UIEditorAssetFieldSpec& spec) {
|
||
|
|
const std::string source =
|
||
|
|
!spec.statusText.empty() ? spec.statusText : ResolveUIEditorAssetFieldValueText(spec);
|
||
|
|
for (char character : source) {
|
||
|
|
if (std::isalnum(static_cast<unsigned char>(character)) != 0) {
|
||
|
|
return std::string(
|
||
|
|
1u,
|
||
|
|
static_cast<char>(std::toupper(static_cast<unsigned char>(character))));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return "-";
|
||
|
|
}
|
||
|
|
|
||
|
|
bool IsUIEditorAssetFieldPointInside(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;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIEditorAssetFieldLayout BuildUIEditorAssetFieldLayout(
|
||
|
|
const UIRect& bounds,
|
||
|
|
const UIEditorAssetFieldSpec& spec,
|
||
|
|
const UIEditorAssetFieldMetrics& metrics) {
|
||
|
|
const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout(
|
||
|
|
bounds,
|
||
|
|
metrics.valueBoxMinWidth,
|
||
|
|
UIEditorFieldRowLayoutMetrics {
|
||
|
|
metrics.rowHeight,
|
||
|
|
metrics.horizontalPadding,
|
||
|
|
metrics.labelControlGap,
|
||
|
|
metrics.controlColumnStart,
|
||
|
|
metrics.controlTrailingInset,
|
||
|
|
metrics.controlInsetY,
|
||
|
|
});
|
||
|
|
|
||
|
|
UIEditorAssetFieldLayout layout = {};
|
||
|
|
layout.bounds = hostLayout.bounds;
|
||
|
|
layout.labelRect = hostLayout.labelRect;
|
||
|
|
layout.controlRect = hostLayout.controlRect;
|
||
|
|
layout.valueRect = hostLayout.controlRect;
|
||
|
|
|
||
|
|
float trailingX = layout.valueRect.x + layout.valueRect.width;
|
||
|
|
if (ShowsPickerButton(spec)) {
|
||
|
|
trailingX -= metrics.actionButtonWidth;
|
||
|
|
layout.pickerRect = UIRect(
|
||
|
|
trailingX,
|
||
|
|
layout.valueRect.y,
|
||
|
|
metrics.actionButtonWidth,
|
||
|
|
layout.valueRect.height);
|
||
|
|
trailingX -= metrics.actionButtonGap;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ShowsClearButton(spec)) {
|
||
|
|
trailingX -= metrics.actionButtonWidth;
|
||
|
|
layout.clearRect = UIRect(
|
||
|
|
trailingX,
|
||
|
|
layout.valueRect.y,
|
||
|
|
metrics.actionButtonWidth,
|
||
|
|
layout.valueRect.height);
|
||
|
|
trailingX -= metrics.actionButtonGap;
|
||
|
|
}
|
||
|
|
|
||
|
|
const float previewSize =
|
||
|
|
(std::min)(metrics.previewSize, ClampNonNegative(layout.valueRect.height - 4.0f));
|
||
|
|
layout.previewRect = UIRect(
|
||
|
|
layout.valueRect.x + metrics.previewInsetX,
|
||
|
|
layout.valueRect.y + ClampNonNegative((layout.valueRect.height - previewSize) * 0.5f),
|
||
|
|
previewSize,
|
||
|
|
previewSize);
|
||
|
|
|
||
|
|
const float contentLeft = layout.previewRect.x + layout.previewRect.width + metrics.previewGap;
|
||
|
|
if (ShowsStatusBadge(spec)) {
|
||
|
|
const float estimatedBadgeWidth =
|
||
|
|
EstimateTextWidth(spec.statusText, metrics.statusBadgeFontSize) +
|
||
|
|
metrics.statusBadgePaddingX * 2.0f;
|
||
|
|
const float badgeWidth = (std::min)(
|
||
|
|
(std::max)(metrics.statusBadgeMinWidth, estimatedBadgeWidth),
|
||
|
|
ClampNonNegative((trailingX - contentLeft) * 0.45f));
|
||
|
|
if (badgeWidth > 20.0f) {
|
||
|
|
trailingX -= badgeWidth;
|
||
|
|
layout.statusBadgeRect = UIRect(
|
||
|
|
trailingX,
|
||
|
|
layout.valueRect.y + ClampNonNegative((layout.valueRect.height - metrics.statusBadgeHeight) * 0.5f),
|
||
|
|
badgeWidth,
|
||
|
|
(std::min)(metrics.statusBadgeHeight, layout.valueRect.height));
|
||
|
|
trailingX -= metrics.statusBadgeGap;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
layout.textRect = UIRect(
|
||
|
|
contentLeft + metrics.valueTextInsetX,
|
||
|
|
layout.valueRect.y,
|
||
|
|
ClampNonNegative(trailingX - contentLeft - metrics.valueTextInsetX),
|
||
|
|
layout.valueRect.height);
|
||
|
|
return layout;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIEditorAssetFieldHitTarget HitTestUIEditorAssetField(
|
||
|
|
const UIEditorAssetFieldLayout& layout,
|
||
|
|
const UIPoint& point) {
|
||
|
|
if (layout.clearRect.width > 0.0f &&
|
||
|
|
IsUIEditorAssetFieldPointInside(layout.clearRect, point)) {
|
||
|
|
return { UIEditorAssetFieldHitTargetKind::ClearButton };
|
||
|
|
}
|
||
|
|
if (layout.pickerRect.width > 0.0f &&
|
||
|
|
IsUIEditorAssetFieldPointInside(layout.pickerRect, point)) {
|
||
|
|
return { UIEditorAssetFieldHitTargetKind::PickerButton };
|
||
|
|
}
|
||
|
|
if (IsUIEditorAssetFieldPointInside(layout.valueRect, point)) {
|
||
|
|
return { UIEditorAssetFieldHitTargetKind::ValueBox };
|
||
|
|
}
|
||
|
|
if (IsUIEditorAssetFieldPointInside(layout.bounds, point)) {
|
||
|
|
return { UIEditorAssetFieldHitTargetKind::Row };
|
||
|
|
}
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
void AppendUIEditorAssetFieldBackground(
|
||
|
|
UIDrawList& drawList,
|
||
|
|
const UIEditorAssetFieldLayout& layout,
|
||
|
|
const UIEditorAssetFieldSpec& spec,
|
||
|
|
const UIEditorAssetFieldState& state,
|
||
|
|
const UIEditorAssetFieldPalette& palette,
|
||
|
|
const UIEditorAssetFieldMetrics& metrics) {
|
||
|
|
const auto rowFillColor = ResolveRowFillColor(state, palette);
|
||
|
|
if (rowFillColor.a > 0.0f) {
|
||
|
|
drawList.AddFilledRect(layout.bounds, rowFillColor, metrics.cornerRounding);
|
||
|
|
}
|
||
|
|
|
||
|
|
const auto rowBorderColor = state.focused ? palette.focusedBorderColor : palette.borderColor;
|
||
|
|
if (rowBorderColor.a > 0.0f) {
|
||
|
|
drawList.AddRectOutline(
|
||
|
|
layout.bounds,
|
||
|
|
rowBorderColor,
|
||
|
|
state.focused ? metrics.focusedBorderThickness : metrics.borderThickness,
|
||
|
|
metrics.cornerRounding);
|
||
|
|
}
|
||
|
|
|
||
|
|
drawList.AddFilledRect(
|
||
|
|
layout.valueRect,
|
||
|
|
ResolveValueFillColor(spec, state, palette),
|
||
|
|
metrics.valueBoxRounding);
|
||
|
|
drawList.AddRectOutline(
|
||
|
|
layout.valueRect,
|
||
|
|
state.focused ? palette.controlFocusedBorderColor : palette.controlBorderColor,
|
||
|
|
state.focused ? metrics.focusedBorderThickness : metrics.borderThickness,
|
||
|
|
metrics.valueBoxRounding);
|
||
|
|
|
||
|
|
if (HasUIEditorAssetFieldValue(spec)) {
|
||
|
|
drawList.AddFilledRectLinearGradient(
|
||
|
|
layout.previewRect,
|
||
|
|
palette.previewBaseColor,
|
||
|
|
spec.tint,
|
||
|
|
::XCEngine::UI::UILinearGradientDirection::Vertical,
|
||
|
|
metrics.previewRounding);
|
||
|
|
} else {
|
||
|
|
drawList.AddFilledRect(layout.previewRect, palette.previewEmptyColor, metrics.previewRounding);
|
||
|
|
}
|
||
|
|
drawList.AddRectOutline(
|
||
|
|
layout.previewRect,
|
||
|
|
palette.previewBorderColor,
|
||
|
|
metrics.borderThickness,
|
||
|
|
metrics.previewRounding);
|
||
|
|
|
||
|
|
if (layout.statusBadgeRect.width > 0.0f && layout.statusBadgeRect.height > 0.0f) {
|
||
|
|
drawList.AddFilledRect(layout.statusBadgeRect, palette.statusBadgeColor, metrics.badgeRounding);
|
||
|
|
drawList.AddRectOutline(
|
||
|
|
layout.statusBadgeRect,
|
||
|
|
palette.statusBadgeBorderColor,
|
||
|
|
metrics.borderThickness,
|
||
|
|
metrics.badgeRounding);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (layout.pickerRect.width > 0.0f) {
|
||
|
|
const auto fill =
|
||
|
|
ResolveActionFillColor(UIEditorAssetFieldHitTargetKind::PickerButton, state, palette);
|
||
|
|
drawList.AddFilledRect(layout.pickerRect, fill, metrics.valueBoxRounding);
|
||
|
|
drawList.AddLine(
|
||
|
|
UIPoint(layout.pickerRect.x, layout.pickerRect.y + 1.0f),
|
||
|
|
UIPoint(layout.pickerRect.x, layout.pickerRect.y + layout.pickerRect.height - 1.0f),
|
||
|
|
palette.separatorColor,
|
||
|
|
1.0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (layout.clearRect.width > 0.0f) {
|
||
|
|
const auto fill =
|
||
|
|
ResolveActionFillColor(UIEditorAssetFieldHitTargetKind::ClearButton, state, palette);
|
||
|
|
drawList.AddFilledRect(layout.clearRect, fill, metrics.valueBoxRounding);
|
||
|
|
drawList.AddLine(
|
||
|
|
UIPoint(layout.clearRect.x, layout.clearRect.y + 1.0f),
|
||
|
|
UIPoint(layout.clearRect.x, layout.clearRect.y + layout.clearRect.height - 1.0f),
|
||
|
|
palette.separatorColor,
|
||
|
|
1.0f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void AppendUIEditorAssetFieldForeground(
|
||
|
|
UIDrawList& drawList,
|
||
|
|
const UIEditorAssetFieldLayout& layout,
|
||
|
|
const UIEditorAssetFieldSpec& spec,
|
||
|
|
const UIEditorAssetFieldState&,
|
||
|
|
const UIEditorAssetFieldPalette& palette,
|
||
|
|
const UIEditorAssetFieldMetrics& metrics) {
|
||
|
|
drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.labelRect, metrics.labelFontSize));
|
||
|
|
drawList.AddText(
|
||
|
|
UIPoint(
|
||
|
|
layout.labelRect.x,
|
||
|
|
ResolveUIEditorTextTop(layout.labelRect, metrics.labelFontSize, metrics.labelTextInsetY)),
|
||
|
|
spec.label,
|
||
|
|
palette.labelColor,
|
||
|
|
metrics.labelFontSize);
|
||
|
|
drawList.PopClipRect();
|
||
|
|
|
||
|
|
drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.previewRect, metrics.previewGlyphFontSize));
|
||
|
|
drawList.AddText(
|
||
|
|
UIPoint(
|
||
|
|
layout.previewRect.x +
|
||
|
|
ClampNonNegative((layout.previewRect.width - metrics.previewGlyphFontSize) * 0.5f),
|
||
|
|
ResolveUIEditorTextTop(layout.previewRect, metrics.previewGlyphFontSize, -1.0f)),
|
||
|
|
ResolveUIEditorAssetFieldPreviewGlyph(spec),
|
||
|
|
palette.previewGlyphColor,
|
||
|
|
metrics.previewGlyphFontSize);
|
||
|
|
drawList.PopClipRect();
|
||
|
|
|
||
|
|
drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.textRect, metrics.valueFontSize));
|
||
|
|
drawList.AddText(
|
||
|
|
UIPoint(
|
||
|
|
layout.textRect.x,
|
||
|
|
ResolveUIEditorTextTop(layout.textRect, metrics.valueFontSize, metrics.valueTextInsetY)),
|
||
|
|
ResolveUIEditorAssetFieldValueText(spec),
|
||
|
|
HasUIEditorAssetFieldValue(spec) ? palette.valueColor : palette.emptyValueColor,
|
||
|
|
metrics.valueFontSize);
|
||
|
|
drawList.PopClipRect();
|
||
|
|
|
||
|
|
if (layout.statusBadgeRect.width > 0.0f && layout.statusBadgeRect.height > 0.0f) {
|
||
|
|
const UIRect badgeTextRect(
|
||
|
|
layout.statusBadgeRect.x + metrics.statusBadgePaddingX,
|
||
|
|
layout.statusBadgeRect.y,
|
||
|
|
ClampNonNegative(layout.statusBadgeRect.width - metrics.statusBadgePaddingX * 2.0f),
|
||
|
|
layout.statusBadgeRect.height);
|
||
|
|
drawList.PushClipRect(ResolveUIEditorTextClipRect(badgeTextRect, metrics.statusBadgeFontSize));
|
||
|
|
drawList.AddText(
|
||
|
|
UIPoint(
|
||
|
|
badgeTextRect.x,
|
||
|
|
ResolveUIEditorTextTop(badgeTextRect, metrics.statusBadgeFontSize, -1.0f)),
|
||
|
|
spec.statusText,
|
||
|
|
palette.statusBadgeTextColor,
|
||
|
|
metrics.statusBadgeFontSize);
|
||
|
|
drawList.PopClipRect();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (layout.pickerRect.width > 0.0f) {
|
||
|
|
drawList.AddText(
|
||
|
|
UIPoint(
|
||
|
|
layout.pickerRect.x +
|
||
|
|
ClampNonNegative((layout.pickerRect.width - metrics.actionGlyphFontSize) * 0.5f),
|
||
|
|
ResolveUIEditorTextTop(
|
||
|
|
layout.pickerRect,
|
||
|
|
metrics.actionGlyphFontSize,
|
||
|
|
metrics.actionGlyphInsetY)),
|
||
|
|
"o",
|
||
|
|
palette.pickerGlyphColor,
|
||
|
|
metrics.actionGlyphFontSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (layout.clearRect.width > 0.0f) {
|
||
|
|
drawList.AddText(
|
||
|
|
UIPoint(
|
||
|
|
layout.clearRect.x +
|
||
|
|
ClampNonNegative((layout.clearRect.width - metrics.actionGlyphFontSize) * 0.5f),
|
||
|
|
ResolveUIEditorTextTop(
|
||
|
|
layout.clearRect,
|
||
|
|
metrics.actionGlyphFontSize,
|
||
|
|
metrics.actionGlyphInsetY)),
|
||
|
|
"X",
|
||
|
|
palette.clearGlyphColor,
|
||
|
|
metrics.actionGlyphFontSize);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void AppendUIEditorAssetField(
|
||
|
|
UIDrawList& drawList,
|
||
|
|
const UIRect& bounds,
|
||
|
|
const UIEditorAssetFieldSpec& spec,
|
||
|
|
const UIEditorAssetFieldState& state,
|
||
|
|
const UIEditorAssetFieldPalette& palette,
|
||
|
|
const UIEditorAssetFieldMetrics& metrics) {
|
||
|
|
const UIEditorAssetFieldLayout layout = BuildUIEditorAssetFieldLayout(bounds, spec, metrics);
|
||
|
|
AppendUIEditorAssetFieldBackground(drawList, layout, spec, state, palette, metrics);
|
||
|
|
AppendUIEditorAssetFieldForeground(drawList, layout, spec, state, palette, metrics);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace XCEngine::UI::Editor::Widgets
|