2026-04-19 00:03:25 +08:00
|
|
|
#include "Features/Inspector/InspectorPresentationModel.h"
|
|
|
|
|
|
|
|
|
|
#include "Features/Inspector/Components/IInspectorComponentEditor.h"
|
2026-04-21 00:57:14 +08:00
|
|
|
#include "Features/Inspector/Components/InspectorComponentEditorUtils.h"
|
2026-04-19 00:03:25 +08:00
|
|
|
#include "Features/Inspector/Components/InspectorComponentEditorRegistry.h"
|
|
|
|
|
#include "Scene/EditorSceneRuntime.h"
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/Components/GameObject.h>
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
#include <algorithm>
|
2026-04-19 00:03:25 +08:00
|
|
|
#include <filesystem>
|
|
|
|
|
|
|
|
|
|
namespace XCEngine::UI::Editor::App {
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
using ::XCEngine::Components::GameObject;
|
|
|
|
|
using Widgets::UIEditorPropertyGridField;
|
|
|
|
|
using Widgets::UIEditorPropertyGridFieldKind;
|
|
|
|
|
using Widgets::UIEditorPropertyGridSection;
|
|
|
|
|
|
|
|
|
|
std::string PathToUtf8String(const std::filesystem::path& path) {
|
|
|
|
|
const std::u8string value = path.u8string();
|
|
|
|
|
return std::string(value.begin(), value.end());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string ResolveSceneObjectDisplayName(
|
|
|
|
|
const InspectorSceneObjectSubject& sceneObject) {
|
|
|
|
|
return sceneObject.displayName.empty()
|
|
|
|
|
? std::string("GameObject")
|
|
|
|
|
: sceneObject.displayName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string ResolveProjectSelectionTitle(
|
|
|
|
|
const EditorSelectionState& selection) {
|
|
|
|
|
if (!selection.displayName.empty()) {
|
|
|
|
|
return selection.displayName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return selection.directory
|
|
|
|
|
? std::string("Folder")
|
|
|
|
|
: std::string("Asset");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
std::string ResolveNoneSelectionTitle() {
|
|
|
|
|
return "Nothing selected";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string ResolveNoneSelectionSubtitle() {
|
|
|
|
|
return "Select a hierarchy item or project asset.";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
UIEditorPropertyGridField BuildReadOnlyTextField(
|
|
|
|
|
std::string fieldId,
|
|
|
|
|
std::string label,
|
|
|
|
|
std::string value) {
|
|
|
|
|
UIEditorPropertyGridField field = {};
|
|
|
|
|
field.fieldId = std::move(fieldId);
|
|
|
|
|
field.label = std::move(label);
|
|
|
|
|
field.kind = UIEditorPropertyGridFieldKind::Text;
|
|
|
|
|
field.valueText = std::move(value);
|
|
|
|
|
field.readOnly = true;
|
|
|
|
|
return field;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppendReadOnlySection(
|
|
|
|
|
InspectorPresentationModel& model,
|
|
|
|
|
std::string sectionId,
|
|
|
|
|
std::string title,
|
|
|
|
|
std::vector<UIEditorPropertyGridField> fields) {
|
|
|
|
|
UIEditorPropertyGridSection section = {};
|
|
|
|
|
section.sectionId = std::move(sectionId);
|
|
|
|
|
section.title = std::move(title);
|
|
|
|
|
section.fields = std::move(fields);
|
|
|
|
|
model.sections.push_back(std::move(section));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
const EditorSceneComponentDescriptor* FindComponentDescriptor(
|
|
|
|
|
const std::vector<EditorSceneComponentDescriptor>& descriptors,
|
|
|
|
|
std::string_view componentId) {
|
|
|
|
|
const auto it = std::find_if(
|
|
|
|
|
descriptors.begin(),
|
|
|
|
|
descriptors.end(),
|
|
|
|
|
[componentId](const EditorSceneComponentDescriptor& descriptor) {
|
|
|
|
|
return descriptor.componentId == componentId;
|
|
|
|
|
});
|
|
|
|
|
return it != descriptors.end() ? &(*it) : nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InspectorComponentEditorContext BuildComponentEditorContext(
|
|
|
|
|
const EditorSceneComponentDescriptor& descriptor,
|
|
|
|
|
const GameObject* gameObject,
|
|
|
|
|
const IInspectorComponentEditor* editor) {
|
|
|
|
|
InspectorComponentEditorContext context = {};
|
|
|
|
|
context.gameObject = gameObject;
|
|
|
|
|
context.component = descriptor.component;
|
|
|
|
|
context.componentId = descriptor.componentId;
|
|
|
|
|
context.typeName = descriptor.typeName;
|
|
|
|
|
context.displayName = editor != nullptr
|
|
|
|
|
? std::string(editor->GetDisplayName())
|
|
|
|
|
: descriptor.typeName;
|
|
|
|
|
context.removable = descriptor.removable;
|
|
|
|
|
return context;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppendProjectStructureSignature(
|
|
|
|
|
const EditorSelectionState& selection,
|
|
|
|
|
std::string& signature) {
|
|
|
|
|
AppendInspectorStructureToken(signature, "project");
|
|
|
|
|
AppendInspectorStructureToken(signature, selection.directory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppendNoneSelectionStructureSignature(std::string& signature) {
|
|
|
|
|
AppendInspectorStructureToken(signature, "none");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
void AppendFallbackComponentSection(
|
|
|
|
|
InspectorPresentationModel& model,
|
|
|
|
|
const EditorSceneComponentDescriptor& descriptor) {
|
|
|
|
|
std::vector<UIEditorPropertyGridField> fields = {};
|
|
|
|
|
fields.push_back(BuildReadOnlyTextField(
|
|
|
|
|
"component." + descriptor.componentId + ".type",
|
|
|
|
|
"Type",
|
|
|
|
|
descriptor.typeName));
|
|
|
|
|
fields.push_back(BuildReadOnlyTextField(
|
|
|
|
|
"component." + descriptor.componentId + ".status",
|
|
|
|
|
"Status",
|
|
|
|
|
"Inspector not implemented"));
|
|
|
|
|
|
|
|
|
|
AppendReadOnlySection(
|
|
|
|
|
model,
|
|
|
|
|
"component." + descriptor.componentId,
|
|
|
|
|
descriptor.typeName,
|
|
|
|
|
std::move(fields));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
void AppendFallbackComponentStructureSignature(
|
|
|
|
|
const EditorSceneComponentDescriptor& descriptor,
|
|
|
|
|
std::string& signature) {
|
|
|
|
|
AppendInspectorStructureToken(signature, "fallback");
|
|
|
|
|
AppendInspectorStructureToken(signature, descriptor.componentId);
|
|
|
|
|
AppendInspectorStructureToken(signature, descriptor.typeName);
|
|
|
|
|
AppendInspectorStructureToken(signature, descriptor.removable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppendComponentStructureSignature(
|
|
|
|
|
const EditorSceneComponentDescriptor& descriptor,
|
|
|
|
|
const GameObject* gameObject,
|
|
|
|
|
const InspectorComponentEditorRegistry& componentEditorRegistry,
|
|
|
|
|
std::string& signature) {
|
|
|
|
|
const IInspectorComponentEditor* editor =
|
|
|
|
|
componentEditorRegistry.FindEditor(descriptor.typeName);
|
|
|
|
|
if (editor == nullptr) {
|
|
|
|
|
AppendFallbackComponentStructureSignature(descriptor, signature);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InspectorComponentEditorContext context =
|
|
|
|
|
BuildComponentEditorContext(descriptor, gameObject, editor);
|
|
|
|
|
AppendInspectorStructureToken(signature, descriptor.componentId);
|
|
|
|
|
AppendInspectorStructureToken(signature, descriptor.typeName);
|
|
|
|
|
AppendInspectorStructureToken(signature, descriptor.removable);
|
|
|
|
|
editor->AppendStructureSignature(context, signature);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
void AppendComponentPresentation(
|
|
|
|
|
InspectorPresentationModel& model,
|
|
|
|
|
const EditorSceneComponentDescriptor& descriptor,
|
|
|
|
|
const GameObject* gameObject,
|
2026-04-21 00:57:14 +08:00
|
|
|
const InspectorComponentEditorRegistry& componentEditorRegistry,
|
|
|
|
|
std::string& structureSignature) {
|
2026-04-19 00:03:25 +08:00
|
|
|
InspectorPresentationComponentBinding binding = {};
|
|
|
|
|
binding.componentId = descriptor.componentId;
|
|
|
|
|
binding.typeName = descriptor.typeName;
|
|
|
|
|
binding.removable = descriptor.removable;
|
|
|
|
|
|
|
|
|
|
const IInspectorComponentEditor* editor =
|
|
|
|
|
componentEditorRegistry.FindEditor(descriptor.typeName);
|
|
|
|
|
if (editor != nullptr) {
|
2026-04-21 00:57:14 +08:00
|
|
|
const InspectorComponentEditorContext context =
|
|
|
|
|
BuildComponentEditorContext(descriptor, gameObject, editor);
|
2026-04-19 00:03:25 +08:00
|
|
|
binding.displayName = context.displayName;
|
2026-04-21 00:57:14 +08:00
|
|
|
AppendComponentStructureSignature(
|
|
|
|
|
descriptor,
|
|
|
|
|
gameObject,
|
|
|
|
|
componentEditorRegistry,
|
|
|
|
|
structureSignature);
|
2026-04-19 00:03:25 +08:00
|
|
|
|
|
|
|
|
const std::size_t sectionCountBefore = model.sections.size();
|
|
|
|
|
editor->BuildSections(context, model.sections);
|
|
|
|
|
for (std::size_t sectionIndex = sectionCountBefore;
|
|
|
|
|
sectionIndex < model.sections.size();
|
|
|
|
|
++sectionIndex) {
|
|
|
|
|
for (const UIEditorPropertyGridField& field :
|
|
|
|
|
model.sections[sectionIndex].fields) {
|
|
|
|
|
binding.fieldIds.push_back(field.fieldId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
binding.displayName = descriptor.typeName;
|
2026-04-21 00:57:14 +08:00
|
|
|
AppendFallbackComponentStructureSignature(descriptor, structureSignature);
|
2026-04-19 00:03:25 +08:00
|
|
|
AppendFallbackComponentSection(model, descriptor);
|
|
|
|
|
if (!model.sections.empty()) {
|
|
|
|
|
for (const UIEditorPropertyGridField& field :
|
|
|
|
|
model.sections.back().fields) {
|
|
|
|
|
binding.fieldIds.push_back(field.fieldId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
model.componentBindings.push_back(std::move(binding));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
std::string BuildSceneObjectStructureSignature(
|
|
|
|
|
const InspectorSceneObjectSubject& sceneObject,
|
|
|
|
|
const EditorSceneRuntime& sceneRuntime,
|
|
|
|
|
const InspectorComponentEditorRegistry& componentEditorRegistry) {
|
|
|
|
|
std::string signature = {};
|
|
|
|
|
AppendInspectorStructureToken(signature, "scene");
|
|
|
|
|
for (const EditorSceneComponentDescriptor& descriptor :
|
|
|
|
|
sceneRuntime.GetSelectedComponents()) {
|
|
|
|
|
AppendComponentStructureSignature(
|
|
|
|
|
descriptor,
|
|
|
|
|
sceneObject.gameObject,
|
|
|
|
|
componentEditorRegistry,
|
|
|
|
|
signature);
|
|
|
|
|
}
|
|
|
|
|
return signature;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InspectorPresentationSyncResult SyncSceneObjectPresentationValues(
|
|
|
|
|
InspectorPresentationModel& model,
|
|
|
|
|
const InspectorSceneObjectSubject& sceneObject,
|
|
|
|
|
const EditorSceneRuntime& sceneRuntime,
|
|
|
|
|
const InspectorComponentEditorRegistry& componentEditorRegistry) {
|
|
|
|
|
InspectorPresentationSyncResult result = {};
|
|
|
|
|
const std::vector<EditorSceneComponentDescriptor> descriptors =
|
|
|
|
|
sceneRuntime.GetSelectedComponents();
|
|
|
|
|
|
|
|
|
|
model.hasSelection = true;
|
|
|
|
|
model.showHeader = false;
|
|
|
|
|
model.title = ResolveSceneObjectDisplayName(sceneObject);
|
|
|
|
|
model.subtitle = "GameObject";
|
|
|
|
|
|
|
|
|
|
for (const InspectorPresentationComponentBinding& binding :
|
|
|
|
|
model.componentBindings) {
|
|
|
|
|
const EditorSceneComponentDescriptor* descriptor =
|
|
|
|
|
FindComponentDescriptor(descriptors, binding.componentId);
|
|
|
|
|
if (descriptor == nullptr || descriptor->typeName != binding.typeName) {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const IInspectorComponentEditor* editor =
|
|
|
|
|
componentEditorRegistry.FindEditor(binding.typeName);
|
|
|
|
|
if (editor == nullptr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const InspectorComponentEditorContext context =
|
|
|
|
|
BuildComponentEditorContext(*descriptor, sceneObject.gameObject, editor);
|
|
|
|
|
for (const std::string& fieldId : binding.fieldIds) {
|
|
|
|
|
const auto location =
|
|
|
|
|
Widgets::FindUIEditorPropertyGridFieldLocation(model.sections, fieldId);
|
|
|
|
|
if (!location.IsValid() ||
|
|
|
|
|
location.sectionIndex >= model.sections.size() ||
|
|
|
|
|
location.fieldIndex >= model.sections[location.sectionIndex].fields.size()) {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.valueChanged |= editor->SyncFieldValue(
|
|
|
|
|
context,
|
|
|
|
|
model.sections[location.sectionIndex].fields[location.fieldIndex]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.success = true;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
InspectorPresentationModel BuildInspectorPresentationModel(
|
|
|
|
|
const InspectorSubject& subject,
|
|
|
|
|
const EditorSceneRuntime& sceneRuntime,
|
|
|
|
|
const InspectorComponentEditorRegistry& componentEditorRegistry) {
|
|
|
|
|
InspectorPresentationModel model = {};
|
|
|
|
|
model.hasSelection = subject.HasSelection();
|
|
|
|
|
|
|
|
|
|
switch (subject.kind) {
|
|
|
|
|
case InspectorSubjectKind::ProjectAsset: {
|
|
|
|
|
const EditorSelectionState& selection = subject.projectAsset.selection;
|
|
|
|
|
const std::string title = ResolveProjectSelectionTitle(selection);
|
|
|
|
|
const std::string typeLabel =
|
|
|
|
|
selection.directory ? std::string("Folder") : std::string("Asset");
|
|
|
|
|
|
|
|
|
|
model.title = title;
|
|
|
|
|
model.subtitle = typeLabel;
|
2026-04-21 00:57:14 +08:00
|
|
|
AppendProjectStructureSignature(selection, model.structureSignature);
|
2026-04-19 00:03:25 +08:00
|
|
|
|
|
|
|
|
AppendReadOnlySection(
|
|
|
|
|
model,
|
|
|
|
|
"project.identity",
|
|
|
|
|
"Identity",
|
|
|
|
|
{
|
|
|
|
|
BuildReadOnlyTextField("project.identity.type", "Type", typeLabel),
|
|
|
|
|
BuildReadOnlyTextField("project.identity.name", "Name", title),
|
|
|
|
|
BuildReadOnlyTextField("project.identity.id", "Id", selection.itemId)
|
|
|
|
|
});
|
|
|
|
|
AppendReadOnlySection(
|
|
|
|
|
model,
|
|
|
|
|
"project.location",
|
|
|
|
|
"Location",
|
|
|
|
|
{
|
|
|
|
|
BuildReadOnlyTextField(
|
|
|
|
|
"project.location.path",
|
|
|
|
|
"Path",
|
|
|
|
|
PathToUtf8String(selection.absolutePath))
|
|
|
|
|
});
|
|
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::SceneObject: {
|
|
|
|
|
const InspectorSceneObjectSubject& sceneObject = subject.sceneObject;
|
2026-04-21 00:57:14 +08:00
|
|
|
model.showHeader = false;
|
|
|
|
|
model.title = ResolveSceneObjectDisplayName(sceneObject);
|
2026-04-19 00:03:25 +08:00
|
|
|
model.subtitle = "GameObject";
|
2026-04-21 00:57:14 +08:00
|
|
|
AppendInspectorStructureToken(model.structureSignature, "scene");
|
2026-04-19 00:03:25 +08:00
|
|
|
|
|
|
|
|
for (const EditorSceneComponentDescriptor& descriptor :
|
|
|
|
|
sceneRuntime.GetSelectedComponents()) {
|
|
|
|
|
AppendComponentPresentation(
|
|
|
|
|
model,
|
|
|
|
|
descriptor,
|
|
|
|
|
sceneObject.gameObject,
|
2026-04-21 00:57:14 +08:00
|
|
|
componentEditorRegistry,
|
|
|
|
|
model.structureSignature);
|
2026-04-19 00:03:25 +08:00
|
|
|
}
|
|
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::None:
|
|
|
|
|
default:
|
2026-04-21 00:57:14 +08:00
|
|
|
model.title = ResolveNoneSelectionTitle();
|
|
|
|
|
model.subtitle = ResolveNoneSelectionSubtitle();
|
|
|
|
|
AppendNoneSelectionStructureSignature(model.structureSignature);
|
2026-04-19 00:03:25 +08:00
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
std::string BuildInspectorStructureSignature(
|
|
|
|
|
const InspectorSubject& subject,
|
|
|
|
|
const EditorSceneRuntime& sceneRuntime,
|
|
|
|
|
const InspectorComponentEditorRegistry& componentEditorRegistry) {
|
|
|
|
|
switch (subject.kind) {
|
|
|
|
|
case InspectorSubjectKind::ProjectAsset: {
|
|
|
|
|
std::string signature = {};
|
|
|
|
|
AppendProjectStructureSignature(subject.projectAsset.selection, signature);
|
|
|
|
|
return signature;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::SceneObject:
|
|
|
|
|
return BuildSceneObjectStructureSignature(
|
|
|
|
|
subject.sceneObject,
|
|
|
|
|
sceneRuntime,
|
|
|
|
|
componentEditorRegistry);
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::None:
|
|
|
|
|
default: {
|
|
|
|
|
std::string signature = {};
|
|
|
|
|
AppendNoneSelectionStructureSignature(signature);
|
|
|
|
|
return signature;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InspectorPresentationSyncResult SyncInspectorPresentationModelValues(
|
|
|
|
|
InspectorPresentationModel& model,
|
|
|
|
|
const InspectorSubject& subject,
|
|
|
|
|
const EditorSceneRuntime& sceneRuntime,
|
|
|
|
|
const InspectorComponentEditorRegistry& componentEditorRegistry) {
|
|
|
|
|
switch (subject.kind) {
|
|
|
|
|
case InspectorSubjectKind::ProjectAsset:
|
|
|
|
|
model.hasSelection = true;
|
|
|
|
|
model.showHeader = true;
|
|
|
|
|
model.title = ResolveProjectSelectionTitle(subject.projectAsset.selection);
|
|
|
|
|
model.subtitle =
|
|
|
|
|
subject.projectAsset.selection.directory ? "Folder" : "Asset";
|
|
|
|
|
return InspectorPresentationSyncResult{
|
|
|
|
|
.success = true,
|
|
|
|
|
.valueChanged = false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::SceneObject:
|
|
|
|
|
return SyncSceneObjectPresentationValues(
|
|
|
|
|
model,
|
|
|
|
|
subject.sceneObject,
|
|
|
|
|
sceneRuntime,
|
|
|
|
|
componentEditorRegistry);
|
|
|
|
|
|
|
|
|
|
case InspectorSubjectKind::None:
|
|
|
|
|
default:
|
|
|
|
|
model.hasSelection = false;
|
|
|
|
|
model.showHeader = true;
|
|
|
|
|
model.title = ResolveNoneSelectionTitle();
|
|
|
|
|
model.subtitle = ResolveNoneSelectionSubtitle();
|
|
|
|
|
return InspectorPresentationSyncResult{
|
|
|
|
|
.success = true,
|
|
|
|
|
.valueChanged = false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
} // namespace XCEngine::UI::Editor::App
|