#include "Features/Inspector/AddComponentPanel.h" #include "Composition/EditorContext.h" #include "Features/Inspector/Components/IInspectorComponentEditor.h" #include "Features/Inspector/Components/InspectorComponentEditorRegistry.h" #include #include #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::UIPointerButton; using ::XCEngine::UI::UIRect; constexpr float kPanelPadding = 8.0f; constexpr float kHeaderGap = 8.0f; constexpr float kCaptionHeight = 16.0f; constexpr float kTargetHeight = 16.0f; constexpr float kCaptionFontSize = 11.0f; constexpr float kTargetFontSize = 12.0f; constexpr float kEntryHeight = 30.0f; constexpr float kEntryGap = 4.0f; constexpr float kEntryTextPadding = 8.0f; constexpr float kReasonFontSize = 10.0f; constexpr std::size_t kInvalidEntryIndex = static_cast(-1); 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); } UIColor ResolveEntryFillColor( bool enabled, bool hovered, bool pressed, const Widgets::UIEditorPropertyGridPalette& palette) { if (!enabled) { return palette.valueBoxReadOnlyColor; } if (pressed) { return palette.valueBoxEditingColor; } if (hovered) { return palette.valueBoxHoverColor; } return palette.valueBoxColor; } } // namespace void AddComponentPanel::ResetPanelState() { m_visible = false; m_hasTarget = false; m_bounds = {}; m_targetDisplayName.clear(); m_entries.clear(); ResetInteractionState(); } void AddComponentPanel::ResetInteractionState() { m_hoveredEntryIndex = kInvalidEntryIndex; m_pressedEntryIndex = kInvalidEntryIndex; } void AddComponentPanel::RebuildEntries( const ::XCEngine::Components::GameObject* gameObject) { const auto& editors = InspectorComponentEditorRegistry::Get().GetEditors(); m_entries.clear(); m_entries.reserve(editors.size()); const float entryX = m_bounds.x + kPanelPadding; const float entryWidth = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f); float nextY = m_bounds.y + kPanelPadding + kCaptionHeight + kTargetHeight + kHeaderGap; for (const auto& editorPtr : editors) { const IInspectorComponentEditor* editor = editorPtr.get(); if (editor == nullptr || !editor->ShowInAddComponentMenu()) { continue; } EntryPresentation entry = {}; entry.componentTypeName = std::string(editor->GetComponentTypeName()); entry.displayName = std::string(editor->GetDisplayName()); entry.enabled = editor->CanAddTo(gameObject); entry.disabledReason = std::string(editor->GetAddDisabledReason(gameObject)); entry.rect = UIRect(entryX, nextY, entryWidth, kEntryHeight); m_entries.push_back(std::move(entry)); nextY += kEntryHeight + kEntryGap; } if (m_hoveredEntryIndex >= m_entries.size()) { m_hoveredEntryIndex = kInvalidEntryIndex; } if (m_pressedEntryIndex >= m_entries.size()) { m_pressedEntryIndex = kInvalidEntryIndex; } } bool AddComponentPanel::TryActivateEntry( EditorContext& context, std::size_t entryIndex) { if (entryIndex >= m_entries.size()) { return false; } const EntryPresentation& entry = m_entries[entryIndex]; if (!entry.enabled) { return false; } if (!context.GetSceneRuntime().AddComponentToSelectedGameObject(entry.componentTypeName)) { return false; } m_hasTarget = context.GetSceneRuntime().GetSelectedGameObject() != nullptr; m_targetDisplayName = context.GetSceneRuntime().GetSelectedDisplayName(); RebuildEntries(context.GetSceneRuntime().GetSelectedGameObject()); return true; } std::size_t AddComponentPanel::HitTestEntry(const UIPoint& point) const { for (std::size_t index = 0u; index < m_entries.size(); ++index) { if (ContainsPoint(m_entries[index].rect, point)) { return index; } } return kInvalidEntryIndex; } void AddComponentPanel::Update( EditorContext& context, const AddComponentPanelHostContext& hostContext, const std::vector& inputEvents) { if (!hostContext.mounted) { ResetPanelState(); return; } m_visible = true; m_bounds = hostContext.bounds; m_hasTarget = context.GetSceneRuntime().GetSelectedGameObject() != nullptr; m_targetDisplayName = context.GetSceneRuntime().GetSelectedDisplayName(); RebuildEntries(context.GetSceneRuntime().GetSelectedGameObject()); if (hostContext.focusLost) { ResetInteractionState(); } for (const UIInputEvent& event : inputEvents) { switch (event.type) { case UIInputEventType::PointerMove: m_hoveredEntryIndex = HitTestEntry(event.position); break; case UIInputEventType::PointerLeave: case UIInputEventType::FocusLost: ResetInteractionState(); break; case UIInputEventType::PointerButtonDown: if (event.pointerButton != UIPointerButton::Left) { break; } m_hoveredEntryIndex = HitTestEntry(event.position); m_pressedEntryIndex = m_hoveredEntryIndex < m_entries.size() && m_entries[m_hoveredEntryIndex].enabled ? m_hoveredEntryIndex : kInvalidEntryIndex; break; case UIInputEventType::PointerButtonUp: if (event.pointerButton != UIPointerButton::Left) { break; } m_hoveredEntryIndex = HitTestEntry(event.position); if (m_pressedEntryIndex != kInvalidEntryIndex && m_pressedEntryIndex == m_hoveredEntryIndex) { TryActivateEntry(context, m_pressedEntryIndex); } m_pressedEntryIndex = kInvalidEntryIndex; break; default: break; } } } void AddComponentPanel::Append(UIDrawList& drawList) const { if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) { return; } const Widgets::UIEditorPropertyGridPalette& palette = ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridPalette(); const Widgets::UIEditorPropertyGridMetrics& metrics = ::XCEngine::UI::Editor::GetUIEditorFixedPropertyGridMetrics(); drawList.AddFilledRect(m_bounds, palette.surfaceColor); drawList.AddText( UIPoint( m_bounds.x + kPanelPadding, ResolveTextTop(m_bounds.y + kPanelPadding, kCaptionHeight, kCaptionFontSize)), "Target", palette.labelTextColor, kCaptionFontSize); const std::string targetText = m_hasTarget ? m_targetDisplayName : std::string("No scene object selected."); drawList.AddText( UIPoint( m_bounds.x + kPanelPadding, ResolveTextTop( m_bounds.y + kPanelPadding + kCaptionHeight, kTargetHeight, kTargetFontSize)), targetText, m_hasTarget ? palette.valueTextColor : palette.readOnlyValueTextColor, kTargetFontSize); if (m_entries.empty()) { drawList.AddText( UIPoint( m_bounds.x + kPanelPadding, ResolveTextTop( m_bounds.y + kPanelPadding + kCaptionHeight + kTargetHeight + kHeaderGap, kTargetHeight, kTargetFontSize)), "No registered component editors.", palette.readOnlyValueTextColor, kTargetFontSize); return; } drawList.PushClipRect(m_bounds); for (std::size_t index = 0u; index < m_entries.size(); ++index) { const EntryPresentation& entry = m_entries[index]; const bool hovered = index == m_hoveredEntryIndex; const bool pressed = index == m_pressedEntryIndex; drawList.AddFilledRect( entry.rect, ResolveEntryFillColor(entry.enabled, hovered, pressed, palette), metrics.valueBoxRounding); drawList.AddRectOutline( entry.rect, palette.valueBoxBorderColor, metrics.borderThickness, metrics.valueBoxRounding); const UIColor nameColor = entry.enabled ? palette.valueTextColor : palette.readOnlyValueTextColor; const float nameY = entry.disabledReason.empty() ? ResolveTextTop(entry.rect.y, entry.rect.height, kTargetFontSize) : entry.rect.y + 2.0f; drawList.AddText( UIPoint(entry.rect.x + kEntryTextPadding, nameY), entry.displayName, nameColor, kTargetFontSize); if (!entry.disabledReason.empty()) { drawList.AddText( UIPoint( entry.rect.x + kEntryTextPadding, entry.rect.y + entry.rect.height - 14.0f), entry.disabledReason, palette.labelTextColor, kReasonFontSize); } } drawList.PopClipRect(); } } // namespace XCEngine::UI::Editor::App