305 lines
9.6 KiB
C++
305 lines
9.6 KiB
C++
|
|
#include "Features/Inspector/AddComponentPanel.h"
|
||
|
|
|
||
|
|
#include "Composition/EditorContext.h"
|
||
|
|
#include "Features/Inspector/Components/IInspectorComponentEditor.h"
|
||
|
|
#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h"
|
||
|
|
|
||
|
|
#include <XCEditor/Fields/UIEditorFieldStyle.h>
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <cmath>
|
||
|
|
#include <string_view>
|
||
|
|
|
||
|
|
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<std::size_t>(-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<UIInputEvent>& 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
|