417 lines
16 KiB
C++
417 lines
16 KiB
C++
|
|
#include <XCEditor/Widgets/UIEditorPropertyGrid.h>
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
|
||
|
|
namespace XCEngine::UI::Editor::Widgets {
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
float ClampNonNegative(float value) {
|
||
|
|
return (std::max)(value, 0.0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
float ResolveSectionHeaderHeight(
|
||
|
|
const UIEditorPropertyGridSection& section,
|
||
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
||
|
|
return section.desiredHeaderHeight > 0.0f
|
||
|
|
? section.desiredHeaderHeight
|
||
|
|
: metrics.sectionHeaderHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
float ResolveFieldRowHeight(
|
||
|
|
const UIEditorPropertyGridField& field,
|
||
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
||
|
|
return field.desiredHeight > 0.0f
|
||
|
|
? field.desiredHeight
|
||
|
|
: metrics.fieldRowHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
::XCEngine::UI::UIPoint ResolveDisclosureGlyphPosition(
|
||
|
|
const ::XCEngine::UI::UIRect& rect,
|
||
|
|
float textInsetY) {
|
||
|
|
return ::XCEngine::UI::UIPoint(rect.x + 2.0f, rect.y + textInsetY - 1.0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
const std::string& ResolveDisplayedFieldValue(
|
||
|
|
const UIEditorPropertyGridField& field,
|
||
|
|
const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel) {
|
||
|
|
if (propertyEditModel.HasActiveEdit() &&
|
||
|
|
propertyEditModel.GetActiveFieldId() == field.fieldId) {
|
||
|
|
return propertyEditModel.GetStagedValue();
|
||
|
|
}
|
||
|
|
|
||
|
|
return field.valueText;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
bool IsUIEditorPropertyGridPointInside(
|
||
|
|
const ::XCEngine::UI::UIRect& rect,
|
||
|
|
const ::XCEngine::UI::UIPoint& point) {
|
||
|
|
return point.x >= rect.x &&
|
||
|
|
point.x <= rect.x + rect.width &&
|
||
|
|
point.y >= rect.y &&
|
||
|
|
point.y <= rect.y + rect.height;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::size_t FindUIEditorPropertyGridSectionIndex(
|
||
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
||
|
|
std::string_view sectionId) {
|
||
|
|
for (std::size_t sectionIndex = 0u; sectionIndex < sections.size(); ++sectionIndex) {
|
||
|
|
if (sections[sectionIndex].sectionId == sectionId) {
|
||
|
|
return sectionIndex;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return UIEditorPropertyGridInvalidIndex;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIEditorPropertyGridFieldLocation FindUIEditorPropertyGridFieldLocation(
|
||
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
||
|
|
std::string_view fieldId) {
|
||
|
|
for (std::size_t sectionIndex = 0u; sectionIndex < sections.size(); ++sectionIndex) {
|
||
|
|
const auto& fields = sections[sectionIndex].fields;
|
||
|
|
for (std::size_t fieldIndex = 0u; fieldIndex < fields.size(); ++fieldIndex) {
|
||
|
|
if (fields[fieldIndex].fieldId == fieldId) {
|
||
|
|
return { sectionIndex, fieldIndex };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
std::size_t FindUIEditorPropertyGridVisibleFieldIndex(
|
||
|
|
const UIEditorPropertyGridLayout& layout,
|
||
|
|
std::string_view fieldId,
|
||
|
|
const std::vector<UIEditorPropertyGridSection>& sections) {
|
||
|
|
for (std::size_t visibleIndex = 0u;
|
||
|
|
visibleIndex < layout.visibleFieldIndices.size();
|
||
|
|
++visibleIndex) {
|
||
|
|
const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleIndex];
|
||
|
|
const std::size_t fieldIndex = layout.visibleFieldIndices[visibleIndex];
|
||
|
|
if (sectionIndex >= sections.size() ||
|
||
|
|
fieldIndex >= sections[sectionIndex].fields.size()) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sections[sectionIndex].fields[fieldIndex].fieldId == fieldId) {
|
||
|
|
return visibleIndex;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return UIEditorPropertyGridInvalidIndex;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout(
|
||
|
|
const ::XCEngine::UI::UIRect& bounds,
|
||
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
||
|
|
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
||
|
|
UIEditorPropertyGridLayout layout = {};
|
||
|
|
layout.bounds = ::XCEngine::UI::UIRect(
|
||
|
|
bounds.x,
|
||
|
|
bounds.y,
|
||
|
|
ClampNonNegative(bounds.width),
|
||
|
|
ClampNonNegative(bounds.height));
|
||
|
|
|
||
|
|
const float inset = ClampNonNegative(metrics.contentInset);
|
||
|
|
const float contentX = layout.bounds.x + inset;
|
||
|
|
const float contentY = layout.bounds.y + inset;
|
||
|
|
const float contentWidth = (std::max)(0.0f, layout.bounds.width - inset * 2.0f);
|
||
|
|
|
||
|
|
float cursorY = contentY;
|
||
|
|
for (std::size_t sectionIndex = 0u; sectionIndex < sections.size(); ++sectionIndex) {
|
||
|
|
const UIEditorPropertyGridSection& section = sections[sectionIndex];
|
||
|
|
const float headerHeight = ResolveSectionHeaderHeight(section, metrics);
|
||
|
|
const bool expanded = expansionModel.IsExpanded(section.sectionId);
|
||
|
|
|
||
|
|
const ::XCEngine::UI::UIRect headerRect(
|
||
|
|
contentX,
|
||
|
|
cursorY,
|
||
|
|
contentWidth,
|
||
|
|
headerHeight);
|
||
|
|
const ::XCEngine::UI::UIRect disclosureRect(
|
||
|
|
headerRect.x + metrics.horizontalPadding,
|
||
|
|
headerRect.y + (headerRect.height - metrics.disclosureExtent) * 0.5f,
|
||
|
|
metrics.disclosureExtent,
|
||
|
|
metrics.disclosureExtent);
|
||
|
|
const float titleX =
|
||
|
|
disclosureRect.x + disclosureRect.width + metrics.disclosureLabelGap;
|
||
|
|
const ::XCEngine::UI::UIRect titleRect(
|
||
|
|
titleX,
|
||
|
|
headerRect.y,
|
||
|
|
(std::max)(0.0f, headerRect.x + headerRect.width - titleX - metrics.horizontalPadding),
|
||
|
|
headerRect.height);
|
||
|
|
|
||
|
|
layout.sectionIndices.push_back(sectionIndex);
|
||
|
|
layout.sectionHeaderRects.push_back(headerRect);
|
||
|
|
layout.sectionDisclosureRects.push_back(disclosureRect);
|
||
|
|
layout.sectionTitleRects.push_back(titleRect);
|
||
|
|
layout.sectionExpanded.push_back(expanded);
|
||
|
|
|
||
|
|
cursorY += headerHeight;
|
||
|
|
if (expanded) {
|
||
|
|
for (std::size_t fieldIndex = 0u; fieldIndex < section.fields.size(); ++fieldIndex) {
|
||
|
|
const UIEditorPropertyGridField& field = section.fields[fieldIndex];
|
||
|
|
const float rowHeight = ResolveFieldRowHeight(field, metrics);
|
||
|
|
const ::XCEngine::UI::UIRect rowRect(
|
||
|
|
contentX,
|
||
|
|
cursorY,
|
||
|
|
contentWidth,
|
||
|
|
rowHeight);
|
||
|
|
const float controlX =
|
||
|
|
(std::min)(
|
||
|
|
rowRect.x + rowRect.width - metrics.horizontalPadding,
|
||
|
|
rowRect.x + metrics.controlColumnStart);
|
||
|
|
const float labelWidth =
|
||
|
|
(std::max)(
|
||
|
|
0.0f,
|
||
|
|
controlX - rowRect.x - metrics.horizontalPadding - metrics.labelControlGap);
|
||
|
|
const ::XCEngine::UI::UIRect labelRect(
|
||
|
|
rowRect.x + metrics.horizontalPadding,
|
||
|
|
rowRect.y,
|
||
|
|
labelWidth,
|
||
|
|
rowRect.height);
|
||
|
|
const ::XCEngine::UI::UIRect valueRect(
|
||
|
|
controlX,
|
||
|
|
rowRect.y + metrics.valueBoxInsetY,
|
||
|
|
(std::max)(
|
||
|
|
0.0f,
|
||
|
|
rowRect.x + rowRect.width - controlX - metrics.horizontalPadding),
|
||
|
|
(std::max)(0.0f, rowRect.height - metrics.valueBoxInsetY * 2.0f));
|
||
|
|
|
||
|
|
layout.visibleFieldSectionIndices.push_back(sectionIndex);
|
||
|
|
layout.visibleFieldIndices.push_back(fieldIndex);
|
||
|
|
layout.fieldRowRects.push_back(rowRect);
|
||
|
|
layout.fieldLabelRects.push_back(labelRect);
|
||
|
|
layout.fieldValueRects.push_back(valueRect);
|
||
|
|
layout.fieldReadOnly.push_back(field.readOnly);
|
||
|
|
|
||
|
|
cursorY += rowHeight + metrics.rowGap;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!section.fields.empty()) {
|
||
|
|
cursorY -= metrics.rowGap;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
cursorY += metrics.sectionGap;
|
||
|
|
}
|
||
|
|
|
||
|
|
return layout;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIEditorPropertyGridHitTarget HitTestUIEditorPropertyGrid(
|
||
|
|
const UIEditorPropertyGridLayout& layout,
|
||
|
|
const ::XCEngine::UI::UIPoint& point) {
|
||
|
|
for (std::size_t sectionVisibleIndex = 0u;
|
||
|
|
sectionVisibleIndex < layout.sectionHeaderRects.size();
|
||
|
|
++sectionVisibleIndex) {
|
||
|
|
if (!IsUIEditorPropertyGridPointInside(layout.sectionHeaderRects[sectionVisibleIndex], point)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIEditorPropertyGridHitTarget target = {};
|
||
|
|
target.kind = UIEditorPropertyGridHitTargetKind::SectionHeader;
|
||
|
|
target.sectionIndex = layout.sectionIndices[sectionVisibleIndex];
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (std::size_t visibleFieldIndex = 0u;
|
||
|
|
visibleFieldIndex < layout.fieldRowRects.size();
|
||
|
|
++visibleFieldIndex) {
|
||
|
|
if (!IsUIEditorPropertyGridPointInside(layout.fieldRowRects[visibleFieldIndex], point)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIEditorPropertyGridHitTarget target = {};
|
||
|
|
target.visibleFieldIndex = visibleFieldIndex;
|
||
|
|
target.sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex];
|
||
|
|
target.fieldIndex = layout.visibleFieldIndices[visibleFieldIndex];
|
||
|
|
target.kind = IsUIEditorPropertyGridPointInside(layout.fieldValueRects[visibleFieldIndex], point)
|
||
|
|
? UIEditorPropertyGridHitTargetKind::ValueBox
|
||
|
|
: UIEditorPropertyGridHitTargetKind::FieldRow;
|
||
|
|
return target;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
void AppendUIEditorPropertyGridBackground(
|
||
|
|
::XCEngine::UI::UIDrawList& drawList,
|
||
|
|
const UIEditorPropertyGridLayout& layout,
|
||
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
||
|
|
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||
|
|
const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
|
||
|
|
const UIEditorPropertyGridState& state,
|
||
|
|
const UIEditorPropertyGridPalette& palette,
|
||
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
||
|
|
drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding);
|
||
|
|
drawList.AddRectOutline(
|
||
|
|
layout.bounds,
|
||
|
|
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||
|
|
state.focused ? metrics.focusedBorderThickness : metrics.borderThickness,
|
||
|
|
metrics.cornerRounding);
|
||
|
|
|
||
|
|
for (std::size_t sectionVisibleIndex = 0u;
|
||
|
|
sectionVisibleIndex < layout.sectionHeaderRects.size();
|
||
|
|
++sectionVisibleIndex) {
|
||
|
|
const UIEditorPropertyGridSection& section =
|
||
|
|
sections[layout.sectionIndices[sectionVisibleIndex]];
|
||
|
|
const bool hovered =
|
||
|
|
state.hoveredFieldId.empty() &&
|
||
|
|
state.hoveredSectionId == section.sectionId;
|
||
|
|
drawList.AddFilledRect(
|
||
|
|
layout.sectionHeaderRects[sectionVisibleIndex],
|
||
|
|
hovered ? palette.sectionHeaderHoverColor : palette.sectionHeaderColor,
|
||
|
|
metrics.cornerRounding);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (std::size_t visibleFieldIndex = 0u;
|
||
|
|
visibleFieldIndex < layout.fieldRowRects.size();
|
||
|
|
++visibleFieldIndex) {
|
||
|
|
const UIEditorPropertyGridSection& section =
|
||
|
|
sections[layout.visibleFieldSectionIndices[visibleFieldIndex]];
|
||
|
|
const UIEditorPropertyGridField& field =
|
||
|
|
section.fields[layout.visibleFieldIndices[visibleFieldIndex]];
|
||
|
|
const bool selected = selectionModel.IsSelected(field.fieldId);
|
||
|
|
const bool hovered = state.hoveredFieldId == field.fieldId;
|
||
|
|
const bool editing = propertyEditModel.HasActiveEdit() &&
|
||
|
|
propertyEditModel.GetActiveFieldId() == field.fieldId;
|
||
|
|
|
||
|
|
if (selected || hovered) {
|
||
|
|
drawList.AddFilledRect(
|
||
|
|
layout.fieldRowRects[visibleFieldIndex],
|
||
|
|
selected
|
||
|
|
? (state.focused ? palette.fieldSelectedFocusedColor : palette.fieldSelectedColor)
|
||
|
|
: palette.fieldHoverColor,
|
||
|
|
metrics.cornerRounding);
|
||
|
|
}
|
||
|
|
|
||
|
|
const ::XCEngine::UI::UIColor valueBoxColor =
|
||
|
|
field.readOnly
|
||
|
|
? palette.valueBoxReadOnlyColor
|
||
|
|
: (editing
|
||
|
|
? palette.valueBoxEditingColor
|
||
|
|
: (hovered ? palette.valueBoxHoverColor : palette.valueBoxColor));
|
||
|
|
drawList.AddFilledRect(
|
||
|
|
layout.fieldValueRects[visibleFieldIndex],
|
||
|
|
valueBoxColor,
|
||
|
|
metrics.valueBoxRounding);
|
||
|
|
drawList.AddRectOutline(
|
||
|
|
layout.fieldValueRects[visibleFieldIndex],
|
||
|
|
editing ? palette.valueBoxEditingBorderColor : palette.valueBoxBorderColor,
|
||
|
|
editing ? metrics.editOutlineThickness : metrics.borderThickness,
|
||
|
|
metrics.valueBoxRounding);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void AppendUIEditorPropertyGridForeground(
|
||
|
|
::XCEngine::UI::UIDrawList& drawList,
|
||
|
|
const UIEditorPropertyGridLayout& layout,
|
||
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
||
|
|
const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
|
||
|
|
const UIEditorPropertyGridPalette& palette,
|
||
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
||
|
|
drawList.PushClipRect(layout.bounds);
|
||
|
|
|
||
|
|
for (std::size_t sectionVisibleIndex = 0u;
|
||
|
|
sectionVisibleIndex < layout.sectionHeaderRects.size();
|
||
|
|
++sectionVisibleIndex) {
|
||
|
|
const UIEditorPropertyGridSection& section =
|
||
|
|
sections[layout.sectionIndices[sectionVisibleIndex]];
|
||
|
|
drawList.AddText(
|
||
|
|
ResolveDisclosureGlyphPosition(
|
||
|
|
layout.sectionDisclosureRects[sectionVisibleIndex],
|
||
|
|
metrics.sectionTextInsetY),
|
||
|
|
layout.sectionExpanded[sectionVisibleIndex] ? "v" : ">",
|
||
|
|
palette.disclosureColor,
|
||
|
|
12.0f);
|
||
|
|
drawList.AddText(
|
||
|
|
::XCEngine::UI::UIPoint(
|
||
|
|
layout.sectionTitleRects[sectionVisibleIndex].x,
|
||
|
|
layout.sectionTitleRects[sectionVisibleIndex].y + metrics.sectionTextInsetY),
|
||
|
|
section.title,
|
||
|
|
palette.sectionTextColor,
|
||
|
|
12.0f);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (std::size_t visibleFieldIndex = 0u;
|
||
|
|
visibleFieldIndex < layout.fieldRowRects.size();
|
||
|
|
++visibleFieldIndex) {
|
||
|
|
const UIEditorPropertyGridSection& section =
|
||
|
|
sections[layout.visibleFieldSectionIndices[visibleFieldIndex]];
|
||
|
|
const UIEditorPropertyGridField& field =
|
||
|
|
section.fields[layout.visibleFieldIndices[visibleFieldIndex]];
|
||
|
|
const bool editing = propertyEditModel.HasActiveEdit() &&
|
||
|
|
propertyEditModel.GetActiveFieldId() == field.fieldId;
|
||
|
|
const std::string& displayedValue =
|
||
|
|
ResolveDisplayedFieldValue(field, propertyEditModel);
|
||
|
|
|
||
|
|
drawList.PushClipRect(layout.fieldLabelRects[visibleFieldIndex]);
|
||
|
|
drawList.AddText(
|
||
|
|
::XCEngine::UI::UIPoint(
|
||
|
|
layout.fieldLabelRects[visibleFieldIndex].x,
|
||
|
|
layout.fieldLabelRects[visibleFieldIndex].y + metrics.labelTextInsetY),
|
||
|
|
field.label,
|
||
|
|
palette.labelTextColor,
|
||
|
|
12.0f);
|
||
|
|
drawList.PopClipRect();
|
||
|
|
|
||
|
|
drawList.PushClipRect(layout.fieldValueRects[visibleFieldIndex]);
|
||
|
|
drawList.AddText(
|
||
|
|
::XCEngine::UI::UIPoint(
|
||
|
|
layout.fieldValueRects[visibleFieldIndex].x + metrics.valueBoxInsetX,
|
||
|
|
layout.fieldValueRects[visibleFieldIndex].y + metrics.valueTextInsetY),
|
||
|
|
displayedValue,
|
||
|
|
field.readOnly ? palette.readOnlyValueTextColor : palette.valueTextColor,
|
||
|
|
12.0f);
|
||
|
|
if (editing) {
|
||
|
|
drawList.AddText(
|
||
|
|
::XCEngine::UI::UIPoint(
|
||
|
|
layout.fieldValueRects[visibleFieldIndex].x +
|
||
|
|
(std::max)(0.0f, layout.fieldValueRects[visibleFieldIndex].width - 34.0f),
|
||
|
|
layout.fieldValueRects[visibleFieldIndex].y + metrics.valueTextInsetY),
|
||
|
|
"EDIT",
|
||
|
|
palette.editTagColor,
|
||
|
|
11.0f);
|
||
|
|
}
|
||
|
|
drawList.PopClipRect();
|
||
|
|
}
|
||
|
|
|
||
|
|
drawList.PopClipRect();
|
||
|
|
}
|
||
|
|
|
||
|
|
void AppendUIEditorPropertyGrid(
|
||
|
|
::XCEngine::UI::UIDrawList& drawList,
|
||
|
|
const ::XCEngine::UI::UIRect& bounds,
|
||
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
||
|
|
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
||
|
|
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
||
|
|
const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
|
||
|
|
const UIEditorPropertyGridState& state,
|
||
|
|
const UIEditorPropertyGridPalette& palette,
|
||
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
||
|
|
const UIEditorPropertyGridLayout layout =
|
||
|
|
BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics);
|
||
|
|
AppendUIEditorPropertyGridBackground(
|
||
|
|
drawList,
|
||
|
|
layout,
|
||
|
|
sections,
|
||
|
|
selectionModel,
|
||
|
|
propertyEditModel,
|
||
|
|
state,
|
||
|
|
palette,
|
||
|
|
metrics);
|
||
|
|
AppendUIEditorPropertyGridForeground(
|
||
|
|
drawList,
|
||
|
|
layout,
|
||
|
|
sections,
|
||
|
|
propertyEditModel,
|
||
|
|
palette,
|
||
|
|
metrics);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace XCEngine::UI::Editor::Widgets
|