#include "InspectorPanel.h" #include "Composition/EditorPanelIds.h" #include #include #include "Features/Inspector/Components/IInspectorComponentEditor.h" #include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" #include "Scene/EditorSceneRuntime.h" #include #include namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; constexpr float kPanelPadding = 10.0f; constexpr float kTitleHeight = 18.0f; constexpr float kSubtitleHeight = 16.0f; constexpr float kHeaderGap = 10.0f; constexpr float kTitleFontSize = 13.0f; constexpr float kSubtitleFontSize = 11.0f; constexpr UIColor kTitleColor(0.930f, 0.930f, 0.930f, 1.0f); constexpr UIColor kSubtitleColor(0.660f, 0.660f, 0.660f, 1.0f); constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); bool ContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } float ResolveTextTop(float rectY, float rectHeight, float fontSize) { const float lineHeight = fontSize * 1.6f; return rectY + std::floor((rectHeight - lineHeight) * 0.5f); } std::vector FilterInspectorInputEvents( const UIRect& bounds, const std::vector& inputEvents, bool allowInteraction, bool panelActive) { if (!allowInteraction && !panelActive) { return {}; } std::vector filteredEvents = {}; filteredEvents.reserve(inputEvents.size()); for (const UIInputEvent& event : inputEvents) { switch (event.type) { case UIInputEventType::PointerMove: case UIInputEventType::PointerButtonDown: case UIInputEventType::PointerButtonUp: case UIInputEventType::PointerWheel: if (allowInteraction && ContainsPoint(bounds, event.position)) { filteredEvents.push_back(event); } break; case UIInputEventType::PointerLeave: filteredEvents.push_back(event); break; case UIInputEventType::FocusGained: case UIInputEventType::FocusLost: if (panelActive) { filteredEvents.push_back(event); } break; case UIInputEventType::KeyDown: case UIInputEventType::KeyUp: case UIInputEventType::Character: if (panelActive) { filteredEvents.push_back(event); } break; default: break; } } return filteredEvents; } UIEditorHostCommandEvaluationResult BuildEvaluationResult( bool executable, std::string message) { UIEditorHostCommandEvaluationResult result = {}; result.executable = executable; result.message = std::move(message); return result; } UIEditorHostCommandDispatchResult BuildDispatchResult( bool commandExecuted, std::string message) { UIEditorHostCommandDispatchResult result = {}; result.commandExecuted = commandExecuted; result.message = std::move(message); return result; } } // namespace const UIEditorPanelContentHostPanelState* InspectorPanel::FindMountedInspectorPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const { for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { if (panelState.panelId == kInspectorPanelId && panelState.mounted) { return &panelState; } } return nullptr; } void InspectorPanel::ResetPanelState() { m_visible = false; m_bounds = {}; m_subject = {}; m_subjectKey.clear(); m_presentation = {}; m_gridFrame = {}; m_knownSectionIds.clear(); ResetInteractionState(); } void InspectorPanel::ResetInteractionState() { m_fieldSelection.ClearSelection(); m_sectionExpansion.Clear(); m_propertyEditModel = {}; m_interactionState = {}; m_gridFrame = {}; } void InspectorPanel::SyncExpansionState(bool subjectChanged) { if (subjectChanged) { m_sectionExpansion.Clear(); m_knownSectionIds.clear(); } std::unordered_set currentSectionIds = {}; for (const Widgets::UIEditorPropertyGridSection& section : m_presentation.sections) { currentSectionIds.insert(section.sectionId); if (m_knownSectionIds.find(section.sectionId) == m_knownSectionIds.end()) { m_sectionExpansion.Expand(section.sectionId); } } m_knownSectionIds = std::move(currentSectionIds); } void InspectorPanel::SyncSelectionState() { if (m_fieldSelection.HasSelection() && !Widgets::FindUIEditorPropertyGridFieldLocation( m_presentation.sections, m_fieldSelection.GetSelectedId()) .IsValid()) { m_fieldSelection.ClearSelection(); } if (m_propertyEditModel.HasActiveEdit() && !Widgets::FindUIEditorPropertyGridFieldLocation( m_presentation.sections, m_propertyEditModel.GetActiveFieldId()) .IsValid()) { m_propertyEditModel.CancelEdit(); m_interactionState.textInputState = {}; } if (!m_interactionState.propertyGridState.popupFieldId.empty() && !Widgets::FindUIEditorPropertyGridFieldLocation( m_presentation.sections, m_interactionState.propertyGridState.popupFieldId) .IsValid()) { m_interactionState.propertyGridState.popupFieldId.clear(); } } std::string InspectorPanel::BuildSubjectKey() const { switch (m_subject.kind) { case InspectorSubjectKind::ProjectAsset: return std::string("project:") + m_subject.projectAsset.selection.itemId; case InspectorSubjectKind::SceneObject: return std::string("scene:") + m_subject.sceneObject.itemId; case InspectorSubjectKind::None: default: return "none"; } } UIRect InspectorPanel::BuildGridBounds() const { const float x = m_bounds.x + kPanelPadding; const float width = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f); const float y = m_bounds.y + kPanelPadding + kTitleHeight + kSubtitleHeight + kHeaderGap; const float height = (std::max)(m_bounds.y + m_bounds.height - kPanelPadding - y, 0.0f); return UIRect(x, y, width, height); } const InspectorPresentationComponentBinding* InspectorPanel::FindSelectedComponentBinding() const { if (!m_fieldSelection.HasSelection()) { return nullptr; } const std::string& fieldId = m_fieldSelection.GetSelectedId(); for (const InspectorPresentationComponentBinding& binding : m_presentation.componentBindings) { for (const std::string& ownedFieldId : binding.fieldIds) { if (ownedFieldId == fieldId) { return &binding; } } } return nullptr; } bool InspectorPanel::ApplyChangedField(std::string_view fieldId) { if (m_sceneRuntime == nullptr || m_subject.kind != InspectorSubjectKind::SceneObject) { return false; } const auto location = Widgets::FindUIEditorPropertyGridFieldLocation(m_presentation.sections, fieldId); if (!location.IsValid() || location.sectionIndex >= m_presentation.sections.size() || location.fieldIndex >= m_presentation.sections[location.sectionIndex].fields.size()) { return false; } const Widgets::UIEditorPropertyGridField& field = m_presentation.sections[location.sectionIndex].fields[location.fieldIndex]; const InspectorPresentationComponentBinding* binding = nullptr; for (const InspectorPresentationComponentBinding& candidate : m_presentation.componentBindings) { if (std::find( candidate.fieldIds.begin(), candidate.fieldIds.end(), field.fieldId) != candidate.fieldIds.end()) { binding = &candidate; break; } } if (binding == nullptr) { return false; } const IInspectorComponentEditor* editor = InspectorComponentEditorRegistry::Get().FindEditor(binding->typeName); if (editor == nullptr) { return false; } InspectorComponentEditorContext context = {}; context.gameObject = m_subject.sceneObject.gameObject; context.componentId = binding->componentId; context.typeName = binding->typeName; context.displayName = binding->displayName; context.removable = binding->removable; for (const EditorSceneComponentDescriptor& descriptor : m_sceneRuntime->GetSelectedComponents()) { if (descriptor.componentId == binding->componentId) { context.component = descriptor.component; break; } } return editor->ApplyFieldValue(*m_sceneRuntime, context, field); } void InspectorPanel::Update( const EditorSession& session, EditorSceneRuntime& sceneRuntime, const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector& inputEvents, bool allowInteraction, bool panelActive) { const UIEditorPanelContentHostPanelState* panelState = FindMountedInspectorPanel(contentHostFrame); if (panelState == nullptr) { ResetPanelState(); return; } m_visible = true; m_bounds = panelState->bounds; m_sceneRuntime = &sceneRuntime; m_subject = BuildInspectorSubject(session, sceneRuntime); const std::string nextSubjectKey = BuildSubjectKey(); const bool subjectChanged = m_subjectKey != nextSubjectKey; m_subjectKey = nextSubjectKey; if (subjectChanged) { ResetInteractionState(); } m_presentation = BuildInspectorPresentationModel( m_subject, sceneRuntime, InspectorComponentEditorRegistry::Get()); SyncExpansionState(subjectChanged); SyncSelectionState(); if (m_presentation.sections.empty()) { m_gridFrame = {}; return; } const std::vector filteredEvents = FilterInspectorInputEvents( m_bounds, inputEvents, allowInteraction, panelActive); m_gridFrame = UpdateUIEditorPropertyGridInteraction( m_interactionState, m_fieldSelection, m_sectionExpansion, m_propertyEditModel, BuildGridBounds(), m_presentation.sections, filteredEvents, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); if (m_gridFrame.result.fieldValueChanged && !m_gridFrame.result.changedFieldId.empty() && !ApplyChangedField(m_gridFrame.result.changedFieldId)) { m_presentation = BuildInspectorPresentationModel( m_subject, sceneRuntime, InspectorComponentEditorRegistry::Get()); SyncExpansionState(false); SyncSelectionState(); } } void InspectorPanel::Append(UIDrawList& drawList) const { if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) { return; } drawList.AddFilledRect(m_bounds, kSurfaceColor); const float contentX = m_bounds.x + kPanelPadding; const float contentWidth = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f); float nextY = m_bounds.y + kPanelPadding; const UIRect titleRect(contentX, nextY, contentWidth, kTitleHeight); drawList.AddText( UIPoint(titleRect.x, ResolveTextTop(titleRect.y, titleRect.height, kTitleFontSize)), m_presentation.title, kTitleColor, kTitleFontSize); nextY += titleRect.height; const UIRect subtitleRect(contentX, nextY, contentWidth, kSubtitleHeight); drawList.AddText( UIPoint( subtitleRect.x, ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)), m_presentation.subtitle, kSubtitleColor, kSubtitleFontSize); if (m_presentation.sections.empty()) { return; } Widgets::AppendUIEditorPropertyGrid( drawList, BuildGridBounds(), m_presentation.sections, m_fieldSelection, m_sectionExpansion, m_propertyEditModel, m_interactionState.propertyGridState, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(), ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); } UIEditorHostCommandEvaluationResult InspectorPanel::EvaluateEditCommand( std::string_view commandId) const { const InspectorPresentationComponentBinding* binding = FindSelectedComponentBinding(); if (binding == nullptr) { return BuildEvaluationResult( false, "Select an inspector component field first."); } if (commandId == "edit.delete") { if (!binding->removable || m_sceneRuntime == nullptr || !m_sceneRuntime->CanRemoveSelectedComponent(binding->componentId)) { return BuildEvaluationResult( false, "'" + binding->displayName + "' cannot be removed."); } return BuildEvaluationResult( true, "Remove inspector component '" + binding->displayName + "'."); } return BuildEvaluationResult( false, "Inspector does not expose this edit command."); } UIEditorHostCommandDispatchResult InspectorPanel::DispatchEditCommand( std::string_view commandId) { const UIEditorHostCommandEvaluationResult evaluation = EvaluateEditCommand(commandId); if (!evaluation.executable) { return BuildDispatchResult(false, evaluation.message); } const InspectorPresentationComponentBinding* binding = FindSelectedComponentBinding(); if (binding == nullptr || m_sceneRuntime == nullptr) { return BuildDispatchResult( false, "Inspector component route is unavailable."); } if (commandId == "edit.delete") { if (!m_sceneRuntime->RemoveSelectedComponent(binding->componentId)) { return BuildDispatchResult( false, "Failed to remove inspector component."); } ResetInteractionState(); return BuildDispatchResult( true, "Removed inspector component '" + binding->displayName + "'."); } return BuildDispatchResult( false, "Inspector does not expose this edit command."); } } // namespace XCEngine::UI::Editor::App