#include "InspectorPanel.h" #include "Composition/EditorContext.h" #include "Composition/EditorPanelIds.h" #include "State/EditorColorPickerToolState.h" #include #include #include #include "Features/Inspector/Components/IInspectorComponentEditor.h" #include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" #include "Scene/EditorSceneRuntime.h" #include "State/EditorCommandFocusService.h" #include #include namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; 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); float ResolveInspectorHorizontalPadding( const InspectorPresentationModel& presentation) { return presentation.showHeader ? kPanelPadding : 0.0f; } float ResolveInspectorTopPadding( const InspectorPresentationModel& presentation) { return presentation.showHeader ? kPanelPadding : 0.0f; } float ResolveInspectorBottomPadding( const InspectorPresentationModel& presentation) { return presentation.showHeader ? kPanelPadding : 0.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; } bool AreColorsEqual(const UIColor& lhs, const UIColor& rhs) { return lhs.r == rhs.r && lhs.g == rhs.g && lhs.b == rhs.b && lhs.a == rhs.a; } float ResolveTextTop(float rectY, float rectHeight, float fontSize) { const float lineHeight = fontSize * 1.6f; return rectY + std::floor((rectHeight - lineHeight) * 0.5f); } 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::SetCommandFocusService( EditorCommandFocusService* commandFocusService) { m_commandFocusService = commandFocusService; } void InspectorPanel::ResetPanelState() { m_visible = false; m_bounds = {}; m_sceneRuntime = nullptr; m_subject = {}; m_subjectKey.clear(); m_presentation = {}; m_gridFrame = {}; m_knownSectionIds.clear(); m_lastSceneSelectionStamp = 0u; m_lastProjectSelectionStamp = 0u; m_lastSceneInspectorRevision = 0u; ResetInteractionState(); } void InspectorPanel::ResetInteractionState() { m_fieldSelection.ClearSelection(); m_sectionExpansion.Clear(); m_propertyEditModel = {}; m_interactionState = {}; m_gridFrame = {}; m_lastAppliedColorPickerRevision = 0u; } 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.editableFieldSession = {}; } 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"; } } float InspectorPanel::ResolveHeaderHeight() const { if (!m_presentation.showHeader) { return 0.0f; } return kTitleHeight + kSubtitleHeight + kHeaderGap; } UIRect InspectorPanel::BuildGridBounds() const { const float horizontalPadding = ResolveInspectorHorizontalPadding(m_presentation); const float topPadding = ResolveInspectorTopPadding(m_presentation); const float bottomPadding = ResolveInspectorBottomPadding(m_presentation); const float x = m_bounds.x + horizontalPadding; const float width = (std::max)(m_bounds.width - horizontalPadding * 2.0f, 0.0f); const float y = m_bounds.y + topPadding + ResolveHeaderHeight(); const float height = (std::max)(m_bounds.y + m_bounds.height - bottomPadding - y, 0.0f); return UIRect(x, y, width, height); } void InspectorPanel::ClaimCommandFocus( const std::vector& inputEvents, bool allowInteraction) { if (m_commandFocusService == nullptr) { return; } for (const UIInputEvent& event : inputEvents) { if (event.type == UIInputEventType::FocusGained) { m_commandFocusService->ClaimFocus(EditorActionRoute::Inspector); return; } if (!allowInteraction || event.type != UIInputEventType::PointerButtonDown || !ContainsPoint(m_bounds, event.position)) { continue; } m_commandFocusService->ClaimFocus(EditorActionRoute::Inspector); return; } } 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; } const Widgets::UIEditorPropertyGridField* InspectorPanel::FindField( std::string_view fieldId) const { 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 nullptr; } return &m_presentation.sections[location.sectionIndex].fields[location.fieldIndex]; } Widgets::UIEditorPropertyGridField* InspectorPanel::FindMutableField( std::string_view fieldId) { 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 nullptr; } return &m_presentation.sections[location.sectionIndex].fields[location.fieldIndex]; } void InspectorPanel::CapturePresentationStamps(const EditorContext& context) { m_lastSceneSelectionStamp = context.GetSceneRuntime().GetSelectionStamp(); m_lastProjectSelectionStamp = context.GetProjectRuntime().GetSelectionStamp(); m_lastSceneInspectorRevision = context.GetSceneRuntime().GetInspectorRevision(); } void InspectorPanel::RebuildPresentation( EditorContext& context, bool subjectChanged) { m_presentation = BuildInspectorPresentationModel( m_subject, context.GetSceneRuntime(), InspectorComponentEditorRegistry::Get()); CapturePresentationStamps(context); SyncExpansionState(subjectChanged); SyncSelectionState(); } void InspectorPanel::ForceResyncPresentation(EditorContext& context) { if (m_subject.kind != InspectorSubjectKind::SceneObject) { RebuildPresentation(context, false); return; } const std::string structureSignature = BuildInspectorStructureSignature( m_subject, context.GetSceneRuntime(), InspectorComponentEditorRegistry::Get()); if (structureSignature != m_presentation.structureSignature) { RebuildPresentation(context, false); return; } const InspectorPresentationSyncResult syncResult = SyncInspectorPresentationModelValues( m_presentation, m_subject, context.GetSceneRuntime(), InspectorComponentEditorRegistry::Get()); if (!syncResult.success) { RebuildPresentation(context, false); return; } m_presentation.structureSignature = structureSignature; CapturePresentationStamps(context); SyncSelectionState(); } void InspectorPanel::RefreshPresentation( EditorContext& context, bool subjectChanged) { if (subjectChanged) { RebuildPresentation(context, true); return; } switch (m_subject.kind) { case InspectorSubjectKind::ProjectAsset: if (m_lastProjectSelectionStamp != context.GetProjectRuntime().GetSelectionStamp()) { RebuildPresentation(context, false); } return; case InspectorSubjectKind::SceneObject: { const EditorSceneRuntime& sceneRuntime = context.GetSceneRuntime(); if (m_lastSceneSelectionStamp != sceneRuntime.GetSelectionStamp()) { RebuildPresentation(context, false); return; } if (m_lastSceneInspectorRevision == sceneRuntime.GetInspectorRevision()) { return; } const std::string structureSignature = BuildInspectorStructureSignature( m_subject, sceneRuntime, InspectorComponentEditorRegistry::Get()); if (structureSignature != m_presentation.structureSignature) { RebuildPresentation(context, false); return; } const InspectorPresentationSyncResult syncResult = SyncInspectorPresentationModelValues( m_presentation, m_subject, sceneRuntime, InspectorComponentEditorRegistry::Get()); if (!syncResult.success) { RebuildPresentation(context, false); return; } m_presentation.structureSignature = structureSignature; CapturePresentationStamps(context); SyncSelectionState(); return; } case InspectorSubjectKind::None: default: return; } } bool InspectorPanel::ApplyColorPickerToolValue(EditorContext& context) { EditorColorPickerToolState& toolState = context.GetColorPickerToolState(); if (m_sceneRuntime == nullptr || !toolState.active || toolState.revision == m_lastAppliedColorPickerRevision || !IsEditorColorPickerToolTarget( toolState, m_subjectKey, toolState.inspectorTarget.fieldId)) { return false; } Widgets::UIEditorPropertyGridField* field = FindMutableField(toolState.inspectorTarget.fieldId); if (field == nullptr || field->kind != Widgets::UIEditorPropertyGridFieldKind::Color) { return false; } if (AreColorsEqual(field->colorValue.value, toolState.color)) { m_lastAppliedColorPickerRevision = toolState.revision; return false; } field->colorValue.value = toolState.color; const bool applied = ApplyChangedField(field->fieldId); m_lastAppliedColorPickerRevision = toolState.revision; if (!applied) { ForceResyncPresentation(context); return false; } RefreshPresentation(context, false); return true; } void InspectorPanel::RequestColorPicker( EditorContext& context, std::string_view fieldId) { const Widgets::UIEditorPropertyGridField* field = FindField(fieldId); if (field == nullptr || field->kind != Widgets::UIEditorPropertyGridFieldKind::Color || field->readOnly) { return; } OpenEditorColorPickerToolForInspectorField( context.GetColorPickerToolState(), m_subjectKey, field->fieldId, field->colorValue.value, field->colorValue.showAlpha); } 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( EditorContext& context, const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector& inputEvents, const PanelInputContext& inputContext) { const UIEditorPanelContentHostPanelState* panelState = FindMountedInspectorPanel(contentHostFrame); if (panelState == nullptr) { ResetPanelState(); return; } m_visible = true; m_bounds = panelState->bounds; m_sceneRuntime = &context.GetSceneRuntime(); m_subject = BuildInspectorSubject(context.GetSession(), context.GetSceneRuntime()); const std::string nextSubjectKey = BuildSubjectKey(); const bool subjectChanged = m_subjectKey != nextSubjectKey; m_subjectKey = nextSubjectKey; if (subjectChanged) { ResetInteractionState(); } RefreshPresentation(context, subjectChanged); ApplyColorPickerToolValue(context); if (m_presentation.sections.empty()) { m_gridFrame = {}; return; } const std::vector filteredEvents = BuildUIEditorPanelInputEvents( m_bounds, inputEvents, UIEditorPanelInputFilterOptions{ .allowPointerInBounds = inputContext.allowInteraction, .allowPointerWhileCaptured = false, .allowKeyboardInput = inputContext.hasInputFocus, .allowFocusEvents = inputContext.hasInputFocus || inputContext.focusGained || inputContext.focusLost, .includePointerLeave = inputContext.allowInteraction || inputContext.hasInputFocus }, inputContext.focusGained, inputContext.focusLost); ClaimCommandFocus(filteredEvents, inputContext.allowInteraction); m_gridFrame = UpdateUIEditorPropertyGridInteraction( m_interactionState, m_fieldSelection, m_sectionExpansion, m_propertyEditModel, BuildGridBounds(), m_presentation.sections, filteredEvents, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); if (m_gridFrame.result.pickerRequested && !m_gridFrame.result.requestedFieldId.empty()) { RequestColorPicker(context, m_gridFrame.result.requestedFieldId); } if (m_gridFrame.result.fieldValueChanged && !m_gridFrame.result.changedFieldId.empty()) { if (ApplyChangedField(m_gridFrame.result.changedFieldId)) { RefreshPresentation(context, false); } else { ForceResyncPresentation(context); } } } 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 horizontalPadding = ResolveInspectorHorizontalPadding(m_presentation); const float topPadding = ResolveInspectorTopPadding(m_presentation); const float contentX = m_bounds.x + horizontalPadding; const float contentWidth = (std::max)(m_bounds.width - horizontalPadding * 2.0f, 0.0f); float nextY = m_bounds.y + topPadding; if (m_presentation.showHeader) { 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