Files
XCEngine/new_editor/app/Features/Inspector/InspectorPanel.cpp

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