2026-04-07 16:57:04 +08:00
|
|
|
#include <XCEditor/Widgets/UIEditorPropertyGrid.h>
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
#include <XCEditor/Core/UIEditorTheme.h>
|
|
|
|
|
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
|
|
|
|
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
|
|
|
|
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
|
|
|
|
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
|
|
|
|
#include <XCEditor/Widgets/UIEditorTextField.h>
|
|
|
|
|
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
|
|
|
|
|
2026-04-07 16:57:04 +08:00
|
|
|
#include <algorithm>
|
2026-04-08 02:52:28 +08:00
|
|
|
#include <utility>
|
2026-04-07 16:57:04 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine::UI::Editor::Widgets {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
using ::XCEngine::UI::UIDrawList;
|
|
|
|
|
using ::XCEngine::UI::UIPoint;
|
|
|
|
|
using ::XCEngine::UI::UIRect;
|
|
|
|
|
using ::XCEngine::UI::UISize;
|
|
|
|
|
using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect;
|
|
|
|
|
using ::XCEngine::UI::Widgets::UIPopupPlacement;
|
|
|
|
|
using ::XCEngine::UI::Editor::UIEditorMenuItemKind;
|
|
|
|
|
|
2026-04-07 16:57:04 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
UIPoint ResolveDisclosureGlyphPosition(
|
|
|
|
|
const UIRect& rect,
|
|
|
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
|
|
|
|
return UIPoint(
|
|
|
|
|
rect.x + metrics.disclosureGlyphInsetX,
|
|
|
|
|
rect.y + metrics.sectionTextInsetY + metrics.disclosureGlyphInsetY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorBoolFieldSpec BuildBoolFieldSpec(const UIEditorPropertyGridField& field) {
|
|
|
|
|
UIEditorBoolFieldSpec spec = {};
|
|
|
|
|
spec.fieldId = field.fieldId;
|
|
|
|
|
spec.label = field.label;
|
|
|
|
|
spec.value = field.boolValue;
|
|
|
|
|
spec.readOnly = field.readOnly;
|
|
|
|
|
return spec;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorNumberFieldSpec BuildNumberFieldSpec(const UIEditorPropertyGridField& field) {
|
|
|
|
|
UIEditorNumberFieldSpec spec = {};
|
|
|
|
|
spec.fieldId = field.fieldId;
|
|
|
|
|
spec.label = field.label;
|
|
|
|
|
spec.value = field.numberValue.value;
|
|
|
|
|
spec.step = field.numberValue.step;
|
|
|
|
|
spec.minValue = field.numberValue.minValue;
|
|
|
|
|
spec.maxValue = field.numberValue.maxValue;
|
|
|
|
|
spec.integerMode = field.numberValue.integerMode;
|
|
|
|
|
spec.readOnly = field.readOnly;
|
|
|
|
|
return spec;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorEnumFieldSpec BuildEnumFieldSpec(const UIEditorPropertyGridField& field) {
|
|
|
|
|
UIEditorEnumFieldSpec spec = {};
|
|
|
|
|
spec.fieldId = field.fieldId;
|
|
|
|
|
spec.label = field.label;
|
|
|
|
|
spec.options = field.enumValue.options;
|
|
|
|
|
spec.selectedIndex = field.enumValue.selectedIndex;
|
|
|
|
|
spec.readOnly = field.readOnly;
|
|
|
|
|
return spec;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field) {
|
|
|
|
|
UIEditorTextFieldSpec spec = {};
|
|
|
|
|
spec.fieldId = field.fieldId;
|
|
|
|
|
spec.label = field.label;
|
|
|
|
|
spec.value = field.valueText;
|
|
|
|
|
spec.readOnly = field.readOnly;
|
|
|
|
|
return spec;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct UIEditorPropertyGridFieldRects {
|
|
|
|
|
UIRect labelRect = {};
|
|
|
|
|
UIRect valueRect = {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UIEditorPropertyGridFieldRects ResolveFieldRects(
|
|
|
|
|
const UIRect& rowRect,
|
|
|
|
|
const UIEditorPropertyGridField& field,
|
|
|
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
|
|
|
|
switch (field.kind) {
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Bool: {
|
|
|
|
|
const UIEditorBoolFieldLayout fieldLayout = BuildUIEditorBoolFieldLayout(
|
|
|
|
|
rowRect,
|
|
|
|
|
BuildBoolFieldSpec(field),
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedBoolFieldMetrics(metrics));
|
|
|
|
|
return { fieldLayout.labelRect, fieldLayout.controlRect };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Number: {
|
|
|
|
|
const UIEditorNumberFieldLayout fieldLayout = BuildUIEditorNumberFieldLayout(
|
|
|
|
|
rowRect,
|
|
|
|
|
BuildNumberFieldSpec(field),
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(metrics));
|
|
|
|
|
return { fieldLayout.labelRect, fieldLayout.valueRect };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Enum: {
|
|
|
|
|
const UIEditorEnumFieldLayout fieldLayout = BuildUIEditorEnumFieldLayout(
|
|
|
|
|
rowRect,
|
|
|
|
|
BuildEnumFieldSpec(field),
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics));
|
|
|
|
|
return { fieldLayout.labelRect, fieldLayout.valueRect };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Text:
|
|
|
|
|
default: {
|
|
|
|
|
const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout(
|
|
|
|
|
rowRect,
|
|
|
|
|
BuildTextFieldSpec(field),
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics));
|
|
|
|
|
return { fieldLayout.labelRect, fieldLayout.valueRect };
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-07 16:57:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
const std::string& ResolveDisplayedTextFieldValue(
|
2026-04-07 16:57:04 +08:00
|
|
|
const UIEditorPropertyGridField& field,
|
|
|
|
|
const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel) {
|
2026-04-08 02:52:28 +08:00
|
|
|
if (field.kind == UIEditorPropertyGridFieldKind::Text &&
|
|
|
|
|
propertyEditModel.HasActiveEdit() &&
|
2026-04-07 16:57:04 +08:00
|
|
|
propertyEditModel.GetActiveFieldId() == field.fieldId) {
|
|
|
|
|
return propertyEditModel.GetStagedValue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return field.valueText;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
UIEditorBoolFieldHitTargetKind ResolveBoolHoveredTarget(
|
|
|
|
|
const UIEditorPropertyGridState& state,
|
|
|
|
|
const UIEditorPropertyGridField& field) {
|
|
|
|
|
if (state.hoveredFieldId != field.fieldId) {
|
|
|
|
|
return UIEditorBoolFieldHitTargetKind::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox
|
|
|
|
|
? UIEditorBoolFieldHitTargetKind::Checkbox
|
|
|
|
|
: UIEditorBoolFieldHitTargetKind::Row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorNumberFieldHitTargetKind ResolveNumberHoveredTarget(
|
|
|
|
|
const UIEditorPropertyGridState& state,
|
|
|
|
|
const UIEditorPropertyGridField& field) {
|
|
|
|
|
if (state.hoveredFieldId != field.fieldId) {
|
|
|
|
|
return UIEditorNumberFieldHitTargetKind::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox
|
|
|
|
|
? UIEditorNumberFieldHitTargetKind::ValueBox
|
|
|
|
|
: UIEditorNumberFieldHitTargetKind::Row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorEnumFieldHitTargetKind ResolveEnumHoveredTarget(
|
|
|
|
|
const UIEditorPropertyGridState& state,
|
|
|
|
|
const UIEditorPropertyGridField& field) {
|
|
|
|
|
if (state.hoveredFieldId != field.fieldId) {
|
|
|
|
|
return UIEditorEnumFieldHitTargetKind::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox
|
|
|
|
|
? UIEditorEnumFieldHitTargetKind::ValueBox
|
|
|
|
|
: UIEditorEnumFieldHitTargetKind::Row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorTextFieldHitTargetKind ResolveTextHoveredTarget(
|
|
|
|
|
const UIEditorPropertyGridState& state,
|
|
|
|
|
const UIEditorPropertyGridField& field) {
|
|
|
|
|
if (state.hoveredFieldId != field.fieldId) {
|
|
|
|
|
return UIEditorTextFieldHitTargetKind::None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox
|
|
|
|
|
? UIEditorTextFieldHitTargetKind::ValueBox
|
|
|
|
|
: UIEditorTextFieldHitTargetKind::Row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<UIEditorMenuPopupItem> BuildEnumPopupItems(
|
|
|
|
|
const UIEditorPropertyGridField& field) {
|
|
|
|
|
std::vector<UIEditorMenuPopupItem> items = {};
|
|
|
|
|
if (field.kind != UIEditorPropertyGridFieldKind::Enum) {
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items.reserve(field.enumValue.options.size());
|
|
|
|
|
const std::size_t selectedIndex =
|
|
|
|
|
field.enumValue.options.empty()
|
|
|
|
|
? 0u
|
|
|
|
|
: (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u);
|
|
|
|
|
for (std::size_t index = 0u; index < field.enumValue.options.size(); ++index) {
|
|
|
|
|
UIEditorMenuPopupItem item = {};
|
|
|
|
|
item.itemId = field.fieldId + "." + std::to_string(index);
|
|
|
|
|
item.kind = UIEditorMenuItemKind::Command;
|
|
|
|
|
item.label = field.enumValue.options[index];
|
|
|
|
|
item.enabled = !field.readOnly;
|
|
|
|
|
item.checked = index == selectedIndex;
|
|
|
|
|
items.push_back(std::move(item));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIRect ResolvePopupViewportRect(const UIRect& bounds) {
|
|
|
|
|
return UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BuildEnumPopupRuntime(
|
|
|
|
|
const UIEditorPropertyGridLayout& layout,
|
|
|
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
|
|
|
|
const UIEditorPropertyGridState& state,
|
|
|
|
|
const UIEditorMenuPopupMetrics& popupMetrics,
|
|
|
|
|
UIEditorMenuPopupLayout& popupLayout,
|
|
|
|
|
UIEditorMenuPopupState& popupState,
|
|
|
|
|
std::vector<UIEditorMenuPopupItem>& popupItems) {
|
|
|
|
|
if (state.popupFieldId.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::size_t visibleFieldIndex =
|
|
|
|
|
FindUIEditorPropertyGridVisibleFieldIndex(layout, state.popupFieldId, sections);
|
|
|
|
|
if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex ||
|
|
|
|
|
visibleFieldIndex >= layout.visibleFieldSectionIndices.size() ||
|
|
|
|
|
visibleFieldIndex >= layout.visibleFieldIndices.size()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex];
|
|
|
|
|
const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex];
|
|
|
|
|
if (sectionIndex >= sections.size() ||
|
|
|
|
|
fieldIndex >= sections[sectionIndex].fields.size()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex];
|
|
|
|
|
popupItems = BuildEnumPopupItems(field);
|
|
|
|
|
if (popupItems.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const float popupWidth = (std::max)(
|
|
|
|
|
layout.fieldValueRects[visibleFieldIndex].width,
|
|
|
|
|
ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics));
|
|
|
|
|
const float popupHeight = MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics);
|
|
|
|
|
const auto placement = ResolvePopupPlacementRect(
|
|
|
|
|
layout.fieldValueRects[visibleFieldIndex],
|
|
|
|
|
UISize(popupWidth, popupHeight),
|
|
|
|
|
ResolvePopupViewportRect(layout.bounds),
|
|
|
|
|
UIPopupPlacement::BottomStart);
|
|
|
|
|
|
|
|
|
|
popupLayout = BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics);
|
|
|
|
|
popupState.focused = state.focused || !state.popupFieldId.empty();
|
|
|
|
|
popupState.hoveredIndex =
|
|
|
|
|
state.popupHighlightedIndex < popupItems.size()
|
|
|
|
|
? state.popupHighlightedIndex
|
|
|
|
|
: UIEditorMenuPopupInvalidIndex;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 16:57:04 +08:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
bool IsUIEditorPropertyGridPointInside(
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIRect& rect,
|
|
|
|
|
const UIPoint& point) {
|
2026-04-07 16:57:04 +08:00
|
|
|
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 {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
std::string ResolveUIEditorPropertyGridFieldValueText(
|
|
|
|
|
const UIEditorPropertyGridField& field) {
|
|
|
|
|
switch (field.kind) {
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Bool:
|
|
|
|
|
return field.boolValue ? "true" : "false";
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Number:
|
|
|
|
|
return FormatUIEditorNumberFieldValue(BuildNumberFieldSpec(field));
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Enum:
|
|
|
|
|
return ResolveUIEditorEnumFieldValueText(BuildEnumFieldSpec(field));
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Text:
|
|
|
|
|
default:
|
|
|
|
|
return field.valueText;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 16:57:04 +08:00
|
|
|
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(
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIRect& bounds,
|
2026-04-07 16:57:04 +08:00
|
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
|
|
|
|
const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
|
|
|
|
|
const UIEditorPropertyGridMetrics& metrics) {
|
|
|
|
|
UIEditorPropertyGridLayout layout = {};
|
2026-04-08 02:52:28 +08:00
|
|
|
layout.bounds = UIRect(
|
2026-04-07 16:57:04 +08:00
|
|
|
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);
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIRect headerRect(
|
2026-04-07 16:57:04 +08:00
|
|
|
contentX,
|
|
|
|
|
cursorY,
|
|
|
|
|
contentWidth,
|
|
|
|
|
headerHeight);
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIRect disclosureRect(
|
2026-04-07 16:57:04 +08:00
|
|
|
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;
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIRect titleRect(
|
2026-04-07 16:57:04 +08:00
|
|
|
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);
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIRect rowRect(
|
2026-04-07 16:57:04 +08:00
|
|
|
contentX,
|
|
|
|
|
cursorY,
|
|
|
|
|
contentWidth,
|
|
|
|
|
rowHeight);
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIEditorPropertyGridFieldRects fieldRects =
|
|
|
|
|
ResolveFieldRects(rowRect, field, metrics);
|
2026-04-07 16:57:04 +08:00
|
|
|
|
|
|
|
|
layout.visibleFieldSectionIndices.push_back(sectionIndex);
|
|
|
|
|
layout.visibleFieldIndices.push_back(fieldIndex);
|
|
|
|
|
layout.fieldRowRects.push_back(rowRect);
|
2026-04-08 02:52:28 +08:00
|
|
|
layout.fieldLabelRects.push_back(fieldRects.labelRect);
|
|
|
|
|
layout.fieldValueRects.push_back(fieldRects.valueRect);
|
2026-04-07 16:57:04 +08:00
|
|
|
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,
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIPoint& point) {
|
2026-04-07 16:57:04 +08:00
|
|
|
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(
|
2026-04-08 02:52:28 +08:00
|
|
|
UIDrawList& drawList,
|
2026-04-07 16:57:04 +08:00
|
|
|
const UIEditorPropertyGridLayout& layout,
|
|
|
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
|
|
|
|
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
|
2026-04-08 02:52:28 +08:00
|
|
|
const ::XCEngine::UI::Widgets::UIPropertyEditModel&,
|
2026-04-07 16:57:04 +08:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
if (selected || hovered) {
|
|
|
|
|
drawList.AddFilledRect(
|
|
|
|
|
layout.fieldRowRects[visibleFieldIndex],
|
|
|
|
|
selected
|
|
|
|
|
? (state.focused ? palette.fieldSelectedFocusedColor : palette.fieldSelectedColor)
|
|
|
|
|
: palette.fieldHoverColor,
|
|
|
|
|
metrics.cornerRounding);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppendUIEditorPropertyGridForeground(
|
2026-04-08 02:52:28 +08:00
|
|
|
UIDrawList& drawList,
|
2026-04-07 16:57:04 +08:00
|
|
|
const UIEditorPropertyGridLayout& layout,
|
|
|
|
|
const std::vector<UIEditorPropertyGridSection>& sections,
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIEditorPropertyGridState& state,
|
2026-04-07 16:57:04 +08:00
|
|
|
const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
|
|
|
|
|
const UIEditorPropertyGridPalette& palette,
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIEditorPropertyGridMetrics& metrics,
|
|
|
|
|
const UIEditorMenuPopupPalette& popupPalette,
|
|
|
|
|
const UIEditorMenuPopupMetrics& popupMetrics) {
|
2026-04-07 16:57:04 +08:00
|
|
|
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],
|
2026-04-08 02:52:28 +08:00
|
|
|
metrics),
|
2026-04-07 16:57:04 +08:00
|
|
|
layout.sectionExpanded[sectionVisibleIndex] ? "v" : ">",
|
|
|
|
|
palette.disclosureColor,
|
2026-04-08 02:52:28 +08:00
|
|
|
metrics.disclosureGlyphFontSize);
|
2026-04-07 16:57:04 +08:00
|
|
|
drawList.AddText(
|
2026-04-08 02:52:28 +08:00
|
|
|
UIPoint(
|
2026-04-07 16:57:04 +08:00
|
|
|
layout.sectionTitleRects[sectionVisibleIndex].x,
|
|
|
|
|
layout.sectionTitleRects[sectionVisibleIndex].y + metrics.sectionTextInsetY),
|
|
|
|
|
section.title,
|
|
|
|
|
palette.sectionTextColor,
|
2026-04-08 02:52:28 +08:00
|
|
|
metrics.sectionFontSize);
|
2026-04-07 16:57:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIEditorBoolFieldMetrics boolMetrics =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedBoolFieldMetrics(metrics);
|
|
|
|
|
const UIEditorBoolFieldPalette boolPalette =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedBoolFieldPalette(palette);
|
|
|
|
|
const UIEditorNumberFieldMetrics numberMetrics =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(metrics);
|
|
|
|
|
const UIEditorNumberFieldPalette numberPalette =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldPalette(palette);
|
|
|
|
|
const UIEditorTextFieldMetrics textMetrics =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics);
|
|
|
|
|
const UIEditorTextFieldPalette textPalette =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(palette);
|
|
|
|
|
const UIEditorEnumFieldMetrics enumMetrics =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics);
|
|
|
|
|
const UIEditorEnumFieldPalette enumPalette =
|
|
|
|
|
::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldPalette(palette);
|
|
|
|
|
|
2026-04-07 16:57:04 +08:00
|
|
|
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;
|
|
|
|
|
|
2026-04-08 02:52:28 +08:00
|
|
|
switch (field.kind) {
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Bool: {
|
|
|
|
|
UIEditorBoolFieldState fieldState = {};
|
|
|
|
|
fieldState.hoveredTarget = ResolveBoolHoveredTarget(state, field);
|
|
|
|
|
fieldState.focused = state.focused;
|
|
|
|
|
fieldState.active = state.pressedFieldId == field.fieldId;
|
|
|
|
|
AppendUIEditorBoolField(
|
|
|
|
|
drawList,
|
|
|
|
|
layout.fieldRowRects[visibleFieldIndex],
|
|
|
|
|
BuildBoolFieldSpec(field),
|
|
|
|
|
fieldState,
|
|
|
|
|
boolPalette,
|
|
|
|
|
boolMetrics);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Number: {
|
|
|
|
|
UIEditorNumberFieldState fieldState = {};
|
|
|
|
|
fieldState.hoveredTarget = ResolveNumberHoveredTarget(state, field);
|
|
|
|
|
fieldState.activeTarget =
|
|
|
|
|
state.pressedFieldId == field.fieldId
|
|
|
|
|
? UIEditorNumberFieldHitTargetKind::ValueBox
|
|
|
|
|
: UIEditorNumberFieldHitTargetKind::None;
|
|
|
|
|
fieldState.focused = state.focused;
|
|
|
|
|
fieldState.editing = editing;
|
|
|
|
|
fieldState.displayText = editing
|
|
|
|
|
? propertyEditModel.GetStagedValue()
|
|
|
|
|
: ResolveUIEditorPropertyGridFieldValueText(field);
|
|
|
|
|
AppendUIEditorNumberField(
|
|
|
|
|
drawList,
|
|
|
|
|
layout.fieldRowRects[visibleFieldIndex],
|
|
|
|
|
BuildNumberFieldSpec(field),
|
|
|
|
|
fieldState,
|
|
|
|
|
numberPalette,
|
|
|
|
|
numberMetrics);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Enum: {
|
|
|
|
|
UIEditorEnumFieldState fieldState = {};
|
|
|
|
|
fieldState.hoveredTarget = ResolveEnumHoveredTarget(state, field);
|
|
|
|
|
fieldState.focused = state.focused;
|
|
|
|
|
fieldState.active = state.pressedFieldId == field.fieldId;
|
|
|
|
|
fieldState.popupOpen = state.popupFieldId == field.fieldId;
|
|
|
|
|
AppendUIEditorEnumField(
|
|
|
|
|
drawList,
|
|
|
|
|
layout.fieldRowRects[visibleFieldIndex],
|
|
|
|
|
BuildEnumFieldSpec(field),
|
|
|
|
|
fieldState,
|
|
|
|
|
enumPalette,
|
|
|
|
|
enumMetrics);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIEditorPropertyGridFieldKind::Text:
|
|
|
|
|
default: {
|
|
|
|
|
const std::string& displayedValue =
|
|
|
|
|
ResolveDisplayedTextFieldValue(field, propertyEditModel);
|
|
|
|
|
UIEditorTextFieldState fieldState = {};
|
|
|
|
|
fieldState.hoveredTarget = ResolveTextHoveredTarget(state, field);
|
|
|
|
|
fieldState.activeTarget =
|
|
|
|
|
state.pressedFieldId == field.fieldId
|
|
|
|
|
? (state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox
|
|
|
|
|
? UIEditorTextFieldHitTargetKind::ValueBox
|
|
|
|
|
: UIEditorTextFieldHitTargetKind::Row)
|
|
|
|
|
: UIEditorTextFieldHitTargetKind::None;
|
|
|
|
|
fieldState.focused = state.focused;
|
|
|
|
|
fieldState.editing = editing;
|
|
|
|
|
fieldState.displayText = displayedValue;
|
|
|
|
|
|
|
|
|
|
UIEditorTextFieldSpec fieldSpec = BuildTextFieldSpec(field);
|
|
|
|
|
fieldSpec.value = editing ? std::string() : displayedValue;
|
|
|
|
|
AppendUIEditorTextField(
|
|
|
|
|
drawList,
|
|
|
|
|
layout.fieldRowRects[visibleFieldIndex],
|
|
|
|
|
fieldSpec,
|
|
|
|
|
fieldState,
|
|
|
|
|
textPalette,
|
|
|
|
|
textMetrics);
|
|
|
|
|
|
|
|
|
|
if (editing) {
|
|
|
|
|
drawList.PushClipRect(
|
|
|
|
|
ResolveUIEditorTextClipRect(
|
|
|
|
|
layout.fieldValueRects[visibleFieldIndex],
|
|
|
|
|
metrics.tagFontSize));
|
|
|
|
|
drawList.AddText(
|
|
|
|
|
UIPoint(
|
|
|
|
|
layout.fieldValueRects[visibleFieldIndex].x +
|
|
|
|
|
(std::max)(0.0f, layout.fieldValueRects[visibleFieldIndex].width - 34.0f),
|
|
|
|
|
ResolveUIEditorTextTop(
|
|
|
|
|
layout.fieldValueRects[visibleFieldIndex],
|
|
|
|
|
metrics.tagFontSize,
|
|
|
|
|
metrics.valueTextInsetY)),
|
|
|
|
|
"EDIT",
|
|
|
|
|
palette.editTagColor,
|
|
|
|
|
metrics.tagFontSize);
|
|
|
|
|
drawList.PopClipRect();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-04-07 16:57:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawList.PopClipRect();
|
2026-04-08 02:52:28 +08:00
|
|
|
|
|
|
|
|
UIEditorMenuPopupLayout popupLayout = {};
|
|
|
|
|
UIEditorMenuPopupState popupState = {};
|
|
|
|
|
std::vector<UIEditorMenuPopupItem> popupItems = {};
|
|
|
|
|
if (BuildEnumPopupRuntime(layout, sections, state, popupMetrics, popupLayout, popupState, popupItems)) {
|
|
|
|
|
AppendUIEditorMenuPopupBackground(drawList, popupLayout, popupItems, popupState, popupPalette, popupMetrics);
|
|
|
|
|
AppendUIEditorMenuPopupForeground(drawList, popupLayout, popupItems, popupState, popupPalette, popupMetrics);
|
|
|
|
|
}
|
2026-04-07 16:57:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppendUIEditorPropertyGrid(
|
2026-04-08 02:52:28 +08:00
|
|
|
UIDrawList& drawList,
|
|
|
|
|
const UIRect& bounds,
|
2026-04-07 16:57:04 +08:00
|
|
|
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,
|
2026-04-08 02:52:28 +08:00
|
|
|
const UIEditorPropertyGridMetrics& metrics,
|
|
|
|
|
const UIEditorMenuPopupPalette& popupPalette,
|
|
|
|
|
const UIEditorMenuPopupMetrics& popupMetrics) {
|
2026-04-07 16:57:04 +08:00
|
|
|
const UIEditorPropertyGridLayout layout =
|
|
|
|
|
BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics);
|
|
|
|
|
AppendUIEditorPropertyGridBackground(
|
|
|
|
|
drawList,
|
|
|
|
|
layout,
|
|
|
|
|
sections,
|
|
|
|
|
selectionModel,
|
|
|
|
|
propertyEditModel,
|
|
|
|
|
state,
|
|
|
|
|
palette,
|
|
|
|
|
metrics);
|
|
|
|
|
AppendUIEditorPropertyGridForeground(
|
|
|
|
|
drawList,
|
|
|
|
|
layout,
|
|
|
|
|
sections,
|
2026-04-08 02:52:28 +08:00
|
|
|
state,
|
2026-04-07 16:57:04 +08:00
|
|
|
propertyEditModel,
|
|
|
|
|
palette,
|
2026-04-08 02:52:28 +08:00
|
|
|
metrics,
|
|
|
|
|
popupPalette,
|
|
|
|
|
popupMetrics);
|
2026-04-07 16:57:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace XCEngine::UI::Editor::Widgets
|