454 lines
14 KiB
C++
454 lines
14 KiB
C++
#include "InspectorPanel.h"
|
|
|
|
#include <XCEditor/App/EditorPanelIds.h>
|
|
#include <XCEditor/Fields/UIEditorFieldStyle.h>
|
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
|
|
|
#include "Features/Inspector/Components/IInspectorComponentEditor.h"
|
|
#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h"
|
|
#include "Scene/EditorSceneRuntime.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
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<UIInputEvent> FilterInspectorInputEvents(
|
|
const UIRect& bounds,
|
|
const std::vector<UIInputEvent>& inputEvents,
|
|
bool allowInteraction,
|
|
bool panelActive) {
|
|
if (!allowInteraction && !panelActive) {
|
|
return {};
|
|
}
|
|
|
|
std::vector<UIInputEvent> 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<std::string> 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<UIInputEvent>& 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<UIInputEvent> 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
|