#include "InspectorPanel.h" #include "Composition/EditorContext.h" #include "State/EditorColorPickerToolState.h" #include #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::UIPointerButton; using ::XCEngine::UI::UIRect; using ::XCEngine::UI::UIInputEventType; 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 float kAddComponentButtonHeight = 24.0f; constexpr float kAddComponentButtonTopGap = 10.0f; constexpr float kAddComponentButtonFontSize = 12.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); void OffsetRectY(UIRect& rect, float deltaY) { rect.y += deltaY; } bool ShouldOffsetPointerEventPosition(UIInputEventType type) { switch (type) { case UIInputEventType::PointerMove: case UIInputEventType::PointerEnter: case UIInputEventType::PointerLeave: case UIInputEventType::PointerButtonDown: case UIInputEventType::PointerButtonUp: case UIInputEventType::PointerWheel: return true; default: return false; } } std::vector BuildScrolledPropertyGridInputEvents( const std::vector& inputEvents, float verticalOffset) { std::vector adjustedEvents = inputEvents; for (UIInputEvent& event : adjustedEvents) { if (ShouldOffsetPointerEventPosition(event.type)) { event.position.y += verticalOffset; } } return adjustedEvents; } Widgets::UIEditorPropertyGridLayout TranslatePropertyGridLayoutForScroll( const Widgets::UIEditorPropertyGridLayout& layout, float verticalOffset) { Widgets::UIEditorPropertyGridLayout translated = layout; if (verticalOffset == 0.0f) { return translated; } const float deltaY = -verticalOffset; for (UIRect& rect : translated.sectionHeaderRects) { OffsetRectY(rect, deltaY); } for (UIRect& rect : translated.sectionDisclosureRects) { OffsetRectY(rect, deltaY); } for (UIRect& rect : translated.sectionTitleRects) { OffsetRectY(rect, deltaY); } for (UIRect& rect : translated.fieldRowRects) { OffsetRectY(rect, deltaY); } for (UIRect& rect : translated.fieldLabelRects) { OffsetRectY(rect, deltaY); } for (UIRect& rect : translated.fieldValueRects) { OffsetRectY(rect, deltaY); } return translated; } float ResolvePropertyGridContentBottom( const Widgets::UIEditorPropertyGridLayout& gridLayout, const UIRect& contentBounds) { float contentBottom = contentBounds.y; if (!gridLayout.sectionHeaderRects.empty()) { const UIRect& lastSectionRect = gridLayout.sectionHeaderRects.back(); contentBottom = (std::max)(contentBottom, lastSectionRect.y + lastSectionRect.height); } if (!gridLayout.fieldRowRects.empty()) { const UIRect& lastFieldRect = gridLayout.fieldRowRects.back(); contentBottom = (std::max)(contentBottom, lastFieldRect.y + lastFieldRect.height); } return contentBottom; } float ResolveAddComponentButtonTop( const Widgets::UIEditorPropertyGridLayout& gridLayout, const UIRect& contentBounds) { return ResolvePropertyGridContentBottom(gridLayout, contentBounds) + kAddComponentButtonTopGap; } Widgets::UIEditorScrollViewPalette BuildInspectorScrollPalette() { Widgets::UIEditorScrollViewPalette palette = ::XCEngine::UI::Editor::ResolveUIEditorScrollViewPalette(); palette.surfaceColor.a = 0.0f; palette.borderColor.a = 0.0f; palette.focusedBorderColor.a = 0.0f; return palette; } 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); } float ResolveCenteredTextX( const UIRect& rect, std::string_view text, float fontSize) { const float estimatedTextWidth = static_cast(text.size()) * fontSize * 0.56f; return rect.x + std::floor((std::max)(rect.width - estimatedTextWidth, 0.0f) * 0.5f); } UIColor ResolveAddComponentButtonFillColor(bool hovered, bool pressed) { const Widgets::UIEditorPropertyGridPalette& palette = ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(); if (pressed) { return palette.valueBoxEditingColor; } if (hovered) { return palette.valueBoxHoverColor; } return palette.valueBoxColor; } 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 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_scrollInteractionState = {}; m_scrollFrame = {}; m_scrollVerticalOffset = 0.0f; m_interactionState = {}; m_gridFrame = {}; m_lastAppliedColorPickerRevision = 0u; ResetAddComponentButtonState(); } bool InspectorPanel::HasActivePointerCapture() const { return HasActiveUIEditorScrollViewPointerCapture(m_scrollInteractionState); } 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; } bool InspectorPanel::ShouldShowAddComponentButton() const { return m_subject.kind == InspectorSubjectKind::SceneObject; } UIRect InspectorPanel::BuildScrollViewportBounds() 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); } UIRect InspectorPanel::BuildAddComponentButtonRect( const Widgets::UIEditorPropertyGridLayout& gridLayout, const UIRect& contentBounds) const { if (!ShouldShowAddComponentButton()) { return {}; } return UIRect( contentBounds.x, ResolveAddComponentButtonTop(gridLayout, contentBounds), contentBounds.width, kAddComponentButtonHeight); } float InspectorPanel::MeasureScrollableContentHeight( const UIRect& contentBounds) const { float contentBottom = contentBounds.y; Widgets::UIEditorPropertyGridLayout layout = {}; if (!m_presentation.sections.empty()) { layout = Widgets::BuildUIEditorPropertyGridLayout( contentBounds, m_presentation.sections, m_sectionExpansion, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); contentBottom = ResolvePropertyGridContentBottom(layout, contentBounds); } if (ShouldShowAddComponentButton()) { const UIRect buttonRect = BuildAddComponentButtonRect(layout, contentBounds); contentBottom = (std::max)(contentBottom, buttonRect.y + buttonRect.height); } return (std::max)(contentBottom - contentBounds.y, 0.0f); } void InspectorPanel::RebuildScrollableLayout() { m_scrollFrame = {}; m_gridFrame.layout = {}; const UIRect viewportBounds = BuildScrollViewportBounds(); if (viewportBounds.width <= 0.0f || viewportBounds.height <= 0.0f) { m_scrollVerticalOffset = 0.0f; return; } const Widgets::UIEditorScrollViewMetrics& scrollMetrics = ::XCEngine::UI::Editor::ResolveUIEditorScrollViewMetrics(); const float contentHeight = MeasureScrollableContentHeight(viewportBounds); m_scrollVerticalOffset = Widgets::ClampUIEditorScrollViewOffset( viewportBounds, contentHeight, m_scrollVerticalOffset, scrollMetrics); m_scrollFrame.layout = Widgets::BuildUIEditorScrollViewLayout( viewportBounds, contentHeight, m_scrollVerticalOffset, scrollMetrics); m_scrollFrame.result.verticalOffset = m_scrollVerticalOffset; if (!m_presentation.sections.empty()) { const Widgets::UIEditorPropertyGridLayout unscrolledLayout = Widgets::BuildUIEditorPropertyGridLayout( m_scrollFrame.layout.contentRect, m_presentation.sections, m_sectionExpansion, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); m_gridFrame.layout = TranslatePropertyGridLayoutForScroll(unscrolledLayout, m_scrollVerticalOffset); } } 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); context.RequestOpenUtilityWindow(EditorUtilityWindowKind::ColorPicker); } void InspectorPanel::ResetAddComponentButtonState() { m_addComponentButtonHovered = false; m_addComponentButtonPressed = false; } void InspectorPanel::UpdateAddComponentButton( EditorContext& context, const std::vector& inputEvents) { if (!ShouldShowAddComponentButton()) { ResetAddComponentButtonState(); return; } const UIRect buttonRect = BuildAddComponentButtonRect(m_gridFrame.layout, m_scrollFrame.layout.contentRect); if (buttonRect.width <= 0.0f || buttonRect.height <= 0.0f) { ResetAddComponentButtonState(); return; } for (const UIInputEvent& event : inputEvents) { switch (event.type) { case UIInputEventType::PointerMove: m_addComponentButtonHovered = ContainsPoint(buttonRect, event.position); break; case UIInputEventType::PointerLeave: case UIInputEventType::FocusLost: ResetAddComponentButtonState(); break; case UIInputEventType::PointerButtonDown: if (event.pointerButton != UIPointerButton::Left) { break; } m_addComponentButtonHovered = ContainsPoint(buttonRect, event.position); m_addComponentButtonPressed = m_addComponentButtonHovered; break; case UIInputEventType::PointerButtonUp: if (event.pointerButton != UIPointerButton::Left) { break; } if (m_addComponentButtonPressed && ContainsPoint(buttonRect, event.position)) { context.RequestOpenUtilityWindow(EditorUtilityWindowKind::AddComponent); } m_addComponentButtonHovered = ContainsPoint(buttonRect, event.position); m_addComponentButtonPressed = false; break; default: break; } } } 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 UIEditorHostedPanelDispatchEntry& dispatchEntry, const std::vector& inputEvents) { if (!dispatchEntry.mounted) { ResetPanelState(); return; } m_visible = true; m_bounds = dispatchEntry.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); const std::vector filteredEvents = BuildUIEditorPanelInputEvents( m_bounds, inputEvents, UIEditorPanelInputFilterOptions{ .allowPointerInBounds = dispatchEntry.allowInteraction, .allowPointerWhileCaptured = HasActivePointerCapture(), .allowKeyboardInput = dispatchEntry.focused, .allowFocusEvents = dispatchEntry.focused || HasActivePointerCapture() || dispatchEntry.focusGained || dispatchEntry.focusLost, .includePointerLeave = dispatchEntry.allowInteraction || dispatchEntry.focused || HasActivePointerCapture() }, dispatchEntry.focusGained, dispatchEntry.focusLost); TryClaimHostedPanelCommandFocus( m_commandFocusService, EditorActionRoute::Inspector, filteredEvents, m_bounds, dispatchEntry.allowInteraction); RebuildScrollableLayout(); const UIRect scrollViewportBounds = BuildScrollViewportBounds(); if (scrollViewportBounds.width > 0.0f && scrollViewportBounds.height > 0.0f) { m_scrollInteractionState.scrollViewState.focused = m_interactionState.propertyGridState.focused; m_scrollFrame = UpdateUIEditorScrollViewInteraction( m_scrollInteractionState, m_scrollVerticalOffset, scrollViewportBounds, MeasureScrollableContentHeight(scrollViewportBounds), filteredEvents, ::XCEngine::UI::Editor::ResolveUIEditorScrollViewMetrics()); if (m_scrollFrame.result.focusChanged) { m_interactionState.propertyGridState.focused = m_scrollInteractionState.scrollViewState.focused; } } else { m_scrollFrame = {}; m_scrollVerticalOffset = 0.0f; } m_gridFrame.result = {}; if (!m_presentation.sections.empty() && m_scrollFrame.layout.contentRect.width > 0.0f && m_scrollFrame.layout.contentRect.height > 0.0f) { const std::vector gridEvents = BuildScrolledPropertyGridInputEvents(filteredEvents, m_scrollVerticalOffset); const UIEditorPropertyGridInteractionFrame interactionFrame = UpdateUIEditorPropertyGridInteraction( m_interactionState, m_fieldSelection, m_sectionExpansion, m_propertyEditModel, m_scrollFrame.layout.contentRect, m_presentation.sections, gridEvents, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); m_gridFrame.result = interactionFrame.result; if (interactionFrame.result.pickerRequested && !interactionFrame.result.requestedFieldId.empty()) { RequestColorPicker(context, interactionFrame.result.requestedFieldId); } if (interactionFrame.result.fieldValueChanged && !interactionFrame.result.changedFieldId.empty()) { if (ApplyChangedField(interactionFrame.result.changedFieldId)) { RefreshPresentation(context, false); } else { ForceResyncPresentation(context); } } } RebuildScrollableLayout(); UpdateAddComponentButton(context, filteredEvents); } 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_scrollFrame.layout.bounds.width <= 0.0f || m_scrollFrame.layout.bounds.height <= 0.0f) { return; } Widgets::AppendUIEditorScrollViewBackground( drawList, m_scrollFrame.layout, m_scrollInteractionState.scrollViewState, BuildInspectorScrollPalette(), ::XCEngine::UI::Editor::ResolveUIEditorScrollViewMetrics()); drawList.PushClipRect(m_scrollFrame.layout.contentRect); if (!m_presentation.sections.empty()) { Widgets::AppendUIEditorPropertyGridBackground( drawList, m_gridFrame.layout, m_presentation.sections, m_fieldSelection, m_propertyEditModel, m_interactionState.propertyGridState, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(), ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); Widgets::AppendUIEditorPropertyGridForeground( drawList, m_gridFrame.layout, m_presentation.sections, m_fieldSelection, m_interactionState.propertyGridState, m_propertyEditModel, ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(), ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics()); } if (ShouldShowAddComponentButton()) { const UIRect buttonRect = BuildAddComponentButtonRect(m_gridFrame.layout, m_scrollFrame.layout.contentRect); if (buttonRect.width > 0.0f && buttonRect.height > 0.0f) { const Widgets::UIEditorPropertyGridPalette& palette = ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(); const Widgets::UIEditorPropertyGridMetrics& metrics = ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics(); drawList.AddFilledRect( buttonRect, ResolveAddComponentButtonFillColor( m_addComponentButtonHovered, m_addComponentButtonPressed), metrics.valueBoxRounding); drawList.AddRectOutline( buttonRect, palette.valueBoxBorderColor, metrics.borderThickness, metrics.valueBoxRounding); drawList.AddText( UIPoint( ResolveCenteredTextX(buttonRect, "Add Component", kAddComponentButtonFontSize), ResolveTextTop(buttonRect.y, buttonRect.height, kAddComponentButtonFontSize)), "Add Component", palette.valueTextColor, kAddComponentButtonFontSize); } } drawList.PopClipRect(); } 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