Files
XCEngine/new_editor/src/Fields/UIEditorPropertyGridInteraction.cpp

1197 lines
48 KiB
C++
Raw Normal View History

2026-04-10 00:41:28 +08:00
#include <XCEditor/Fields/UIEditorPropertyGridInteraction.h>
2026-04-10 00:41:28 +08:00
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorColorFieldInteraction.h>
#include <XCEditor/Menu/UIEditorMenuPopup.h>
2026-04-10 00:41:28 +08:00
#include <XCEditor/Fields/UIEditorNumberField.h>
2026-04-08 02:52:28 +08:00
#include <XCEngine/Input/InputTypes.h>
2026-04-08 02:52:28 +08:00
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
2026-04-08 02:52:28 +08:00
#include <algorithm>
#include <utility>
namespace XCEngine::UI::Editor {
namespace {
using ::XCEngine::Input::KeyCode;
using ::XCEngine::UI::Text::HandleKeyDown;
using ::XCEngine::UI::Text::InsertCharacter;
using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIPointerButton;
2026-04-08 02:52:28 +08:00
using ::XCEngine::UI::UISize;
2026-04-10 00:41:28 +08:00
using ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics;
using ::XCEngine::UI::Editor::UIEditorColorFieldInteractionFrame;
using ::XCEngine::UI::Editor::UIEditorColorFieldInteractionState;
using ::XCEngine::UI::Editor::UpdateUIEditorColorFieldInteraction;
2026-04-08 02:52:28 +08:00
using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect;
using ::XCEngine::UI::Widgets::UIPopupPlacement;
using Widgets::BuildUIEditorPropertyGridLayout;
2026-04-08 02:52:28 +08:00
using Widgets::FindUIEditorPropertyGridFieldLocation;
using Widgets::FindUIEditorPropertyGridVisibleFieldIndex;
using Widgets::HitTestUIEditorPropertyGrid;
using Widgets::IsUIEditorPropertyGridPointInside;
2026-04-08 02:52:28 +08:00
using Widgets::ResolveUIEditorPropertyGridFieldValueText;
using Widgets::UIEditorPropertyGridField;
using Widgets::UIEditorPropertyGridFieldKind;
using Widgets::UIEditorPropertyGridHitTarget;
using Widgets::UIEditorPropertyGridHitTargetKind;
using Widgets::UIEditorPropertyGridInvalidIndex;
2026-04-08 02:52:28 +08:00
using Widgets::UIEditorPropertyGridLayout;
using Widgets::UIEditorPropertyGridSection;
2026-04-10 00:41:28 +08:00
using Widgets::UIEditorPropertyGridColorFieldVisualState;
2026-04-08 02:52:28 +08:00
using Widgets::UIEditorMenuPopupHitTarget;
using Widgets::UIEditorMenuPopupHitTargetKind;
using Widgets::UIEditorMenuPopupInvalidIndex;
using Widgets::UIEditorMenuPopupItem;
using Widgets::UIEditorMenuPopupLayout;
2026-04-10 00:41:28 +08:00
using Widgets::UIEditorColorFieldHitTargetKind;
using Widgets::UIEditorColorFieldSpec;
2026-04-08 02:52:28 +08:00
using ::XCEngine::UI::Editor::UIEditorMenuItemKind;
bool ShouldUsePointerPosition(const UIInputEvent& event) {
switch (event.type) {
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
case UIInputEventType::PointerButtonDown:
case UIInputEventType::PointerButtonUp:
return true;
default:
return false;
}
}
bool HasSystemModifiers(const ::XCEngine::UI::UIInputModifiers& modifiers) {
return modifiers.control || modifiers.alt || modifiers.super;
}
bool ApplyKeyboardNavigation(
UIEditorPropertyGridInteractionState& state,
std::int32_t keyCode) {
switch (static_cast<KeyCode>(keyCode)) {
case KeyCode::Up:
return state.keyboardNavigation.MovePrevious();
case KeyCode::Down:
return state.keyboardNavigation.MoveNext();
case KeyCode::Home:
return state.keyboardNavigation.MoveHome();
case KeyCode::End:
return state.keyboardNavigation.MoveEnd();
default:
return false;
}
}
void ClearHoverState(UIEditorPropertyGridInteractionState& state) {
state.propertyGridState.hoveredSectionId.clear();
state.propertyGridState.hoveredFieldId.clear();
2026-04-08 02:52:28 +08:00
state.propertyGridState.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::None;
}
void ClearPopupState(UIEditorPropertyGridInteractionState& state) {
state.propertyGridState.popupFieldId.clear();
state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex;
state.pressedPopupIndex = UIEditorPropertyGridInvalidIndex;
}
bool IsInlineEditable(const UIEditorPropertyGridField& field) {
return field.kind == UIEditorPropertyGridFieldKind::Text ||
field.kind == UIEditorPropertyGridFieldKind::Number;
}
bool IsNumberEditCharacter(const UIEditorPropertyGridField& field, std::uint32_t character) {
if (field.kind != UIEditorPropertyGridFieldKind::Number) {
return true;
}
if (character >= static_cast<std::uint32_t>('0') &&
character <= static_cast<std::uint32_t>('9')) {
return true;
}
if (character == static_cast<std::uint32_t>('-') ||
character == static_cast<std::uint32_t>('+')) {
return true;
}
return !field.numberValue.integerMode && character == static_cast<std::uint32_t>('.');
}
2026-04-10 00:41:28 +08:00
UIEditorColorFieldSpec BuildColorFieldSpec(const UIEditorPropertyGridField& field) {
UIEditorColorFieldSpec spec = {};
spec.fieldId = field.fieldId;
spec.label = field.label;
spec.value = field.colorValue.value;
spec.showAlpha = field.colorValue.showAlpha;
spec.readOnly = field.readOnly;
return spec;
}
2026-04-08 02:52:28 +08:00
UIEditorPropertyGridField* FindMutableField(
std::vector<UIEditorPropertyGridSection>& sections,
std::string_view fieldId) {
const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId);
if (!location.IsValid() ||
location.sectionIndex >= sections.size() ||
location.fieldIndex >= sections[location.sectionIndex].fields.size()) {
return nullptr;
}
return &sections[location.sectionIndex].fields[location.fieldIndex];
}
const UIEditorPropertyGridField* FindField(
const std::vector<UIEditorPropertyGridSection>& sections,
std::string_view fieldId) {
const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId);
if (!location.IsValid() ||
location.sectionIndex >= sections.size() ||
location.fieldIndex >= sections[location.sectionIndex].fields.size()) {
return nullptr;
}
return &sections[location.sectionIndex].fields[location.fieldIndex];
}
void SetChangedValueResult(
const UIEditorPropertyGridField& field,
UIEditorPropertyGridInteractionResult& result) {
result.fieldValueChanged = true;
result.changedFieldId = field.fieldId;
result.changedValue = ResolveUIEditorPropertyGridFieldValueText(field);
}
2026-04-10 00:41:28 +08:00
void MergeInteractionResult(
UIEditorPropertyGridInteractionResult& accumulated,
const UIEditorPropertyGridInteractionResult& current) {
accumulated.consumed = accumulated.consumed || current.consumed;
accumulated.sectionToggled = accumulated.sectionToggled || current.sectionToggled;
accumulated.selectionChanged = accumulated.selectionChanged || current.selectionChanged;
accumulated.keyboardNavigated = accumulated.keyboardNavigated || current.keyboardNavigated;
accumulated.editStarted = accumulated.editStarted || current.editStarted;
accumulated.editValueChanged = accumulated.editValueChanged || current.editValueChanged;
accumulated.editCommitted = accumulated.editCommitted || current.editCommitted;
accumulated.editCommitRejected = accumulated.editCommitRejected || current.editCommitRejected;
accumulated.editCanceled = accumulated.editCanceled || current.editCanceled;
accumulated.popupOpened = accumulated.popupOpened || current.popupOpened;
accumulated.popupClosed = accumulated.popupClosed || current.popupClosed;
accumulated.fieldValueChanged = accumulated.fieldValueChanged || current.fieldValueChanged;
accumulated.secondaryClicked = accumulated.secondaryClicked || current.secondaryClicked;
if (current.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None) {
accumulated.hitTarget = current.hitTarget;
}
if (!current.toggledSectionId.empty()) {
accumulated.toggledSectionId = current.toggledSectionId;
}
if (!current.selectedFieldId.empty()) {
accumulated.selectedFieldId = current.selectedFieldId;
}
if (!current.activeFieldId.empty()) {
accumulated.activeFieldId = current.activeFieldId;
}
if (!current.committedFieldId.empty()) {
accumulated.committedFieldId = current.committedFieldId;
}
if (!current.committedValue.empty()) {
accumulated.committedValue = current.committedValue;
}
if (!current.changedFieldId.empty()) {
accumulated.changedFieldId = current.changedFieldId;
}
if (!current.changedValue.empty()) {
accumulated.changedValue = current.changedValue;
}
}
UIEditorPropertyGridColorFieldVisualState* FindMutableColorFieldVisualState(
UIEditorPropertyGridInteractionState& state,
std::string_view fieldId) {
for (UIEditorPropertyGridColorFieldVisualState& entry : state.propertyGridState.colorFieldStates) {
if (entry.fieldId == fieldId) {
return &entry;
}
}
return nullptr;
}
const UIEditorPropertyGridColorFieldVisualState* FindColorFieldVisualState(
const UIEditorPropertyGridInteractionState& state,
std::string_view fieldId) {
for (const UIEditorPropertyGridColorFieldVisualState& entry : state.propertyGridState.colorFieldStates) {
if (entry.fieldId == fieldId) {
return &entry;
}
}
return nullptr;
}
UIEditorColorFieldInteractionState BuildColorFieldInteractionState(
const UIEditorPropertyGridInteractionState& state,
std::string_view fieldId) {
UIEditorColorFieldInteractionState interactionState = {};
if (const UIEditorPropertyGridColorFieldVisualState* entry =
FindColorFieldVisualState(state, fieldId);
entry != nullptr) {
interactionState.colorFieldState = entry->state;
}
interactionState.pointerPosition = state.pointerPosition;
interactionState.hasPointerPosition = state.hasPointerPosition;
return interactionState;
}
void StoreColorFieldVisualState(
UIEditorPropertyGridInteractionState& state,
std::string_view fieldId,
const UIEditorColorFieldInteractionState& interactionState) {
if (UIEditorPropertyGridColorFieldVisualState* entry =
FindMutableColorFieldVisualState(state, fieldId);
entry != nullptr) {
entry->state = interactionState.colorFieldState;
return;
}
UIEditorPropertyGridColorFieldVisualState entry = {};
entry.fieldId = std::string(fieldId);
entry.state = interactionState.colorFieldState;
state.propertyGridState.colorFieldStates.push_back(std::move(entry));
}
void PruneColorFieldVisualStates(
UIEditorPropertyGridInteractionState& state,
const UIEditorPropertyGridLayout& layout,
const std::vector<UIEditorPropertyGridSection>& sections) {
const auto isVisibleColorFieldId = [&layout, &sections](std::string_view fieldId) {
const std::size_t visibleFieldIndex =
FindUIEditorPropertyGridVisibleFieldIndex(layout, fieldId, 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];
return sectionIndex < sections.size() &&
fieldIndex < sections[sectionIndex].fields.size() &&
sections[sectionIndex].fields[fieldIndex].kind == UIEditorPropertyGridFieldKind::Color;
};
state.propertyGridState.colorFieldStates.erase(
std::remove_if(
state.propertyGridState.colorFieldStates.begin(),
state.propertyGridState.colorFieldStates.end(),
[&isVisibleColorFieldId](const UIEditorPropertyGridColorFieldVisualState& entry) {
return !isVisibleColorFieldId(entry.fieldId);
}),
state.propertyGridState.colorFieldStates.end());
}
void CloseOtherColorFieldPopups(
UIEditorPropertyGridInteractionState& state,
std::string_view keepFieldId) {
for (UIEditorPropertyGridColorFieldVisualState& entry : state.propertyGridState.colorFieldStates) {
if (entry.fieldId == keepFieldId) {
continue;
}
entry.state.popupOpen = false;
entry.state.activeTarget = UIEditorColorFieldHitTargetKind::None;
}
}
2026-04-08 02:52:28 +08:00
std::vector<UIEditorMenuPopupItem> BuildPopupItems(
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;
}
::XCEngine::UI::UIRect ResolvePopupViewportRect(
const ::XCEngine::UI::UIRect& bounds) {
return ::XCEngine::UI::UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f);
}
bool BuildPopupLayout(
const UIEditorPropertyGridInteractionState& state,
const UIEditorPropertyGridLayout& layout,
const std::vector<UIEditorPropertyGridSection>& sections,
const Widgets::UIEditorMenuPopupMetrics& popupMetrics,
UIEditorMenuPopupLayout& popupLayout,
std::vector<UIEditorMenuPopupItem>& popupItems) {
if (state.propertyGridState.popupFieldId.empty()) {
return false;
}
const std::size_t visibleFieldIndex =
FindUIEditorPropertyGridVisibleFieldIndex(
layout,
state.propertyGridState.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 = BuildPopupItems(field);
if (popupItems.empty()) {
return false;
}
const float popupWidth = (std::max)(
layout.fieldValueRects[visibleFieldIndex].width,
Widgets::ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics));
const float popupHeight = Widgets::MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics);
const auto placement = ResolvePopupPlacementRect(
layout.fieldValueRects[visibleFieldIndex],
UISize(popupWidth, popupHeight),
ResolvePopupViewportRect(layout.bounds),
UIPopupPlacement::BottomStart);
popupLayout = Widgets::BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics);
return true;
}
UIEditorMenuPopupHitTarget ResolvePopupHit(
const UIEditorPropertyGridInteractionState& state,
const UIEditorPropertyGridLayout& layout,
const std::vector<UIEditorPropertyGridSection>& sections,
const Widgets::UIEditorMenuPopupMetrics& popupMetrics) {
if (!state.hasPointerPosition) {
return {};
}
UIEditorMenuPopupLayout popupLayout = {};
std::vector<UIEditorMenuPopupItem> popupItems = {};
if (!BuildPopupLayout(state, layout, sections, popupMetrics, popupLayout, popupItems)) {
return {};
}
return Widgets::HitTestUIEditorMenuPopup(popupLayout, popupItems, state.pointerPosition);
}
void SyncHoverTarget(
UIEditorPropertyGridInteractionState& state,
2026-04-08 02:52:28 +08:00
const UIEditorPropertyGridLayout& layout,
const std::vector<UIEditorPropertyGridSection>& sections,
const Widgets::UIEditorMenuPopupMetrics& popupMetrics) {
ClearHoverState(state);
if (!state.hasPointerPosition) {
return;
}
2026-04-08 02:52:28 +08:00
const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, layout, sections, popupMetrics);
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) {
state.propertyGridState.popupHighlightedIndex = popupHit.index;
return;
}
if (popupHit.kind != UIEditorMenuPopupHitTargetKind::None) {
return;
}
const UIEditorPropertyGridHitTarget hitTarget =
HitTestUIEditorPropertyGrid(layout, state.pointerPosition);
2026-04-08 02:52:28 +08:00
state.propertyGridState.hoveredHitTarget = hitTarget.kind;
if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader &&
hitTarget.sectionIndex < sections.size()) {
state.propertyGridState.hoveredSectionId = sections[hitTarget.sectionIndex].sectionId;
return;
}
if ((hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow ||
hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) &&
hitTarget.sectionIndex < sections.size() &&
hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size()) {
state.propertyGridState.hoveredFieldId =
sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex].fieldId;
}
}
void SyncKeyboardNavigation(
UIEditorPropertyGridInteractionState& state,
const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
2026-04-08 02:52:28 +08:00
const UIEditorPropertyGridLayout& layout,
const std::vector<UIEditorPropertyGridSection>& sections) {
state.keyboardNavigation.SetItemCount(layout.visibleFieldIndices.size());
state.keyboardNavigation.ClampToItemCount();
if (!selectionModel.HasSelection()) {
return;
}
const std::size_t selectedVisibleIndex =
FindUIEditorPropertyGridVisibleFieldIndex(
layout,
selectionModel.GetSelectedId(),
sections);
if (selectedVisibleIndex == UIEditorPropertyGridInvalidIndex) {
return;
}
if (!state.keyboardNavigation.HasCurrentIndex() ||
state.keyboardNavigation.GetCurrentIndex() != selectedVisibleIndex) {
state.keyboardNavigation.SetCurrentIndex(selectedVisibleIndex);
}
}
2026-04-08 02:52:28 +08:00
bool SelectVisibleField(
UIEditorPropertyGridInteractionState& state,
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
const UIEditorPropertyGridLayout& layout,
const std::vector<UIEditorPropertyGridSection>& sections,
std::size_t visibleFieldIndex,
UIEditorPropertyGridInteractionResult& result) {
if (visibleFieldIndex >= layout.visibleFieldIndices.size() ||
visibleFieldIndex >= layout.visibleFieldSectionIndices.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];
result.selectionChanged = selectionModel.SetSelection(field.fieldId);
result.selectedFieldId = field.fieldId;
result.consumed = true;
state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex);
return true;
}
bool BeginFieldEdit(
UIEditorPropertyGridInteractionState& state,
::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
2026-04-08 02:52:28 +08:00
const UIEditorPropertyGridField& field,
UIEditorPropertyGridInteractionResult& result) {
2026-04-08 02:52:28 +08:00
if (field.readOnly || !IsInlineEditable(field)) {
return false;
}
2026-04-08 02:52:28 +08:00
const std::string initialValue =
field.kind == UIEditorPropertyGridFieldKind::Text
? field.valueText
: ResolveUIEditorPropertyGridFieldValueText(field);
const bool changed = propertyEditModel.BeginEdit(field.fieldId, initialValue);
if (!changed &&
(!propertyEditModel.HasActiveEdit() ||
propertyEditModel.GetActiveFieldId() != field.fieldId)) {
return false;
}
state.textInputState.value = propertyEditModel.GetStagedValue();
state.textInputState.caret = state.textInputState.value.size();
result.editStarted = changed;
result.activeFieldId = field.fieldId;
result.consumed = true;
return true;
}
bool CommitActiveEdit(
UIEditorPropertyGridInteractionState& state,
::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
2026-04-08 02:52:28 +08:00
std::vector<UIEditorPropertyGridSection>& sections,
UIEditorPropertyGridInteractionResult& result) {
if (!propertyEditModel.HasActiveEdit()) {
return false;
}
2026-04-08 02:52:28 +08:00
UIEditorPropertyGridField* field =
FindMutableField(sections, propertyEditModel.GetActiveFieldId());
if (field == nullptr) {
propertyEditModel.CancelEdit();
state.textInputState = {};
return false;
}
2026-04-08 02:52:28 +08:00
result.activeFieldId = field->fieldId;
if (field->kind == UIEditorPropertyGridFieldKind::Number) {
Widgets::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;
double parsedValue = field->numberValue.value;
if (!Widgets::TryParseUIEditorNumberFieldValue(
spec,
propertyEditModel.GetStagedValue(),
parsedValue)) {
result.editCommitRejected = true;
result.consumed = true;
return false;
}
field->numberValue.value = parsedValue;
result.committedFieldId = field->fieldId;
result.committedValue = ResolveUIEditorPropertyGridFieldValueText(*field);
SetChangedValueResult(*field, result);
} else {
field->valueText = propertyEditModel.GetStagedValue();
result.committedFieldId = field->fieldId;
result.committedValue = field->valueText;
SetChangedValueResult(*field, result);
}
propertyEditModel.CommitEdit();
state.textInputState = {};
result.editCommitted = true;
result.consumed = true;
return true;
}
bool CancelActiveEdit(
UIEditorPropertyGridInteractionState& state,
::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
UIEditorPropertyGridInteractionResult& result) {
if (!propertyEditModel.HasActiveEdit()) {
return false;
}
result.activeFieldId = propertyEditModel.GetActiveFieldId();
if (!propertyEditModel.CancelEdit()) {
return false;
}
state.textInputState = {};
result.editCanceled = true;
result.consumed = true;
return true;
}
2026-04-08 02:52:28 +08:00
void ClosePopup(
UIEditorPropertyGridInteractionState& state,
UIEditorPropertyGridInteractionResult& result) {
2026-04-08 02:52:28 +08:00
if (state.propertyGridState.popupFieldId.empty()) {
return;
}
ClearPopupState(state);
result.popupClosed = true;
result.consumed = true;
}
void OpenPopup(
UIEditorPropertyGridInteractionState& state,
const UIEditorPropertyGridField& field,
UIEditorPropertyGridInteractionResult& result) {
if (field.kind != UIEditorPropertyGridFieldKind::Enum ||
field.readOnly ||
field.enumValue.options.empty()) {
return;
}
if (state.propertyGridState.popupFieldId == field.fieldId) {
return;
}
state.propertyGridState.popupFieldId = field.fieldId;
state.propertyGridState.popupHighlightedIndex =
(std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u);
result.popupOpened = true;
result.consumed = true;
}
void MovePopupHighlight(
UIEditorPropertyGridInteractionState& state,
const UIEditorPropertyGridField& field,
int delta) {
if (field.kind != UIEditorPropertyGridFieldKind::Enum ||
field.enumValue.options.empty()) {
state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex;
return;
}
if (state.propertyGridState.popupHighlightedIndex == UIEditorPropertyGridInvalidIndex ||
state.propertyGridState.popupHighlightedIndex >= field.enumValue.options.size()) {
state.propertyGridState.popupHighlightedIndex =
(std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u);
}
const std::size_t currentIndex = state.propertyGridState.popupHighlightedIndex;
if (delta < 0) {
state.propertyGridState.popupHighlightedIndex = currentIndex == 0u ? 0u : currentIndex - 1u;
} else {
state.propertyGridState.popupHighlightedIndex =
currentIndex + 1u >= field.enumValue.options.size()
? field.enumValue.options.size() - 1u
: currentIndex + 1u;
}
}
void JumpPopupHighlightToEdge(
UIEditorPropertyGridInteractionState& state,
const UIEditorPropertyGridField& field,
bool toEnd) {
if (field.kind != UIEditorPropertyGridFieldKind::Enum ||
field.enumValue.options.empty()) {
state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex;
return;
}
state.propertyGridState.popupHighlightedIndex = toEnd
? field.enumValue.options.size() - 1u
: 0u;
}
bool CommitPopupSelection(
UIEditorPropertyGridInteractionState& state,
std::vector<UIEditorPropertyGridSection>& sections,
UIEditorPropertyGridInteractionResult& result) {
UIEditorPropertyGridField* field =
FindMutableField(sections, state.propertyGridState.popupFieldId);
if (field == nullptr ||
field->kind != UIEditorPropertyGridFieldKind::Enum ||
field->readOnly ||
field->enumValue.options.empty() ||
state.propertyGridState.popupHighlightedIndex == UIEditorPropertyGridInvalidIndex ||
state.propertyGridState.popupHighlightedIndex >= field->enumValue.options.size()) {
return false;
}
2026-04-08 02:52:28 +08:00
field->enumValue.selectedIndex = state.propertyGridState.popupHighlightedIndex;
SetChangedValueResult(*field, result);
ClosePopup(state, result);
return true;
}
bool ToggleBoolField(
UIEditorPropertyGridField& field,
UIEditorPropertyGridInteractionResult& result) {
if (field.kind != UIEditorPropertyGridFieldKind::Bool || field.readOnly) {
return false;
}
2026-04-08 02:52:28 +08:00
field.boolValue = !field.boolValue;
SetChangedValueResult(field, result);
result.consumed = true;
return true;
}
2026-04-10 00:41:28 +08:00
bool ProcessColorFieldEvent(
UIEditorPropertyGridInteractionState& state,
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
const UIEditorPropertyGridLayout& layout,
std::vector<UIEditorPropertyGridSection>& sections,
const Widgets::UIEditorPropertyGridMetrics& metrics,
const UIInputEvent& event,
UIEditorPropertyGridInteractionResult& result) {
const Widgets::UIEditorColorFieldMetrics colorMetrics =
BuildUIEditorPropertyGridColorFieldMetrics(metrics);
const ::XCEngine::UI::UIRect popupViewportRect = ResolvePopupViewportRect(layout.bounds);
bool handled = false;
for (std::size_t visibleFieldIndex = 0u;
visibleFieldIndex < layout.visibleFieldIndices.size();
++visibleFieldIndex) {
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()) {
continue;
}
UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex];
if (field.kind != UIEditorPropertyGridFieldKind::Color) {
continue;
}
UIEditorColorFieldInteractionState colorState =
BuildColorFieldInteractionState(state, field.fieldId);
const bool popupWasOpen = colorState.colorFieldState.popupOpen;
UIEditorColorFieldSpec spec = BuildColorFieldSpec(field);
const UIEditorColorFieldInteractionFrame frame =
UpdateUIEditorColorFieldInteraction(
colorState,
spec,
layout.fieldRowRects[visibleFieldIndex],
{ event },
colorMetrics,
popupViewportRect);
if (!frame.result.consumed &&
frame.result.hitTarget.kind == UIEditorColorFieldHitTargetKind::None &&
!popupWasOpen &&
!colorState.colorFieldState.popupOpen) {
continue;
}
handled = true;
field.colorValue.value = spec.value;
field.colorValue.showAlpha = spec.showAlpha;
StoreColorFieldVisualState(state, field.fieldId, colorState);
if (frame.result.popupOpened) {
ClosePopup(state, result);
CloseOtherColorFieldPopups(state, field.fieldId);
}
state.propertyGridState.focused = colorState.colorFieldState.focused;
state.propertyGridState.pressedFieldId.clear();
if (frame.result.hitTarget.kind != UIEditorColorFieldHitTargetKind::None ||
frame.result.popupOpened ||
frame.result.colorChanged ||
popupWasOpen ||
colorState.colorFieldState.popupOpen) {
result.selectionChanged =
selectionModel.SetSelection(field.fieldId) || result.selectionChanged;
result.selectedFieldId = field.fieldId;
result.activeFieldId = field.fieldId;
state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex);
}
if (propertyEditModel.HasActiveEdit() &&
propertyEditModel.GetActiveFieldId() != field.fieldId &&
(frame.result.hitTarget.kind != UIEditorColorFieldHitTargetKind::None ||
frame.result.popupOpened)) {
CommitActiveEdit(state, propertyEditModel, sections, result);
}
result.popupOpened = result.popupOpened || frame.result.popupOpened;
result.popupClosed = result.popupClosed || frame.result.popupClosed;
result.consumed = result.consumed || frame.result.consumed;
if (frame.result.colorChanged) {
SetChangedValueResult(field, result);
}
if (frame.result.hitTarget.kind == UIEditorColorFieldHitTargetKind::Row) {
result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::FieldRow;
result.hitTarget.sectionIndex = sectionIndex;
result.hitTarget.fieldIndex = fieldIndex;
result.hitTarget.visibleFieldIndex = visibleFieldIndex;
} else if (frame.result.hitTarget.kind != UIEditorColorFieldHitTargetKind::None) {
result.hitTarget.kind = UIEditorPropertyGridHitTargetKind::ValueBox;
result.hitTarget.sectionIndex = sectionIndex;
result.hitTarget.fieldIndex = fieldIndex;
result.hitTarget.visibleFieldIndex = visibleFieldIndex;
}
}
return handled;
}
} // namespace
UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction(
UIEditorPropertyGridInteractionState& state,
::XCEngine::UI::Widgets::UISelectionModel& selectionModel,
::XCEngine::UI::Widgets::UIExpansionModel& expansionModel,
::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel,
const ::XCEngine::UI::UIRect& bounds,
2026-04-08 02:52:28 +08:00
std::vector<UIEditorPropertyGridSection>& sections,
const std::vector<UIInputEvent>& inputEvents,
2026-04-08 02:52:28 +08:00
const Widgets::UIEditorPropertyGridMetrics& metrics,
const Widgets::UIEditorMenuPopupMetrics& popupMetrics) {
UIEditorPropertyGridLayout layout =
BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics);
2026-04-10 00:41:28 +08:00
PruneColorFieldVisualStates(state, layout, sections);
SyncKeyboardNavigation(state, selectionModel, layout, sections);
2026-04-08 02:52:28 +08:00
SyncHoverTarget(state, layout, sections, popupMetrics);
UIEditorPropertyGridInteractionResult interactionResult = {};
for (const UIInputEvent& event : inputEvents) {
if (ShouldUsePointerPosition(event)) {
state.pointerPosition = event.position;
state.hasPointerPosition = true;
} else if (event.type == UIInputEventType::PointerLeave) {
state.hasPointerPosition = false;
}
UIEditorPropertyGridInteractionResult eventResult = {};
2026-04-08 02:52:28 +08:00
const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, layout, sections, popupMetrics);
const UIEditorPropertyGridHitTarget hitTarget =
state.hasPointerPosition
? HitTestUIEditorPropertyGrid(layout, state.pointerPosition)
: UIEditorPropertyGridHitTarget {};
eventResult.hitTarget = hitTarget;
2026-04-10 00:41:28 +08:00
if (ProcessColorFieldEvent(
state,
selectionModel,
propertyEditModel,
layout,
sections,
metrics,
event,
eventResult)) {
MergeInteractionResult(interactionResult, eventResult);
layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics);
PruneColorFieldVisualStates(state, layout, sections);
SyncKeyboardNavigation(state, selectionModel, layout, sections);
SyncHoverTarget(state, layout, sections, popupMetrics);
continue;
}
switch (event.type) {
case UIInputEventType::FocusGained:
state.propertyGridState.focused = true;
break;
case UIInputEventType::FocusLost:
2026-04-08 02:52:28 +08:00
CommitActiveEdit(state, propertyEditModel, sections, eventResult);
ClosePopup(state, eventResult);
state.propertyGridState.focused = false;
2026-04-08 02:52:28 +08:00
state.propertyGridState.pressedFieldId.clear();
state.hasPointerPosition = false;
ClearHoverState(state);
break;
case UIInputEventType::PointerMove:
case UIInputEventType::PointerEnter:
case UIInputEventType::PointerLeave:
break;
case UIInputEventType::PointerButtonDown: {
2026-04-08 02:52:28 +08:00
if (event.pointerButton == UIPointerButton::Left) {
const bool insideGrid =
state.hasPointerPosition &&
IsUIEditorPropertyGridPointInside(layout.bounds, state.pointerPosition);
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) {
state.propertyGridState.focused = true;
state.pressedPopupIndex = popupHit.index;
eventResult.consumed = true;
} else if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) {
state.propertyGridState.focused = true;
eventResult.consumed = true;
} else if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow ||
hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) {
state.propertyGridState.focused = true;
state.propertyGridState.pressedFieldId =
sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex].fieldId;
eventResult.consumed = true;
} else if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader || insideGrid) {
state.propertyGridState.focused = true;
state.propertyGridState.pressedFieldId.clear();
eventResult.consumed = true;
} else {
state.propertyGridState.focused = false;
state.propertyGridState.pressedFieldId.clear();
}
}
break;
}
case UIInputEventType::PointerButtonUp: {
2026-04-08 02:52:28 +08:00
if (event.pointerButton == UIPointerButton::Left) {
const bool insideGrid =
state.hasPointerPosition &&
IsUIEditorPropertyGridPointInside(layout.bounds, state.pointerPosition);
if (state.pressedPopupIndex != UIEditorPropertyGridInvalidIndex) {
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item &&
popupHit.index == state.pressedPopupIndex) {
eventResult.fieldValueChanged =
CommitPopupSelection(state, sections, eventResult);
} else if (popupHit.kind == UIEditorMenuPopupHitTargetKind::None) {
ClosePopup(state, eventResult);
}
2026-04-08 02:52:28 +08:00
state.pressedPopupIndex = UIEditorPropertyGridInvalidIndex;
state.propertyGridState.pressedFieldId.clear();
eventResult.consumed = true;
break;
}
2026-04-08 02:52:28 +08:00
if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) {
state.propertyGridState.pressedFieldId.clear();
eventResult.consumed = true;
break;
}
if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::None) {
CommitActiveEdit(state, propertyEditModel, sections, eventResult);
ClosePopup(state, eventResult);
state.propertyGridState.focused = insideGrid;
state.propertyGridState.pressedFieldId.clear();
if (insideGrid) {
eventResult.consumed = true;
}
break;
}
if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader &&
hitTarget.sectionIndex < sections.size()) {
2026-04-08 02:52:28 +08:00
CommitActiveEdit(state, propertyEditModel, sections, eventResult);
ClosePopup(state, eventResult);
const std::string& sectionId = sections[hitTarget.sectionIndex].sectionId;
eventResult.sectionToggled = expansionModel.ToggleExpanded(sectionId);
eventResult.toggledSectionId = sectionId;
eventResult.consumed = true;
state.propertyGridState.focused = true;
2026-04-08 02:52:28 +08:00
state.propertyGridState.pressedFieldId.clear();
break;
}
if ((hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow ||
hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) &&
hitTarget.sectionIndex < sections.size() &&
hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size()) {
2026-04-08 02:52:28 +08:00
UIEditorPropertyGridField& field =
sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex];
state.propertyGridState.focused = true;
SelectVisibleField(
state,
selectionModel,
layout,
sections,
hitTarget.visibleFieldIndex,
eventResult);
if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) {
2026-04-08 02:52:28 +08:00
if (field.kind == UIEditorPropertyGridFieldKind::Bool) {
CommitActiveEdit(state, propertyEditModel, sections, eventResult);
ClosePopup(state, eventResult);
ToggleBoolField(field, eventResult);
} else if (field.kind == UIEditorPropertyGridFieldKind::Enum) {
CommitActiveEdit(state, propertyEditModel, sections, eventResult);
if (state.propertyGridState.popupFieldId == field.fieldId) {
ClosePopup(state, eventResult);
} else {
ClearPopupState(state);
OpenPopup(state, field, eventResult);
}
} else {
ClosePopup(state, eventResult);
if (!(propertyEditModel.HasActiveEdit() &&
propertyEditModel.GetActiveFieldId() == field.fieldId)) {
CommitActiveEdit(state, propertyEditModel, sections, eventResult);
BeginFieldEdit(state, propertyEditModel, field, eventResult);
}
}
} else if (state.propertyGridState.popupFieldId != field.fieldId) {
ClosePopup(state, eventResult);
}
}
2026-04-08 02:52:28 +08:00
state.propertyGridState.pressedFieldId.clear();
} else if (event.pointerButton == UIPointerButton::Right &&
(hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow ||
hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) &&
hitTarget.sectionIndex < sections.size() &&
hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size()) {
2026-04-08 02:52:28 +08:00
const UIEditorPropertyGridField& field =
sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex];
eventResult.selectionChanged = selectionModel.SetSelection(field.fieldId);
eventResult.selectedFieldId = field.fieldId;
eventResult.secondaryClicked = true;
eventResult.consumed = true;
state.propertyGridState.focused = true;
if (hitTarget.visibleFieldIndex != UIEditorPropertyGridInvalidIndex) {
state.keyboardNavigation.SetCurrentIndex(hitTarget.visibleFieldIndex);
}
}
break;
}
case UIInputEventType::KeyDown:
if (!state.propertyGridState.focused) {
break;
}
2026-04-08 02:52:28 +08:00
if (!state.propertyGridState.popupFieldId.empty()) {
const UIEditorPropertyGridField* popupField =
FindField(sections, state.propertyGridState.popupFieldId);
if (popupField != nullptr) {
switch (static_cast<KeyCode>(event.keyCode)) {
case KeyCode::Up:
MovePopupHighlight(state, *popupField, -1);
eventResult.consumed = true;
break;
case KeyCode::Down:
MovePopupHighlight(state, *popupField, 1);
eventResult.consumed = true;
break;
case KeyCode::Home:
JumpPopupHighlightToEdge(state, *popupField, false);
eventResult.consumed = true;
break;
case KeyCode::End:
JumpPopupHighlightToEdge(state, *popupField, true);
eventResult.consumed = true;
break;
case KeyCode::Enter:
case KeyCode::Space:
CommitPopupSelection(state, sections, eventResult);
eventResult.consumed = true;
break;
case KeyCode::Escape:
ClosePopup(state, eventResult);
break;
default:
break;
}
}
break;
}
if (propertyEditModel.HasActiveEdit()) {
if (static_cast<KeyCode>(event.keyCode) == KeyCode::Escape) {
CancelActiveEdit(state, propertyEditModel, eventResult);
break;
}
const auto editResult = HandleKeyDown(
state.textInputState,
event.keyCode,
event.modifiers,
{});
if (editResult.handled) {
eventResult.consumed = true;
eventResult.activeFieldId = propertyEditModel.GetActiveFieldId();
if (editResult.valueChanged) {
propertyEditModel.UpdateStagedValue(state.textInputState.value);
eventResult.editValueChanged = true;
}
if (editResult.submitRequested) {
2026-04-08 02:52:28 +08:00
CommitActiveEdit(state, propertyEditModel, sections, eventResult);
}
}
break;
}
2026-04-08 02:52:28 +08:00
if ((static_cast<KeyCode>(event.keyCode) == KeyCode::Enter ||
static_cast<KeyCode>(event.keyCode) == KeyCode::Space) &&
selectionModel.HasSelection()) {
2026-04-08 02:52:28 +08:00
UIEditorPropertyGridField* field =
FindMutableField(sections, selectionModel.GetSelectedId());
if (field != nullptr) {
if (field->kind == UIEditorPropertyGridFieldKind::Bool) {
ToggleBoolField(*field, eventResult);
} else if (field->kind == UIEditorPropertyGridFieldKind::Enum) {
OpenPopup(state, *field, eventResult);
} else if (static_cast<KeyCode>(event.keyCode) == KeyCode::Enter) {
BeginFieldEdit(state, propertyEditModel, *field, eventResult);
}
}
break;
}
if (!event.modifiers.shift &&
!HasSystemModifiers(event.modifiers) &&
ApplyKeyboardNavigation(state, event.keyCode) &&
state.keyboardNavigation.HasCurrentIndex()) {
const std::size_t currentIndex = state.keyboardNavigation.GetCurrentIndex();
if (SelectVisibleField(
state,
selectionModel,
layout,
sections,
currentIndex,
eventResult)) {
eventResult.keyboardNavigated = true;
}
}
break;
case UIInputEventType::Character:
if (!state.propertyGridState.focused ||
!propertyEditModel.HasActiveEdit() ||
HasSystemModifiers(event.modifiers)) {
break;
}
2026-04-08 02:52:28 +08:00
if (const UIEditorPropertyGridField* field =
FindField(sections, propertyEditModel.GetActiveFieldId());
field == nullptr || !IsNumberEditCharacter(*field, event.character)) {
break;
}
if (InsertCharacter(state.textInputState, event.character)) {
propertyEditModel.UpdateStagedValue(state.textInputState.value);
eventResult.consumed = true;
eventResult.editValueChanged = true;
eventResult.activeFieldId = propertyEditModel.GetActiveFieldId();
}
break;
default:
break;
}
layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics);
2026-04-10 00:41:28 +08:00
PruneColorFieldVisualStates(state, layout, sections);
SyncKeyboardNavigation(state, selectionModel, layout, sections);
2026-04-08 02:52:28 +08:00
SyncHoverTarget(state, layout, sections, popupMetrics);
if (eventResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None &&
state.hasPointerPosition) {
eventResult.hitTarget = HitTestUIEditorPropertyGrid(layout, state.pointerPosition);
}
if (eventResult.consumed ||
eventResult.sectionToggled ||
eventResult.selectionChanged ||
eventResult.keyboardNavigated ||
eventResult.editStarted ||
eventResult.editValueChanged ||
eventResult.editCommitted ||
2026-04-08 02:52:28 +08:00
eventResult.editCommitRejected ||
eventResult.editCanceled ||
2026-04-08 02:52:28 +08:00
eventResult.popupOpened ||
eventResult.popupClosed ||
eventResult.fieldValueChanged ||
eventResult.secondaryClicked ||
eventResult.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None ||
!eventResult.toggledSectionId.empty() ||
!eventResult.selectedFieldId.empty() ||
!eventResult.activeFieldId.empty() ||
2026-04-08 02:52:28 +08:00
!eventResult.committedFieldId.empty() ||
!eventResult.changedFieldId.empty()) {
interactionResult = std::move(eventResult);
}
}
layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics);
2026-04-10 00:41:28 +08:00
PruneColorFieldVisualStates(state, layout, sections);
SyncKeyboardNavigation(state, selectionModel, layout, sections);
2026-04-08 02:52:28 +08:00
SyncHoverTarget(state, layout, sections, popupMetrics);
if (interactionResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None &&
state.hasPointerPosition) {
interactionResult.hitTarget = HitTestUIEditorPropertyGrid(layout, state.pointerPosition);
}
return {
std::move(layout),
std::move(interactionResult)
};
}
} // namespace XCEngine::UI::Editor