#include #include #include #include #include #include #include #include #include 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; using ::XCEngine::UI::UISize; using ::XCEngine::UI::Editor::BuildUIEditorPropertyGridColorFieldMetrics; using ::XCEngine::UI::Editor::UIEditorColorFieldInteractionFrame; using ::XCEngine::UI::Editor::UIEditorColorFieldInteractionState; using ::XCEngine::UI::Editor::UpdateUIEditorColorFieldInteraction; using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; using ::XCEngine::UI::Widgets::UIPopupPlacement; using Widgets::BuildUIEditorPropertyGridLayout; using Widgets::FindUIEditorPropertyGridFieldLocation; using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; using Widgets::HitTestUIEditorPropertyGrid; using Widgets::IsUIEditorPropertyGridPointInside; using Widgets::ResolveUIEditorPropertyGridFieldValueText; using Widgets::UIEditorPropertyGridField; using Widgets::UIEditorPropertyGridFieldKind; using Widgets::UIEditorPropertyGridHitTarget; using Widgets::UIEditorPropertyGridHitTargetKind; using Widgets::UIEditorPropertyGridInvalidIndex; using Widgets::UIEditorPropertyGridLayout; using Widgets::UIEditorPropertyGridSection; using Widgets::UIEditorPropertyGridColorFieldVisualState; using Widgets::UIEditorMenuPopupHitTarget; using Widgets::UIEditorMenuPopupHitTargetKind; using Widgets::UIEditorMenuPopupInvalidIndex; using Widgets::UIEditorMenuPopupItem; using Widgets::UIEditorMenuPopupLayout; using Widgets::UIEditorColorFieldHitTargetKind; using Widgets::UIEditorColorFieldSpec; 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)) { 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(); 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('0') && character <= static_cast('9')) { return true; } if (character == static_cast('-') || character == static_cast('+')) { return true; } return !field.numberValue.integerMode && character == static_cast('.'); } 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; } UIEditorPropertyGridField* FindMutableField( std::vector& 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 §ions[location.sectionIndex].fields[location.fieldIndex]; } const UIEditorPropertyGridField* FindField( const std::vector& 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 §ions[location.sectionIndex].fields[location.fieldIndex]; } void SetChangedValueResult( const UIEditorPropertyGridField& field, UIEditorPropertyGridInteractionResult& result) { result.fieldValueChanged = true; result.changedFieldId = field.fieldId; result.changedValue = ResolveUIEditorPropertyGridFieldValueText(field); } 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& sections) { const auto isVisibleColorFieldId = [&layout, §ions](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; } } std::vector BuildPopupItems( const UIEditorPropertyGridField& field) { std::vector 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& sections, const Widgets::UIEditorMenuPopupMetrics& popupMetrics, UIEditorMenuPopupLayout& popupLayout, std::vector& 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& sections, const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { if (!state.hasPointerPosition) { return {}; } UIEditorMenuPopupLayout popupLayout = {}; std::vector popupItems = {}; if (!BuildPopupLayout(state, layout, sections, popupMetrics, popupLayout, popupItems)) { return {}; } return Widgets::HitTestUIEditorMenuPopup(popupLayout, popupItems, state.pointerPosition); } void SyncHoverTarget( UIEditorPropertyGridInteractionState& state, const UIEditorPropertyGridLayout& layout, const std::vector& sections, const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { ClearHoverState(state); if (!state.hasPointerPosition) { return; } 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); 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, const UIEditorPropertyGridLayout& layout, const std::vector& 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); } } bool SelectVisibleField( UIEditorPropertyGridInteractionState& state, ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, const UIEditorPropertyGridLayout& layout, const std::vector& 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, const UIEditorPropertyGridField& field, UIEditorPropertyGridInteractionResult& result) { if (field.readOnly || !IsInlineEditable(field)) { return false; } 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, std::vector& sections, UIEditorPropertyGridInteractionResult& result) { if (!propertyEditModel.HasActiveEdit()) { return false; } UIEditorPropertyGridField* field = FindMutableField(sections, propertyEditModel.GetActiveFieldId()); if (field == nullptr) { propertyEditModel.CancelEdit(); state.textInputState = {}; return false; } 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; } void ClosePopup( UIEditorPropertyGridInteractionState& state, UIEditorPropertyGridInteractionResult& result) { 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& 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; } 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; } field.boolValue = !field.boolValue; SetChangedValueResult(field, result); result.consumed = true; return true; } bool ProcessColorFieldEvent( UIEditorPropertyGridInteractionState& state, ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridLayout& layout, std::vector& 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, std::vector& sections, const std::vector& inputEvents, const Widgets::UIEditorPropertyGridMetrics& metrics, const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { UIEditorPropertyGridLayout layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); PruneColorFieldVisualStates(state, layout, sections); SyncKeyboardNavigation(state, selectionModel, layout, sections); 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 = {}; const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, layout, sections, popupMetrics); const UIEditorPropertyGridHitTarget hitTarget = state.hasPointerPosition ? HitTestUIEditorPropertyGrid(layout, state.pointerPosition) : UIEditorPropertyGridHitTarget {}; eventResult.hitTarget = hitTarget; 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: CommitActiveEdit(state, propertyEditModel, sections, eventResult); ClosePopup(state, eventResult); state.propertyGridState.focused = false; state.propertyGridState.pressedFieldId.clear(); state.hasPointerPosition = false; ClearHoverState(state); break; case UIInputEventType::PointerMove: case UIInputEventType::PointerEnter: case UIInputEventType::PointerLeave: break; case UIInputEventType::PointerButtonDown: { 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: { 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); } state.pressedPopupIndex = UIEditorPropertyGridInvalidIndex; state.propertyGridState.pressedFieldId.clear(); eventResult.consumed = true; break; } 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()) { 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; 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()) { 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) { 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); } } 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()) { 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; } if (!state.propertyGridState.popupFieldId.empty()) { const UIEditorPropertyGridField* popupField = FindField(sections, state.propertyGridState.popupFieldId); if (popupField != nullptr) { switch (static_cast(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(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) { CommitActiveEdit(state, propertyEditModel, sections, eventResult); } } break; } if ((static_cast(event.keyCode) == KeyCode::Enter || static_cast(event.keyCode) == KeyCode::Space) && selectionModel.HasSelection()) { 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(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; } 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); PruneColorFieldVisualStates(state, layout, sections); SyncKeyboardNavigation(state, selectionModel, layout, sections); 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 || eventResult.editCommitRejected || eventResult.editCanceled || eventResult.popupOpened || eventResult.popupClosed || eventResult.fieldValueChanged || eventResult.secondaryClicked || eventResult.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None || !eventResult.toggledSectionId.empty() || !eventResult.selectedFieldId.empty() || !eventResult.activeFieldId.empty() || !eventResult.committedFieldId.empty() || !eventResult.changedFieldId.empty()) { interactionResult = std::move(eventResult); } } layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); PruneColorFieldVisualStates(state, layout, sections); SyncKeyboardNavigation(state, selectionModel, layout, sections); 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